0

I have posted a question on which of two approaches is more efficient for converting an OpenCV mat object to a JavaFXML Image so it can be displayed later in the application.

Most of the comments suggested to use PixelBuffer to construct a WritableImage.

For this purpose I have chosen @James_D proposition which is:

Mat -> MatOfByte -> byte[] -> ByteBuffer -> PixelBuffer -> WritableImage

This what the code looks like:

import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.WritableRaster;
import java.nio.ByteBuffer;

import javafx.concurrent.Task;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.image.Image;
import javafx.scene.image.PixelBuffer;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import org.opencv.core.CvType;
import org.opencv.core.Mat;

public class MatToFXImage extends Task<Image> {
    private final Mat mat;

    public MatToFXImage(Mat mat) {
        if (mat == null)
            throw new IllegalArgumentException("Mat object can't be null");

        this.mat = mat;
    }

    private int determineImageType(int matType) {
        if (matType == CvType.CV_8UC1)
            return BufferedImage.TYPE_BYTE_GRAY;
        else if (matType == CvType.CV_8UC3)
            return BufferedImage.TYPE_3BYTE_BGR;
        else
            throw new IllegalArgumentException("Unsupported Mat type: " + matType);
    }

    @Override
    protected Image call() {
        // MatOfByte byteMat = new MatOfByte();
        // Imgcodecs.imencode(".bmp", mat, byteMat);
        // return new Image(new ByteArrayInputStream(byteMat.toArray()));

        int size = (int) (mat.total() * mat.channels());
        byte[] byteArray = new byte[size];

        mat.get(0, 0, byteArray);

        ByteBuffer buffer = ByteBuffer.wrap(byteArray);
        PixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteBgraPreInstance();
        PixelBuffer<ByteBuffer> pixelBuffer = new PixelBuffer<>(mat.width(), mat.height(), buffer, pixelFormat);

        return new WritableImage(pixelBuffer);

        // int type;

        // type = determineImageType(mat.type());

        // BufferedImage image = new BufferedImage(mat.width(), mat.height(), type);
        // WritableRaster raster = image.getRaster();
        // DataBufferByte dataBuffer = (DataBufferByte) raster.getDataBuffer();

        // // byte[] data = dataBuffer.getData();

        // mat.get(0, 0, dataBuffer.getData());
        // return SwingFXUtils.toFXImage(image, null);
    }
}

I would like to know why it is not working as supposed to do

16
  • 3
    Can you expand on it is not working as supposed to do ? What do you expect to get and what are you actually getting? Since your problem concerns JavaFX, you may need to post some images that illustrate what you are getting versus what you want to get. And if you are getting an error, then post the stack trace. Commented Sep 25 at 11:01
  • 3
    So it just hangs somewhere? Are you sure that's what's happening? If so, can you identify the line in call() at which it's hanging? Can you create a minimal reproducible example that uses the class you posted here that demonstrates the issue? Commented Sep 25 at 12:01
  • 2
    I suspect what's really happening is that the call() method is throwing an exception, and you're not seeing it because it's thrown asynchronously (i.e. in the thread that's running the task) and being handled internally (and silently) by the usual Task machinery. Have you tried something like Task<Image> task = new MatToFxImage(); and then task.exceptionProperty().subscribe(Throwable::printStackTrace); (or otherwise explicitly handling an exception to get its stack trace) before submitting the task to an executor? Commented Sep 25 at 12:05
  • 2
    Create and post a minimal reproducible example. Commented Sep 25 at 14:47
  • 1
    If you can arrange for the Mat to be in a format that supports an alpha channel, that would be ideal. Otherwise you may need to code some manual conversion. There are some nice examples of various kinds of integration of JavaFX with other graphics libraries using PixelBuffers here. Those are probably worth your while to study. Commented Sep 25 at 19:01

1 Answer 1

4

Problem

You mention your test Mats have 3 channels. The JavaFX PixelBuffer class expects one of the two following pixel formats:

Both those formats have 4 channels. The PixelBuffer thinks your buffer is too small because it only has 3 channels. Presumably your data is missing the alpha channel.

PixelBuffer

The primary benefit of PixelBuffer is the ability to use direct NIO Buffers2. Direct buffers hold their data in native memory (i.e., off the Java heap). You can put such buffers in a PixelBuffer without copying the data from native memory into Java memory. A WritableImage can then be used to display the image from that data. This is the most efficient solution if your image data is in native memory and should stay there3. But it does require you to be able to get a direct buffer to the native memory to be fully effective.

If you are not using Java 22+ then there's no public Java API to get a direct ByteBuffer for a specific address. You'd either have to already have the direct ByteBuffer, make use of internal JDK APIs (if there are any), or use NewDirectByteBuffer from the Java Native Interface API (which requires writing your own native code). There may be a third-party library that can help with this (e.g., maybe JNA, not sure).

