Попытка просмотреть тысячи миниатюр в JavaFX, и это слишком медленно - PullRequest
3 голосов
/ 23 января 2020

Мне нужно очень быстро просматривать тысячи миниатюр в кроссплатформенном приложении (маркировка / проверка изображений для машинного обучения). Я написал менеджер миниатюр, который заботится о создании миниатюр в 200 пикселей (например) по мере необходимости. Я написал приложение JavaFX, которое создает ScrollPane с TilePane с 2000 дочерними элементами, каждое с ImageView, которое содержит одно из этих изображений 200x200, считанных с диска в ImageBuffer и преобразованных в изображение JavaFX. Я загружаю, конвертирую и добавляю изображения в TilePane в фоновом режиме (используя Platform.runLater), и, кажется, все работает хорошо.

С 2000 миниатюр в 200x200, TilePane прокручивается очень быстро, как я надеялся. Но при разрешении 400x400, или когда я go до 16000 миниатюр (даже при 100x100), дисплей замедляется до ползания, с «вращающимся леденцом» в течение нескольких секунд между каждым обновлением экрана.

Я использую 6 ГБ, выделенных для JVM. Я сказал каждому 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 плиток. Но тогда проблема в том, что он находится в одном вертикальном списке, а не в виде сетки миниатюр. Возможно, мне следует задать это как новую топи c, но это приводит меня к вопросу о том, как я могу расширить ListView для отображения его элементов в двумерной сетке (фиксированного размера) вместо одномерного списка.

1 Ответ

1 голос
/ 29 января 2020

Я нашел элемент управления GridView с открытым исходным кодом, который пытается имитировать c действия ListView, но в сетке, что я и искал. Кажется, отлично работает. Кажется, он не имеет встроенного множественного выбора, как это делает ListView, но я могу посмотреть на добавленную поддержку для этого (и в идеале передать его обратно в проект с открытым исходным кодом).

Вот код, который демонстрирует его использовать. Я должен был сделать следующее Maven:

<dependency>
  <groupId>org.controlsfx</groupId>
  <artifactId>controlsfx</artifactId>
  <version>8.0.6_20</version>
</dependency>

И вот код Java. У меня были проблемы со всеми вызовами "Platform.runLater ()", которые насыщали поток пользовательского интерфейса JavaFX, и пользовательский интерфейс не отвечал. Так что теперь фоновый поток помещает все изображения в параллельную очередь (как «производитель»), а еще один поток («потребитель») считывает до 1000 изображений из очереди и добавляет их во временный список, а затем выполняет один вызов через Platform.runLater (), чтобы добавить их в пользовательский интерфейс одним действием. Затем он блокирует и ожидает освобождения семафора с помощью вызова runLater (), а затем собирает другой пакет изображений для отправки на следующий вызов runLater (). Таким образом, пользовательский интерфейс может реагировать во время добавления изображений в сетку.

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 изображений без замедления и остается отзывчивым при добавлении изображений. Поэтому я думаю, что это послужит хорошей отправной точкой.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...