在 JavaFX 中显示原始字节数组图像(没有 SwingFXUtils)

Show raw byte array Image in JavaFX (without SwingFXUtils)

我目前正在处理一个项目,我必须处理非常大的图像 (>> 100mb)。 图像采用原始字节数组的格式(现在只有灰度图像,以后的彩色图像每个通道都有一个字节数组)。

我想在 JavaFX imageView 中显示图像。所以我必须在尽可能短的时间内将给定的“原始”图像数据转换为 JavaFX 图像。

我已经尝试了很多解决方案,我在此处和 Whosebug 以及其他来源找到了这些解决方案。

SwingFXUtils

最流行(也是最简单)的解决方案是从原始数据构造一个 BufferedImage 并使用 SwingFXUtils.toFXImage(...) 将其转换为 JavaFX 图像。 在大约 100mb (8184*12000) 的图像上,我测量了以下时间。

  1. BufferedImage 是在 ca. 中创建的。 20-40 毫秒。
  2. 通过 SwingFXUtils.toFXImage(...) 转换为 JavaFX Image 需要 700 多毫秒。这对我的需求来说太多了。

编码图像并将其作为 ByteArrayInputStream 读取

我在这里 () 找到的一种方法是使用 OpenCVs 功能将图像编码为类似 *.bmp 的格式,并直接从 ByteArray 构建 JavaFX 图像。

这个解决方案要复杂得多(需要 OpenCV 或其他一些编码 library/algorithm)并且编码似乎增加了额外的计算步骤。

问题

所以我正在寻找一种更有效的方法来做到这一点。大多数解决方案最后都使用 SwingFXUtils,或者通过遍历所有像素来转换它们来解决问题(这是最慢的可能解决方案)。 有没有一种方法可以实现比 SwingFXUtils.toFXImage(...) 更有效的函数,或者直接从字节数组构建 JavaFX 图像。 也许还有一种方法,可以直接在 JavaFX 中绘制 BufferedImage。因为恕我直言,JavaFX 图像不会带来任何优势,只会让事情变得复杂。

感谢您的回复。

跟进@James_D在评论中的建议以及您对灰度图像的要求:

使用 javafx.scene.image.PixelBuffer 但将 javafx.scene.image.PixelFormat 指定为 BYTE_INDEXED

使用 PixelFormat.createByteIndexedInstance() 或 PixelFormat.createByteIndexedPremultipliedInstance() 将颜色 i 的调色板设置为 RGB (i,i,i) 或 RGBA (i,i,i,1.0)。

那么您应该能够输入 one-channel 字节灰度值数组。

从评论中提炼信息,似乎有两个可行的选择,都使用 WritableImage.

其中,可以使用PixelWriter设置图像中的像素,使用原始字节数据和BYTE_INDEXEDPixelFormat。下面演示了这种方法。在我的系统上生成实际的字节数组数据需要大约 2.5 秒;创建(大)WritableImage 大约需要 0.15 秒,将数据绘制到图像中大约需要 0.12 秒。

import java.nio.ByteBuffer;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;


public class App extends Application {

    @Override
    public void start(Stage stage) {
       int width = 12000 ;
       int height = 8184 ;
       byte[] data = new byte[width*height];

       int[] colors = new int[256];
       for (int i = 0 ; i < colors.length ; i++) {
           colors[i] = (255<<24) | (i << 16) | (i << 8) | i ;
       }
       PixelFormat<ByteBuffer> format = PixelFormat.createByteIndexedInstance(colors);

       
       long start = System.nanoTime();
       
       for (int y = 0 ; y < height ; y++) {
           for (int x = 0 ; x < width; x++) {
               long dist2 = (1L * x - width/2) * (x- width/2) + (y - height/2) * (y-height/2);
               double dist = Math.sqrt(dist2);
               double val = (1 + Math.cos(Math.PI * dist / 1000)) / 2;
               data[x + y * width] = (byte)(val * 255);
           }
       }
       
       long imageDataCreated = System.nanoTime();
       
       
       WritableImage img = new WritableImage(width, height);
       
       long imageCreated = System.nanoTime();
       
       img.getPixelWriter().setPixels(0, 0, width, height, format, data, 0, width);
       
       long imageDrawn = System.nanoTime() ;
       
       ImageView imageView = new ImageView();
       imageView.setPreserveRatio(true);
       imageView.setImage(img);
       
       long imageViewCreated = System.nanoTime();
       
       BorderPane root = new BorderPane(imageView);
       imageView.fitWidthProperty().bind(root.widthProperty());
       imageView.fitHeightProperty().bind(root.heightProperty());
       Scene scene = new Scene(root, 800, 800);
       stage.setScene(scene);
       stage.show();
       
       long stageShowCalled = System.nanoTime();
       
       double nanosPerMilli = 1_000_000.0 ;
       
       System.out.printf(
               "Data creation time: %.3f%n"
            + "Image Creation Time: %.3f%n"
            + "Image Drawing Time: %.3f%n"
            + "ImageView Creation Time: %.3f%n"
            + "Stage Show Time: %.3f%n", 
            (imageDataCreated-start)/nanosPerMilli,
            (imageCreated-imageDataCreated)/nanosPerMilli,
            (imageDrawn-imageCreated)/nanosPerMilli,
            (imageViewCreated-imageDrawn)/nanosPerMilli,
            (stageShowCalled-imageViewCreated)/nanosPerMilli);
    }