If you are using Java 22+ and you know the memory address of the data then you can get a direct ByteBuffer to the data via a MemorySegment4:

// import java.lang.foreign.MemorySegment;
// import javafx.scene.image.Image;
// import javafx.scene.image.PixelBuffer;
// import javafx.scene.image.PixelFormat;
// import javafx.scene.image.WritableImage;

public Image fromAddress(long address, int width, int height) {
  int length = width * height * 4; // may want to guard against overflow
  var segment = MemorySegment.ofAddress(address).reinterpret(length);

  var buffer = segment.asByteBuffer();
  var format = PixelFormat.getByteBgraPreInstance();
  var pixels = new PixelBuffer<>(width, height, buffer, format);

  return new WritableImage(pixels);
}

Unfortunately, if you can't get a direct ByteBuffer to the data then you'll have to copy the data into a Java byte[] somehow. You can then wrap the byte[] in a ByteBuffer and still use PixelBuffer. This will at least limit the number of copies to one. And this is what you seem to be currently trying to do, it's just you have the wrong number of channels.

PixelWriter

However, PixelBuffer rather limits the pixel formats you can use. If you want to use any of the pixel formats provided by the JavaFX API, then you can use the PixelWriter of a WritableImage.

// import java.nio.Buffer;
// import javafx.scene.image.Image;
// import javafx.scene.image.PixelFormat;
// import javafx.scene.image.WritableImage;

public <T extends Buffer> Image fromBuffer(
    T buffer, int width, int height, PixelFormat<T> format) {
  var image = new WritableImage(width, height);

  // This assumes the pixel format has 4 channels when computing the
  // value for 'scanlineStride'. Use a different value, or even try
  // to dynamically compute the value, as appropriate.
  int scanlineStride =
      switch (buffer) {
        case ByteBuffer _ -> width * 4;
        case IntBuffer _ -> width;
        default -> throw new IllegalArgumentException("unsupported buffer type");
      };

  var writer = image.getPixelWriter();
  writer.setPixels(0, 0, width, height, format, buffer, scanlineStride);

  return image;
}

Be aware that the setPixels methods copy the data from the buffer. Therefore if you copied the data from native memory to Java memory already, then this approach will result in copying the data twice. But if you can get a direct ByteBuffer to the data then this approach will only copy the data once while also giving you more freedom regarding the pixel format.

General Solution

So, you have two general solutions within the JavaFX API:

  1. Modify your pixel data to have 4 channels, whether in Java or via OpenCV, then continue using PixelBuffer. Make sure the pixel format is premultiplied BGRA (assuming byte buffer).

  2. Switch from PixelBuffer to PixelWriter with an appropriate 3-channel PixelFormat.

In all approaches mentioned in this answer, the pixel data must be the raw uncompressed pixel data. The format used to read the data is determined by the PixelFormat.


1. Note you can use ByteBuffer#asIntBuffer() to decorate a ByteBuffer with an IntBuffer. This would let you use the ARGB premultiplied format even with a direct ByteBuffer, assuming the underlying bytes are in that format. Make sure you configure the correct byte order.

2. While PixelBuffer uses NIO Buffers, it is not itself an NIO buffer.

3. As noted, a PixelBuffer that's using a ByteBuffer requires the pixel data bytes to be in BGRA format. That's a 4-channel format. If your data is currently in a 3-channel format, such as BGR, then note converting to BGRA will likely result in the image's memory footprint increasing by approximately 33%.

4. The MemorySegment interface is part of the Foreign Function & Memory API added to the standard library in Java 22.

Sign up to request clarification or add additional context in comments.

7 Comments

Thank you for the answer, however, I could not find any information about the static method fromAddress in the Java doc
I believe it is MemorySegment.ofAddress(...). (I will provide an edit to the answer.)
In fromAddress, I changed the first parameter to be a byte array and added the 2 lines at the beginning MemorySegment arraySegment = MemorySegment.ofArray(address); long arrayAddress = arraySegment.address(); However the arraySegment.address() allways return 0
@SedJ601 Was a mistake on my part; James_D fixed it. The java.lang.foreign API became a stable feature in Java 22.
@Starnec I don't know OpenCV that well, so I didn't give full examples. But if you have a byte[] then the MemorySegment approach is the wrong approach. You've already copied the data into Java memory and so MemorySegment won't provide any benefit. The first parameter is an address in my code snippet because that gives you direct access to the native memory, without copying. Now again, I don't know OpenCV that well, but perhaps what you're looking for is Mat#dataAddr().
And regarding the address of zero, see the documentation: "A heap segment obtained from one of the ofArray factory methods has an address of zero."

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.