英文:
Show raw byte array Image in JavaFX (without SwingFXUtils)
问题
我目前正在一个项目中工作,需要处理非常大的图像(大于100MB)。
这些图像的格式是原始字节数组(目前只有灰度图像,以后彩色图像将会为每个通道有一个字节数组)。
我想在JavaFX的imageView中显示这个图像。因此,我必须将给定的“原始”图像数据尽快地转换为JavaFX图像。
我尝试了很多解决方案,这些解决方案来自于这里、stackoverflow以及其他来源。
SwingFXUtils
最流行(也最简单)的解决方案是从原始数据构造一个BufferedImage,然后使用SwingFXUtils.toFXImage(...)
将其转换为JavaFX图像。
在一个大约100MB(8184*12000)的图像上,我测得以下时间。
- BufferedImage的创建大约需要20-40毫秒。
- 通过
SwingFXUtils.toFXImage(...)
将其转换为JavaFX图像需要超过700毫秒。这对我的需求来说太长了。
编码图像并将其读取为ByteArrayInputStream
我在这里找到的一个方法(https://stackoverflow.com/a/33605064/3237961)是使用OpenCV的功能将图像编码为类似*.bmp的格式,然后直接从ByteArray构造一个JavaFX图像。
这个解决方案更加复杂(需要OpenCV或其他编码库/算法),而且编码似乎会增加额外的计算步骤。
问题
因此,我正在寻找一种更有效的方法来做这个。大多数解决方案最终都会使用SwingFXUtils,或者通过迭代所有像素来转换它们来解决问题(这是可能最慢的解决方案)。
是否有一种方法,可以比SwingFXUtils.toFXImage(...)
实现更高效的函数,或者直接从字节数组构造JavaFX图像。
也许还有一种方法,可以直接在JavaFX中绘制BufferedImage。因为在我看来,JavaFX图像并没有带来任何优势,只会使事情变得复杂。
谢谢你的回复。
英文:
I'm currently working on a project, where I have to deal with realy big images (>> 100mb).
The images are in the format of a raw byte array (for now only grayscale images, later color image would have a byte array for each channel).
I want to show the image in a JavaFX imageView. So I have to convert the given "raw" image data to a JavaFX image in as less time as possible.
I've tried a lot of solutions, which I found here and stackoverflow and from other sources.
SwingFXUtils
The most popular (and easy) solution is construct a BufferedImage from the raw Data and convert it to a JavaFX Image using SwingFXUtils.toFXImage(...)
.
On an image with around 100mb (8184*12000) I measured the following times.
- The BufferedImage is created in ca. 20-40ms.
- The conversion to a JavaFX Image via
SwingFXUtils.toFXImage(...)
takes more than 700ms. Which is too much for my needs.
Encode image and read it as a ByteArrayInputStream
One approach I've found here (https://stackoverflow.com/a/33605064/3237961) is to use OpenCVs functionality to encode the image to a format like *.bmp and construct a JavaFX Image directly from the ByteArray.
This solution is way more complex (OpenCV or some other encoding library/algorithm is needed) and encoding seems to add additional computation steps.
Question
So I'm looking for a more efficient way of doing this. Mostly all solutions use the SwingFXUtils in the end, or solve the problem by iterating over all pixels to convert them (which is the slowest possible solution).
Is there a way, to either implement a more efficient function than SwingFXUtils.toFXImage(...)
or construct a JavaFX image directly from the byte array.
Maybe there is also a way, to draw a BufferedImage directly in JavaFX. Because IMHO the JavaFX Image doesn't bring any advantages and it only makes things complicated.
Thanks for your replys.
答案1
得分: 3
根据评论中 @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)。
然后,您应该能够将仅包含单通道字节灰度值的数组输入。
英文:
Following up the suggestion by @James_D in the comments and your requirement of a grayscale image:
Use javafx.scene.image.PixelBuffer
but specify the javafx.scene.image.PixelFormat
as BYTE_INDEXED.
Set up the palette for color i to be RGB (i,i,i) or RGBA (i,i,i,1.0) using PixelFormat.createByteIndexedInstance() or PixelFormat.createByteIndexedPremultipliedInstance().
Then you should be able to just feed in your one-channel array of byte grayscale values.
答案2
得分: 3
从评论中提取的信息显示,对于此问题有两个可行的选项,都使用了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) {
// ... (省略部分代码)
}
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) {
// ... (省略部分代码)
}
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
实现,只需查看底层字节数组并生成正确的值,而无需冗余的数据存储。根据您的确切用例,这可能更高效(例如,如果您可以重用转换后的数据)。
英文:
Distilling the information from the comments, there appear to be two viable options for this, both using a WritableImage
.
In one, you can use the PixelWriter
to set the pixels in the image, using the original byte data and a BYTE_INDEXED
PixelFormat
. The following demos this approach. Generating the actual byte array data here takes ~2.5 seconds on my system; creating the (big) WritableImage
takes about 0.15 seconds, drawing the data into the image about 0.12 seconds.
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();
}
}
The (crude) profiling on my system gives
<!-- language: none -->
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
The other approach is to use a PixelBuffer
. It appears PixelBuffer
does not support indexed colors, so here there is no option but to convert the byte array data to array data representing ARGB values. Here I use a ByteBuffer
where the rgb values are repeated as bytes, and the alpha is always set to 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();
}
}
The timings for this are pretty similar:
<!-- language: none -->
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
Obviously this approach, as written, consumes more memory. There may be a way to create a custom implementation of ByteBuffer
that simply looks into the underlying byte array and generates the correct values without the redundant data storage. Depending on your exact use case, this may be more efficient (if you can reuse the converted data, for example).
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论