javafx - 尝试在 JavaFX 中查看数千个缩略图,速度太慢
问题描述
我需要在跨平台应用程序(标记/验证图像以进行机器学习)中非常快速地查看数千个缩略图。我编写了一个缩略图管理器,负责根据需要创建 200 像素高的缩略图(例如)。我编写了一个 JavaFX 应用程序,它创建了一个带有 2000 个子对象的 TilePane 的 ScrollPane,每个子对象都有一个 ImageView,其中包含从磁盘读取到 ImageBuffer 并转换为 JavaFX 图像的这些 200x200 图像之一。我在后台加载、转换图像并将其添加到 TilePane(使用 Platform.runLater),这一切似乎都运行良好。
TilePane 有 2000 个 200x200 的缩略图,滚动速度非常快,就像我希望的那样。但是在 400x400,或者当我转到 16000 个缩略图(即使是 100x100)时,显示速度会变慢,每次屏幕更新之间都会出现几秒钟的“旋转棒棒糖”。
我正在运行分配给 JVM 的 6GB。我告诉每个 ImageView setCache(true) 和 setCacheHint(CacheHint.SPEED)。一切都加载到内存中并已经渲染,而且仍然很慢。
JavaFX 是否在进行大量图像缩放或其他操作?我只是想知道我能做些什么来让它更快。
下面是我正在做的一个示例,除了这个示例从头开始生成图像而不是读取缩略图(并在需要时生成)。但它重现了问题:
- 它有 200 个窗格,运行良好且快速(在我的笔记本电脑上)。
- 有 2000 个窗格,速度慢得令人讨厌。
- 有 16000 个窗格,它在更新之间旋转几秒钟,这是不可用的。
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.CacheHint;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.TilePane;
import javafx.stage.Stage;
public class ThumbnailBrowser extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
// Create a Scene with a ScrollPane that contains a TilePane.
TilePane tilePane = new TilePane();
tilePane.getStyleClass().add("pane");
tilePane.setCache(true);
tilePane.setCacheHint(CacheHint.SPEED);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
scrollPane.setContent(tilePane);
Scene scene = new Scene(scrollPane, 1000, 600);
primaryStage.setScene(scene);
// Start showing the UI before taking time to load any images
primaryStage.show();
// Load images in the background so the UI stays responsive.
ExecutorService executor = Executors.newFixedThreadPool(20);
executor.submit(() -> {
addImagesToGrid(tilePane);
});
}
private void addImagesToGrid(TilePane tilePane) {
int size = 200;
int numCells = 2000;
for (int i = 0; i < numCells; i++) {
// (In the real application, get a list of image filenames, read each image's thumbnail, generating it if needed.
// (In this minimal reproducible code, we'll just create a new dummy image for each ImageView)
ImageView imageView = new ImageView(createFakeImage(i, size));
imageView.setPreserveRatio(true);
imageView.setFitHeight(size);
imageView.setFitWidth(size);
imageView.setCache(true);
imageView.setCacheHint(CacheHint.SPEED);
Platform.runLater(() -> tilePane.getChildren().add(imageView));
}
}
// Create an image with a bunch of rectangles in it just to have something to display.
private Image createFakeImage(int imageIndex, int size) {
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
for (int i = 1; i < size; i ++) {
g.setColor(new Color(i * imageIndex % 256, i * 2 * (imageIndex + 40) % 256, i * 3 * (imageIndex + 60) % 256));
g.drawRect(i, i, size - i * 2, size - i * 2);
}
return SwingFXUtils.toFXImage(image, null);
}
}
更新:事实证明,如果我在上面的代码中将“TilePane”替换为“ListView”,那么即使有 16,000 个图块,它也能快速滚动。但是问题在于它位于单个垂直列表中,而不是缩略图网格中。也许我应该将此作为一个新主题提出,但这使我想到了如何扩展 ListView 以在(固定大小)二维网格而不是一维列表中显示其元素的问题。
解决方案
我发现了一个开源GridView控件,它试图模仿 ListView 的功能,但在网格中,这正是我所寻找的。它似乎工作得很好。它似乎没有像 ListView 那样内置多选功能,但我可以考虑添加对此的支持(最好将其提交回开源项目)。
这是演示其使用的代码。我必须做以下 Maven 包括:
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>8.0.6_20</version>
</dependency>
然后是 Java 代码。我遇到了所有“Platform.runLater()”调用使 JavaFX UI 线程饱和,使 UI 无响应的问题。所以现在后台线程将所有图像放在并发队列中(作为“生产者”),另一个线程(“消费者”)从队列中读取多达 1000 张图像并将它们添加到临时列表中,然后通过“Platform.runLater()”进行一次调用,通过一个操作将它们添加到 UI。然后它会阻塞并等待 runLater() 调用释放信号量,然后再收集另一批图像以发送到下一次调用 runLater()。这样,UI 可以在将图像添加到网格时做出响应。
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.CacheHint;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import org.controlsfx.control.GridView;
import org.controlsfx.control.cell.ImageGridCell;
// Demo class to illustrate the slowdown problem without worrying about thumbnail generation or fetching.
public class ThumbnailGridViewBrowser extends Application {
private static final int CELL_SIZE = 200;
private final ExecutorService executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
// Create a Scene with a ScrollPane that contains a TilePane.
GridView<Image> gridView = new GridView<>();
gridView.setCellFactory(gridView1 -> new ImageGridCell());
gridView.getStyleClass().add("pane");
gridView.setCache(true);
gridView.setCacheHint(CacheHint.SPEED);
gridView.setCellWidth(CELL_SIZE);
gridView.setCellHeight(CELL_SIZE);
gridView.setHorizontalCellSpacing(10);
gridView.setVerticalCellSpacing(10);
ScrollPane scrollPane = new ScrollPane();
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
scrollPane.setContent(gridView);
primaryStage.setScene(new Scene(scrollPane, 1000, 600));
// Start showing the UI before taking time to load any images
primaryStage.show();
// Load images in the background so the UI stays responsive.
executor.submit(() -> addImagesToGrid(gridView));
// Quit the application when the window is closed.
primaryStage.setOnCloseRequest(x -> {
executor.shutdown();
Platform.exit();
System.exit(0);
});
}
private static final Image POISON_PILL = createFakeImage(1, 1);
private void addImagesToGrid(GridView<Image> gridView) {
int numCells = 16000;
final Queue<Image> imageQueue = new ConcurrentLinkedQueue<>();
executor.submit(() -> deliverImagesToGrid(gridView, imageQueue));
for (int i = 0; i < numCells; i++) {
// (In the real application, get a list of image filenames, read each image's thumbnail, generating it if needed.
// (In this minimal reproducible code, we'll just create a new dummy image for each ImageView)
imageQueue.add(createFakeImage(i, CELL_SIZE));
}
// Add poison image to signal the end of the queue.
imageQueue.add(POISON_PILL);
}
private void deliverImagesToGrid(GridView<Image> gridView, Queue<Image> imageQueue) {
try {
Semaphore semaphore = new Semaphore(1);
semaphore.acquire(); // Get the one and only permit
boolean done = false;
while (!done) {
List<Image> imagesToAdd = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
final Image image = imageQueue.poll();
if (image == null) {
break; // Queue is now empty, so quit adding any to the list
}
else if (image == POISON_PILL) {
done = true;
}
else {
imagesToAdd.add(image);
}
}
if (imagesToAdd.size() > 0) {
Platform.runLater(() ->
{
try {
gridView.getItems().addAll(imagesToAdd);
}
finally {
semaphore.release();
}
});
// Block until the items queued up via Platform.runLater() have been processed by the UI thread and release() has been called.
semaphore.acquire();
}
}
}
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
// Create an image with a bunch of rectangles in it just to have something to display.
private static Image createFakeImage(int imageIndex, int size) {
BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
for (int i = 1; i < size; i ++) {
g.setColor(new Color(i * imageIndex % 256, i * 2 * (imageIndex + 40) % 256, i * 3 * (imageIndex + 60) % 256));
g.drawRect(i, i, size - i * 2, size - i * 2);
}
return SwingFXUtils.toFXImage(image, null);
}
}
此解决方案确实显示了 16,000 张图像,并且没有减速,并且在添加图像时保持响应。所以我认为这是一个很好的起点。
推荐阅读
- reactjs - 带有令牌的确认电子邮件来自 Rails API + React 应用程序。如果是 api,重定向到哪里?
- android - Android Studio - 项目窗口中没有更改的编译器错误
- android - 在项目“应用程序”中,已解决的 Google Play 服务库依赖项依赖于另一个确切版本(例如“[16.0.0]”,但尚未解决
- c++ - 如何以安全的方式通过 C++ 应用程序通过 SQL 插入信息
- javascript - 每次我刷新 expo 客户端时,SecureStore 都会删除我的令牌
- cookies - 由 Google Maps Javascript API 设置的 Cookie
- javascript - Firebase Analytics:我可以检查在 Firebase 更新之前是否记录了 Firebase Javascript Analytics 事件吗?
- python - 如何按行数在 python 中找到唯一记录?
- java - RxJava 和 Single.zip 中的嵌套 Single.flatMap 是一样的吗?
- ios - 视图控制器不遵守 XIB 自动调整大小掩码