在 JavaFX 中显示原始字节数组图像(没有 SwingFXUtils)
Show raw byte array Image in JavaFX (without SwingFXUtils)
我目前正在处理一个项目,我必须处理非常大的图像 (>> 100mb)。
图像采用原始字节数组的格式(现在只有灰度图像,以后的彩色图像每个通道都有一个字节数组)。
我想在 JavaFX imageView 中显示图像。所以我必须在尽可能短的时间内将给定的“原始”图像数据转换为 JavaFX 图像。
我已经尝试了很多解决方案,我在此处和 Whosebug 以及其他来源找到了这些解决方案。
SwingFXUtils
最流行(也是最简单)的解决方案是从原始数据构造一个 BufferedImage 并使用 SwingFXUtils.toFXImage(...)
将其转换为 JavaFX 图像。
在大约 100mb (8184*12000) 的图像上,我测量了以下时间。
- BufferedImage 是在 ca. 中创建的。 20-40 毫秒。
- 通过
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_INDEXED
PixelFormat
。下面演示了这种方法。在我的系统上生成实际的字节数组数据需要大约 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
的自定义实现,它只需查看底层字节数组并生成正确的值,而无需冗余数据存储。根据您的确切用例,这可能更有效(例如,如果您可以重用转换后的数据)。
我目前正在处理一个项目,我必须处理非常大的图像 (>> 100mb)。 图像采用原始字节数组的格式(现在只有灰度图像,以后的彩色图像每个通道都有一个字节数组)。
我想在 JavaFX imageView 中显示图像。所以我必须在尽可能短的时间内将给定的“原始”图像数据转换为 JavaFX 图像。
我已经尝试了很多解决方案,我在此处和 Whosebug 以及其他来源找到了这些解决方案。
SwingFXUtils
最流行(也是最简单)的解决方案是从原始数据构造一个 BufferedImage 并使用 SwingFXUtils.toFXImage(...)
将其转换为 JavaFX 图像。
在大约 100mb (8184*12000) 的图像上,我测量了以下时间。
- BufferedImage 是在 ca. 中创建的。 20-40 毫秒。
- 通过
SwingFXUtils.toFXImage(...)
转换为 JavaFX Image 需要 700 多毫秒。这对我的需求来说太多了。
编码图像并将其作为 ByteArrayInputStream 读取
我在这里 (
这个解决方案要复杂得多(需要 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_INDEXED
PixelFormat
。下面演示了这种方法。在我的系统上生成实际的字节数组数据需要大约 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
的自定义实现,它只需查看底层字节数组并生成正确的值,而无需冗余数据存储。根据您的确切用例,这可能更有效(例如,如果您可以重用转换后的数据)。