I have a need to view thousands of thumbnails very quickly in a cross-platform application (labeling/verifying images for machine learning). I wrote a thumbnail manager that takes care of creating 200-pixel-high thumbnails (for example) as needed. I wrote a JavaFX app that creates a ScrollPane with a TilePane with 2000 children, each with an ImageView that contains one of these 200x200 images read from disk into an ImageBuffer and converted to a JavaFX Image. I load, convert and add the images to the TilePane in the background (using Platform.runLater), and that all seems to work well.
With 2000 thumbnails at 200x200, the TilePane scrolls really fast, just like I was hoping. But at 400x400, or when I go to 16000 thumbnails (even at 100x100), the display slows to a crawl, with a "spinning lollipop" for several seconds between each screen update.
I'm running with 6GB allocated to the JVM. I told each ImageView to setCache(true) and setCacheHint(CacheHint.SPEED). Everything is loaded into memory and already rendered, and it's still really slow.
Is JavaFX doing a bunch of scaling of images or something on the fly? I'm just wondering what I can do to make this a lot faster.
Below is a sample of what I'm doing, except that this example generates images from scratch instead of reading thumbnails (and generating if needed). But it reproduces the problem:
- With 200 panes, it runs nice and fast (on my laptop).
- With 2000 panes, it is annoyingly slow.
- With 16000 panes, it spins for several seconds between updates, which is unusable.
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);
}
}
Update: It turns out that if I replace "TilePane" with "ListView" in the above code, then it scrolls nice and fast, even with 16,000 tiles. But then the problem is that it's in a single vertical list instead of a grid of thumbnails. Perhaps I should ask this as a new topic, but this leads me to the question of how I can extend a ListView to display its elements in a (fixed-size) 2-D grid instead of a 1-D list.