如何在 JavaFX 中以大频率显示图像?
Posted
技术标签:
【中文标题】如何在 JavaFX 中以大频率显示图像?【英文标题】:How to show images in a large frequency in JavaFX? 【发布时间】:2020-06-25 08:51:14 【问题描述】:我的应用程序以 CPU 的速度生成热图图像(大约每秒 30-60 个),我想在单个“实时热图”中显示它们。在 AWT/Swing 中,我可以将它们绘制成一个 JPanel,它就像一个魅力。 最近,我切换到JavaFX,想在这里实现同样的效果;起初,我尝试使用Canvas,虽然速度很慢但还可以,但存在严重的内存泄漏问题,导致应用程序崩溃。现在,我尝试了ImageView 组件——这显然太慢了,因为图像变得相当滞后(在每次新迭代中使用ImageView.setImage)。据我了解,setImage 并不能保证函数完成时图像实际显示出来。
我的印象是我走错了路,以一种不习惯的方式使用这些组件。如何每秒显示 30-60 张图片?
编辑:一个非常简单的测试应用程序。您将需要 JHeatChart 库。 请注意,在台式机上,我得到大约 70-80 FPS 并且可视化效果还可以且流畅,但在较小的树莓派(我的目标机器)上,我得到大约 30 FPS,但可视化效果非常卡。
package sample;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.tc33.jheatchart.HeatChart;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
public class Main extends Application
ImageView imageView = new ImageView();
final int scale = 15;
@Override
public void start(Stage primaryStage)
Thread generator = new Thread(() ->
int col = 0;
LinkedList<Long> fps = new LinkedList<>();
while (true)
fps.add(System.currentTimeMillis());
double[][] matrix = new double[48][128];
for (int i = 0; i < 48; i++)
for (int j = 0; j < 128; j++)
matrix[i][j] = col == j ? Math.random() : 0;
col = (col + 1) % 128;
HeatChart heatChart = new HeatChart(matrix, 0, 1);
heatChart.setShowXAxisValues(false);
heatChart.setShowYAxisValues(false);
heatChart.setLowValueColour(java.awt.Color.black);
heatChart.setHighValueColour(java.awt.Color.white);
heatChart.setAxisThickness(0);
heatChart.setChartMargin(0);
heatChart.setCellSize(new Dimension(1, 1));
long currentTime = System.currentTimeMillis();
fps.removeIf(elem -> currentTime - elem > 1000);
System.out.println(fps.size());
imageView.setImage(SwingFXUtils.toFXImage((BufferedImage) scale(heatChart.getChartImage(), scale), null));
);
VBox box = new VBox();
box.getChildren().add(imageView);
Scene scene = new Scene(box, 1920, 720);
primaryStage.setScene(scene);
primaryStage.show();
generator.start();
public static void main(String[] args)
launch(args);
private static Image scale(Image image, int scale)
BufferedImage res = new BufferedImage(image.getWidth(null) * scale, image.getHeight(null) * scale,
BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(scale, scale);
AffineTransformOp scaleOp =
new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
return scaleOp.filter((BufferedImage) image, res);
【问题讨论】:
你可能会觉得this问答很有趣 大概你正在某个地方的后台线程中生成图像。我会: 1. 将它们生成为int[]
数据并发布它们(小心确保您以一种跨多个线程保持活力和数据完整性的方式执行此操作)。 2. 在 UI 应用程序中,创建一个WritableImage
并将其显示在一个ImageView
中,以及 3. 使用一个AnimationTimer
检索最新的图像数据并将它们传递给图像setPixels(...)
上的setPixels(...)
方法@ 方法@ .如果您可以将一个简单的测试应用程序放在一起,它可能会更容易提供帮助......
这可能很重要。例如,JavaFX 13 添加了 PixelBuffer
,据我了解,它在与 JavaFX 一起使用时显着提高了 vlcj 的性能(尽管该库在本机代码中完成了大部分工作)。
您的代码效率极低。看看这个例子github.com/mipastgt/JFXToolsAndDemos#awtimage。您可以将热图直接渲染到此图像中,并避免所有性能破坏图像生成/转换/副本...
是的:即使使用热图库(我不太相信它会给你太多),HeatChart
类也是可变的。所以你可以简单地创建一个矩阵,一个HeatChart
,然后更新矩阵并每次调用setZValues()
。然后只需发布新图像。但是,如上所述,直接写入图像可能会更好。
【参考方案1】:
您的代码从后台线程更新 UI,这是绝对不允许的。您需要确保从 FX 应用程序线程进行更新。您还想尝试“限制”实际的 UI 更新,使其在每个 JavaFX 帧渲染中不超过一次。最简单的方法是使用AnimationTimer
,每次渲染帧时都会调用其handle()
方法。
这是执行此操作的代码版本:
import java.awt.Dimension;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicReference;
import org.tc33.jheatchart.HeatChart;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application
ImageView imageView = new ImageView();
final int scale = 15;
@Override
public void start(Stage primaryStage)
AtomicReference<BufferedImage> image = new AtomicReference<>();
Thread generator = new Thread(() ->
int col = 0;
LinkedList<Long> fps = new LinkedList<>();
while (true)
fps.add(System.currentTimeMillis());
double[][] matrix = new double[48][128];
for (int i = 0; i < 48; i++)
for (int j = 0; j < 128; j++)
matrix[i][j] = col == j ? Math.random() : 0;
col = (col + 1) % 128;
HeatChart heatChart = new HeatChart(matrix, 0, 1);
heatChart.setShowXAxisValues(false);
heatChart.setShowYAxisValues(false);
heatChart.setLowValueColour(java.awt.Color.black);
heatChart.setHighValueColour(java.awt.Color.white);
heatChart.setAxisThickness(0);
heatChart.setChartMargin(0);
heatChart.setCellSize(new Dimension(1, 1));
long currentTime = System.currentTimeMillis();
fps.removeIf(elem -> currentTime - elem > 1000);
System.out.println(fps.size());
image.set((BufferedImage) scale(heatChart.getChartImage(), scale));
);
VBox box = new VBox();
box.getChildren().add(imageView);
Scene scene = new Scene(box, 1920, 720);
primaryStage.setScene(scene);
primaryStage.show();
generator.setDaemon(true);
generator.start();
AnimationTimer animation = new AnimationTimer()
@Override
public void handle(long now)
BufferedImage img = image.getAndSet(null);
if (img != null)
imageView.setImage(SwingFXUtils.toFXImage(img, null));
;
animation.start();
public static void main(String[] args)
launch(args);
private static Image scale(Image image, int scale)
BufferedImage res = new BufferedImage(image.getWidth(null) * scale, image.getHeight(null) * scale,
BufferedImage.TYPE_INT_ARGB);
AffineTransform at = new AffineTransform();
at.scale(scale, scale);
AffineTransformOp scaleOp = new AffineTransformOp(at, AffineTransformOp.TYPE_NEAREST_NEIGHBOR);
return scaleOp.filter((BufferedImage) image, res);
使用AtomicReference
包装缓冲图像可确保它在两个线程之间安全共享。
在我的机器上,每秒生成大约 130 张图像;请注意,并非所有内容都会显示,因为每次 JavaFX 图形框架显示一帧(通常以 60fps 的速度限制)时,只会显示最新的一个。
如果您想确保显示生成的所有图像,即通过 JavaFX 帧速率限制图像生成,那么您可以使用BlockingQueue
来存储图像:
// AtomicReference<BufferedImage> image = new AtomicReference<>();
// Size of the queue is a trade-off between memory consumption
// and smoothness (essentially works as a buffer size)
BlockingQueue<BufferedImage> image = new ArrayBlockingQueue<>(5);
// ...
// image.set((BufferedImage) scale(heatChart.getChartImage(), scale));
try
image.put((BufferedImage) scale(heatChart.getChartImage(), scale));
catch (InterruptedException exc)
Thread.currentThread.interrupt();
和
@Override
public void handle(long now)
BufferedImage img = image.poll();
if (img != null)
imageView.setImage(SwingFXUtils.toFXImage(img, null));
代码效率很低,因为每次迭代都会生成一个新矩阵、新的HeatChart
等。这会导致在堆上创建许多对象并迅速丢弃,这会导致 GC 过于频繁地运行,尤其是在小内存机器上。也就是说,我将最大堆大小设置为 64MB (-Xmx64m
) 运行它,它仍然运行良好。您或许可以优化代码,但使用如上所示的AnimationTimer
,更快地生成图像不会对 JavaFX 框架造成任何额外的压力。我建议调查使用HeatChart
(即setZValues()
)的可变性以避免创建太多对象,和/或使用PixelBuffer
直接将数据写入图像视图(这需要在FX 应用程序上完成线程)。
这是一个不同的示例,它(几乎)完全最小化了对象的创建,使用一个屏幕外的int[]
数组来计算数据,并使用一个屏幕上的int[]
数组来显示它。有一些低级线程细节来确保屏幕上的数组只能在一致的状态下看到。屏幕上的数组用于PixelBuffer
的底层,而WritableImage
又用于WritableImage
。
该类生成图像数据:
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
public class ImageGenerator
private final int width;
private final int height;
// Keep two copies of the data: one which is not exposed
// that we modify on the fly during computation;
// another which we expose publicly.
// The publicly exposed one can be viewed only in a complete
// state if operations on it are synchronized on this object.
private final int[] privateData ;
private final int[] publicData ;
private final long[] frameTimes ;
private int currentFrameIndex ;
private final AtomicLong averageGenerationTime ;
private final ReentrantLock lock ;
private static final double TWO_PI = 2 * Math.PI;
private static final double PI_BY_TWELVE = Math.PI / 12; // 15 degrees
public ImageGenerator(int width, int height)
super();
this.width = width;
this.height = height;
privateData = new int[width * height];
publicData = new int[width * height];
lock = new ReentrantLock();
this.frameTimes = new long[100];
this.averageGenerationTime = new AtomicLong();
public void generateImage(double angle)
// compute in private data copy:
int minDim = Math.min(width, height);
int minR2 = minDim * minDim / 4;
for (int x = 0; x < width; x++)
int xOff = x - width / 2;
int xOff2 = xOff * xOff;
for (int y = 0; y < height; y++)
int index = x + y * width;
int yOff = y - height / 2;
int yOff2 = yOff * yOff;
int r2 = xOff2 + yOff2;
if (r2 > minR2)
privateData[index] = 0xffffffff; // white
else
double theta = Math.atan2(yOff, xOff);
double delta = Math.abs(theta - angle);
if (delta > TWO_PI - PI_BY_TWELVE)
delta = TWO_PI - delta;
if (delta < PI_BY_TWELVE)
int green = (int) (255 * (1 - delta / PI_BY_TWELVE));
privateData[index] = (0xff << 24) | (green << 8); // green, fading away from center
else
privateData[index] = 0xff << 24; // black
// copy computed data to public data copy:
lock.lock();
try
System.arraycopy(privateData, 0, publicData, 0, privateData.length);
finally
lock.unlock();
frameTimes[currentFrameIndex] = System.nanoTime() ;
int nextIndex = (currentFrameIndex + 1) % frameTimes.length ;
if (frameTimes[nextIndex] > 0)
averageGenerationTime.set((frameTimes[currentFrameIndex] - frameTimes[nextIndex]) / frameTimes.length);
currentFrameIndex = nextIndex ;
public void consumeData(Consumer<int[]> consumer)
lock.lock();
try
consumer.accept(publicData);
finally
lock.unlock();
public long getAverageGenerationTime()
return averageGenerationTime.get() ;
这是用户界面:
import java.nio.IntBuffer;
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.PixelWriter;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class AnimationApp extends Application
private final int size = 400 ;
private IntBuffer buffer ;
@Override
public void start(Stage primaryStage) throws Exception
// background image data generation:
ImageGenerator generator = new ImageGenerator(size, size);
// Generate new image data as fast as possible:
Thread thread = new Thread(() ->
while( true )
long now = System.currentTimeMillis() ;
double angle = 2 * Math.PI * (now % 10000) / 10000 - Math.PI;
generator.generateImage(angle);
);
thread.setDaemon(true);
thread.start();
generator.consumeData(data -> buffer = IntBuffer.wrap(data));
PixelFormat<IntBuffer> format = PixelFormat.getIntArgbPreInstance() ;
PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(size, size, buffer, format);
WritableImage image = new WritableImage(pixelBuffer);
BorderPane root = new BorderPane(new ImageView(image));
Label fps = new Label("FPS: ");
root.setTop(fps);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setTitle("Give me a ping, Vasili. ");
primaryStage.show();
AnimationTimer animation = new AnimationTimer()
@Override
public void handle(long now)
// Update image, ensuring we only see the underlying
// data in a consistent state:
generator.consumeData(data ->
pixelBuffer.updateBuffer(pb -> null);
);
long aveGenTime = generator.getAverageGenerationTime() ;
if (aveGenTime > 0)
double aveFPS = 1.0 / (aveGenTime / 1_000_000_000.0);
fps.setText(String.format("FPS: %.2f", aveFPS));
;
animation.start();
public static void main(String[] args)
Application.launch(args);
对于不依赖于 JavaFX 13 PixelBuffer
的版本,您可以修改此类以使用 PixelWriter
(AIUI 效率不会那么高,但在本示例中运行起来同样流畅) ):
// generator.consumeData(data -> buffer = IntBuffer.wrap(data));
PixelFormat<IntBuffer> format = PixelFormat.getIntArgbPreInstance() ;
// PixelBuffer<IntBuffer> pixelBuffer = new PixelBuffer<>(size, size, buffer, format);
// WritableImage image = new WritableImage(pixelBuffer);
WritableImage image = new WritableImage(size, size);
PixelWriter pixelWriter = image.getPixelWriter() ;
和
AnimationTimer animation = new AnimationTimer()
@Override
public void handle(long now)
// Update image, ensuring we only see the underlying
// data in a consistent state:
generator.consumeData(data ->
// pixelBuffer.updateBuffer(pb -> null);
pixelWriter.setPixels(0, 0, size, size, format, data, 0, size);
);
long aveGenTime = generator.getAverageGenerationTime() ;
if (aveGenTime > 0)
double aveFPS = 1.0 / (aveGenTime / 1_000_000_000.0);
fps.setText(String.format("FPS: %.2f", aveFPS));
;
【讨论】:
虽然这个答案包含很多好的提示,但最初的问题仍然存在:图像仍然有 Swing-Code 没有的跳跃、暂停和滞后(至少在树莓派上)。生成了 30-40 张图片,但 ImageView 貌似跟不上。一个附带问题是:是否至少可以等待 ImageView 发生更新,以便我可以顺序“生成-绘制-生成-绘制”,降低帧率但保证平滑(虽然很慢)“视频”? @fortuneNext 是的;见更新。不过,最终,我认为您找到的热图库并没有给您带来很多好处。如果我是你,我会从头开始实施。我很想知道我的另一个示例是否在树莓派上运行良好……这至少表明使用这些技术会有所帮助。 @fortuneNext 在树莓派上运行应用程序时,您能否使用分析器(例如 VisualVM)来找出瓶颈所在? @James_D 您使用 BlockingQueue 的解决方案带来了平稳但缓慢的重播(这让我接受了您的回答:) 遗憾的是我无法尝试下面的代码,因为 PixelBuffer 是我无法获得的 JavaFX 13还在 ARM 上工作... @fortuneNext 在这种情况下,我建议尝试使用这种方法;即将您的热图生成为原始像素数据。您正在使用的库是为在 AWT/Swing 中使用而设计的,因为它会生成BufferedImage
s;您可能会花费大量 CPU 时间将这些转换为 FX 图像。基本的热图非常简单;实际上只是一堆矩形块,所以应该很容易将其表示为颜色数据的int[ ]
。以上是关于如何在 JavaFX 中以大频率显示图像?的主要内容,如果未能解决你的问题,请参考以下文章
如何在 Qt 小部件中以 PreserveAspectFit 的比例显示图像