首页 > 解决方案 > 尝试在 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 是否在进行大量图像缩放或其他操作?我只是想知道我能做些什么来让它更快。

下面是我正在做的一个示例,除了这个示例从头开始生成图像而不是读取缩略图(并在需要时生成)。但它重现了问题:

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 以在(固定大小)二维网格而不是一维列表中显示其元素的问题。

标签: javafxthumbnails

解决方案


我发现了一个开源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 张图像,并且没有减速,并且在添加图像时保持响应。所以我认为这是一个很好的起点。


推荐阅读