    public static void main(String[] args) {
        launch();
    }

}

我系统上的(粗略的)分析给出了

Data creation time: 2414.017
Image Creation Time: 157.013
Image Drawing Time: 122.539
ImageView Creation Time: 15.626
Stage Show Time: 132.433

另一种方法是使用 PixelBuffer。看来 PixelBuffer 不支持索引颜色,所以这里别无选择,只能将字节数组数据转换为表示 ARGB 值的数组数据。这里我使用 ByteBuffer,其中 rgb 值以字节重复,alpha 始终设置为 0xff:

import java.nio.ByteBuffer;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelBuffer;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;


public class App extends Application {

    @Override
    public void start(Stage stage) {
       int width = 12000 ;
       int height = 8184 ;
       byte[] data = new byte[width*height];

       
       PixelFormat<ByteBuffer> format = PixelFormat.getByteBgraPreInstance();

       
       long start = System.nanoTime();
       
       for (int y = 0 ; y < height ; y++) {
           for (int x = 0 ; x < width; x++) {
               long dist2 = (1L * x - width/2) * (x- width/2) + (y - height/2) * (y-height/2);
               double dist = Math.sqrt(dist2);
               double val = (1 + Math.cos(Math.PI * dist / 1000)) / 2;
               data[x + y * width] = (byte)(val * 255);
           }
       }
       
       long imageDataCreated = System.nanoTime();
       
       byte alpha = (byte) 0xff ;
       
       byte[] convertedData = new byte[4*data.length];
       for (int i = 0 ; i < data.length ; i++) {
           convertedData[4*i] = convertedData[4*i+1] = convertedData[4*i+2] = data[i] ;
           convertedData[4*i+3] = alpha ;
       }
       long imageDataConverted = System.nanoTime() ;
       ByteBuffer buffer = ByteBuffer.wrap(convertedData);
       
       WritableImage img = new WritableImage(new PixelBuffer<ByteBuffer>(width, height, buffer, format));
       
       long imageCreated = System.nanoTime();
       
       
       ImageView imageView = new ImageView();
       imageView.setPreserveRatio(true);
       imageView.setImage(img);
       
       long imageViewCreated = System.nanoTime();
       
       BorderPane root = new BorderPane(imageView);
       imageView.fitWidthProperty().bind(root.widthProperty());
       imageView.fitHeightProperty().bind(root.heightProperty());
       Scene scene = new Scene(root, 800, 800);
       stage.setScene(scene);
       stage.show();
       
       long stageShowCalled = System.nanoTime();
       
       double nanosPerMilli = 1_000_000.0 ;
       
       System.out.printf(
               "Data creation time: %.3f%n"
            + "Data Conversion Time: %.3f%n"
            + "Image Creation Time: %.3f%n"
            + "ImageView Creation Time: %.3f%n"
            + "Stage Show Time: %.3f%n", 
            (imageDataCreated-start)/nanosPerMilli,
            (imageDataConverted-imageDataCreated)/nanosPerMilli,
            (imageCreated-imageDataConverted)/nanosPerMilli,
            (imageViewCreated-imageCreated)/nanosPerMilli,
            (stageShowCalled-imageViewCreated)/nanosPerMilli);
    }

    public static void main(String[] args) {
        launch();
    }

}

时间安排非常相似:

Data creation time: 2870.022
Data Conversion Time: 273.861
Image Creation Time: 4.381
ImageView Creation Time: 15.043
Stage Show Time: 130.475

显然,这种方法,正如所写的那样,会消耗更多的内存。可能有一种方法可以创建 ByteBuffer 的自定义实现,它只需查看底层字节数组并生成正确的值,而无需冗余数据存储。根据您的确切用例,这可能更有效(例如,如果您可以重用转换后的数据)。