4

This is a follow-up question to two previous posts on the most efficient way to convert an OpenCV Mat object into a JavaFX Image (post 1post 2).

@James_D, @Slaw, pointed to two possible approaches, in which @Slaw elaborated in his answer:

MemorySegment(of the Mat) -> ByteBuffer -> PixelBuffer -> return the image using WritableImage

and

Byte [] -> ByteBuffer -> read the buffer using WritableImage and return the image

For the first one, I am getting a fatal error. Meanwhile, with the second approach, I am getting an unusual result where the image becomes repetitive on the X-axis, displaying horizontal stripes.

About the app

This part of the application is responsible for maintaining only a specific color, based on the user's input of the maximum and minimum HSV values.

For this purpose, the class FilterByHSV has the filterImage(int minHue, int maxHue, int minSaturation, int maxSaturation, int minValue, int maxValue) method that returns a matrix, where all pixels within the specified HSV range, using sliders, are shown in color, and the rest are in gray.

Whenever the user changes one of the slider , filterImage is invoked, and the resulting Mat object is then converted into an Image that will be displayed in the application.

For converting the mat into an image, the class MatToFXImage extends Task<Image> implements the necessary logic in the call method.

The code

  1. ProcessImage is to create the UI and its elements.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.RowConstraints;
import javafx.scene.text.Font;
import javafx.stage.Stage;

import java.util.Objects;

public class ProcessImage extends Application {

    private GridPane createFilterWindow() {
        // Create main GridPane
        GridPane mainGridPane = new GridPane();
        mainGridPane.setPrefHeight(600.0);
        mainGridPane.setPrefWidth(800.0);
        mainGridPane.setStyle("-fx-background-color: #5b5b5b");
    
        // Create column constraints
        ColumnConstraints mainColumn = new ColumnConstraints();
        mainColumn.setHgrow(javafx.scene.layout.Priority.ALWAYS);
        mainGridPane.getColumnConstraints().add(mainColumn);
    
        // Create row constraints
        RowConstraints row1 = new RowConstraints();
        row1.setVgrow(javafx.scene.layout.Priority.NEVER);
        RowConstraints row2 = new RowConstraints();
        row2.setVgrow(javafx.scene.layout.Priority.ALWAYS);
        mainGridPane.getRowConstraints().addAll(row1, row2);
    
        // Create first HBox (controls section)
        HBox controlsHBox = new HBox();
        controlsHBox.setAlignment(javafx.geometry.Pos.CENTER);
    
        // Create inner GridPane for controls
        GridPane controlsGridPane = new GridPane();
        controlsGridPane.setAlignment(javafx.geometry.Pos.CENTER);
        controlsGridPane.setVgap(20);
        controlsGridPane.setStyle("-fx-background-color: #00ff93");
        HBox.setHgrow(controlsGridPane, javafx.scene.layout.Priority.ALWAYS);
    
        // Create column constraint for inner GridPane
        ColumnConstraints innerColumn = new ColumnConstraints();
        innerColumn.setHgrow(javafx.scene.layout.Priority.ALWAYS);
        controlsGridPane.getColumnConstraints().add(innerColumn);
    
        // Create slider controls
        createSliderControl(controlsGridPane, 0, "Min H", "minHueSlider", "minHueLabel");
        createSliderControl(controlsGridPane, 1, "Max H", "maxHueSlider", "maxHueLabel");
        createSliderControl(controlsGridPane, 2, "Min S", "minSaturationSlider", "minSaturationLabel");
        createSliderControl(controlsGridPane, 3, "Max S", "maxSaturationSlider", "maxSaturationLabel");
        createSliderControl(controlsGridPane, 4, "Min V", "minValueSlider", "minValueLabel");
        createSliderControl(controlsGridPane, 5, "Max V", "maxValueSlider", "maxValueLabel");
    
        controlsHBox.getChildren().add(controlsGridPane);
        mainGridPane.add(controlsHBox, 0, 0);
    
        // Create second HBox (image section)
        HBox imageHBox = new HBox();
        imageHBox.setAlignment(javafx.geometry.Pos.CENTER);
    
        // Create ImageView
        ImageView inputImage = new ImageView();
        inputImage.setId("inputImage");
        inputImage.setPickOnBounds(true);
        inputImage.setPreserveRatio(true);
        inputImage.setFitHeight(500);
        inputImage.setFitWidth(500);
    
        // Load image
        try {
            Image image = new Image(getClass().getResource("path_to_your_image").toString(), true);
            inputImage.setImage(image);
        } catch (Exception e) {
            System.err.println("Could not load image: " + e.getMessage());
        }
    
        imageHBox.getChildren().add(inputImage);
        mainGridPane.add(imageHBox, 0, 1);
    
        return mainGridPane;
    }
    
    private void createSliderControl(GridPane gridPane, int rowIndex, String labelText, String sliderId, String labelId) {
        HBox hbox = new HBox();
        hbox.setAlignment(javafx.geometry.Pos.CENTER);
        hbox.setPrefHeight(60);
    
        // Create label
        Label nameLabel = new Label(labelText);
        nameLabel.setTextFill(javafx.scene.paint.Color.BLACK);
        nameLabel.setFont(Font.font("Times New Roman Bold", 15.0));
        HBox.setMargin(nameLabel, new Insets(0, 20, 0, 20));
    
        // Create slider
        Slider slider = new Slider();
        slider.setId(sliderId);
        slider.setBlockIncrement(0.1);
        slider.setMajorTickUnit(0.5);
        slider.setMax(255.0);
        slider.getStyleClass().add("slider");
        HBox.setHgrow(slider, javafx.scene.layout.Priority.ALWAYS);
    
        // Create value label
        Label valueLabel = new Label("0");
        valueLabel.setId(labelId);
        valueLabel.setTextFill(javafx.scene.paint.Color.BLACK);
        valueLabel.setFont(Font.font("Times New Roman Bold", 15.0));
        HBox.setMargin(valueLabel, new Insets(0, 20, 0, 20));
    
        hbox.getChildren().addAll(nameLabel, slider, valueLabel);
        gridPane.add(hbox, 0, rowIndex);
    }
    
    @Override
    public void start(Stage stage) throws Exception {
        GridPane parentNode = createFilterWindow();
        Scene scene = new Scene(parentNode, 900, 700);
        stage.setTitle("Test window");
    
        ProcessImageController controller = new ProcessImageController();
        controller.mainGridPane = parentNode;
        controller.inputImage = (ImageView) parentNode.lookup("#inputImage");
        controller.setInputImagePath("C:\\Users\\PC\\Desktop\\bird-9445431.jpg");
        controller.minHueLabel = (Label) parentNode.lookup("#minHueLabel");
        controller.maxHueLabel = (Label) scene.lookup("#maxHueLabel");
        controller.minSaturationLabel = (Label) parentNode.lookup("#minSaturationLabel");
        controller.maxSaturationLabel = (Label) parentNode.lookup("#maxSaturationLabel");
        controller.minValueLabel = (Label) parentNode.lookup("#minValueLabel");
        controller.maxValueLabel = (Label) parentNode.lookup("#maxValueLabel");
        controller.minHueSlider = (Slider) scene.lookup("#minHueSlider");
        controller.maxHueSlider = (Slider) scene.lookup("#maxHueSlider");
        controller.minSaturationSlider = (Slider) parentNode.lookup("#minSaturationSlider");
        controller.maxSaturationSlider = (Slider) parentNode.lookup("#maxSaturationSlider");
        controller.minValueSlider = (Slider) parentNode.lookup("#minValueSlider");
        controller.maxValueSlider = (Slider) parentNode.lookup("#maxValueSlider");
        controller.initialize();
    
        try {
            stage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("resources/window-icon.png"))));
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    
        stage.setScene(scene);
        stage.show();
    
        stage.show();
    }
  1. ProcessImageController Is the controller of the view and its controls.
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import org.opencv.core.Mat;
import tasks.MatToFXImage;
import utils.FilterByHSV;
import utils.HSVAdaptor;

import java.util.Objects;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;

public class ProcessImageController {
    // UI Components
    @FXML
    public GridPane mainGridPane;
    @FXML
    public ImageView inputImage;

    // Labels
    @FXML
    public Label minHueLabel;
    @FXML
    public Label maxHueLabel;
    @FXML
    public Label minSaturationLabel;
    @FXML
    public Label maxSaturationLabel;
    @FXML
    public Label minValueLabel;
    @FXML
    public Label maxValueLabel;

    // Sliders
    @FXML
    public Slider minHueSlider;
    @FXML
    public Slider maxHueSlider;
    @FXML
    public Slider minSaturationSlider;
    @FXML
    public Slider maxSaturationSlider;
    @FXML
    public Slider minValueSlider;
    @FXML
    public Slider maxValueSlider;

    private FilterByHSV filterByHSV;

    // Single-thread executor ensures processing tasks don't overlap and preserves order.
    private final ExecutorService imageProcessingExecutor = Executors.newSingleThreadExecutor(r -> {
        Thread t = new Thread(r, "image-processing");
        t.setDaemon(true);
        return t;
    });

    // Scheduled executor used only for debounce timing.
    private final ScheduledExecutorService debounceExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
        Thread t = new Thread(r, "debounce-timer");
        t.setDaemon(true);
        return t;
    });

    // Holds the currently scheduled debounce task so it can be cancelled/rescheduled.
    private final AtomicReference<ScheduledFuture<?>> pendingDebounce = new AtomicReference<>();

    // Debounce interval in milliseconds.
    private static final long DEBOUNCE_MS = 150L;

    @FXML
    public void initialize() {
        setupSliderListeners();
    }

    private void setupSliderListeners() {
        addSliderPair(minHueSlider, minHueLabel, maxHueSlider, maxHueLabel);
        addSliderPair(minSaturationSlider, minSaturationLabel, maxSaturationSlider, maxSaturationLabel);
        addSliderPair(minValueSlider, minValueLabel, maxValueSlider, maxValueLabel);
    }

    private void addSliderPair(Slider minSlider, Label minLabel, Slider maxSlider, Label maxLabel) {
        // Min slider
        minSlider.valueProperty().addListener((_, _, newV) -> {
            double minVal = newV.doubleValue();
            // Clamp to maintain the gap
            double maxAllowed = maxSlider.getValue() - 10.0;
            if (minVal > maxAllowed) {
                minVal = maxAllowed;
                minSlider.setValue(minVal);
            }
            minLabel.setText(String.format("%.02f", minVal));
            scheduleDebouncedUpdate();
        });

        // Max slider
        maxSlider.valueProperty().addListener((_, _, newV) -> {
            double maxVal = newV.doubleValue();
            // Clamp to maintain the gap
            double minAllowed = minSlider.getValue() + 10.0;
            if (maxVal < minAllowed) {
                maxVal = minAllowed;
                maxSlider.setValue(maxVal);
            }
            maxLabel.setText(String.format("%.02f", maxVal));
            scheduleDebouncedUpdate();
        });
    }

    private void scheduleDebouncedUpdate() {
        ScheduledFuture<?> prev = pendingDebounce.getAndSet(debounceExecutor.schedule(() ->
                Platform.runLater(this::updateImageFilter), DEBOUNCE_MS, TimeUnit.MILLISECONDS));
        if (prev != null)
            prev.cancel(false);
    }

    private Image matToImage(Mat mat) {
        try {
            MatToFXImage task = new MatToFXImage(mat);
//            task.exceptionProperty().subscribe(Throwable::printStackTrace);
            new Thread(task).start();
            return task.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException("Failed to convert Mat to Image", e);
        }
    }

    private static String stripFilePrefix(String filePath) {
        if (filePath != null && filePath.startsWith("file:")) {
            return filePath.substring("file:".length());
        }
        return filePath;
    }

    private void updateImageFilter() {
        if (filterByHSV == null) return;

        // Read current slider values once
        int minHue = (int) HSVAdaptor.adaptHue(minHueSlider.getValue(), 255, 179);
        int maxHue = (int) HSVAdaptor.adaptHue(maxHueSlider.getValue(), 255, 179);

        int minSaturation = (int) minSaturationSlider.getValue();
        int maxSaturation = (int) maxSaturationSlider.getValue();

        int minValue = (int) minValueSlider.getValue();
        int maxValue = (int) maxValueSlider.getValue();

        imageProcessingExecutor.submit(() -> {
            try {
                Mat outputMat = filterByHSV.filterImage(minHue, maxHue, minSaturation, maxSaturation, minValue, maxValue);
                Image outputImage = matToImage(outputMat);
                Platform.runLater(() -> inputImage.setImage(outputImage));
            } catch (Exception e) {
                // Consider logging to a logger instead of throwing if UI stability is preferred
                throw new RuntimeException("Image processing failed", e);
            }
        });
    }

    @FXML
    public void setInputImagePath(String path) {
        Objects.requireNonNull(path, "path");
        Image image = new Image("file:" + path);
        inputImage.setImage(image);
        String filePath = stripFilePrefix(image.getUrl());
        filterByHSV = new FilterByHSV(filePath);
    }

    @FXML
    public void setSlidersValues(double minHue, double maxHue, double minSaturation, double maxSaturation, double minValue, double maxValue) {
        minHueSlider.setValue(minHue);
        maxHueSlider.setValue(maxHue);
        minSaturationSlider.setValue(minSaturation);
        maxSaturationSlider.setValue(maxSaturation);
        minValueSlider.setValue(minValue);
        maxValueSlider.setValue(maxValue);
        scheduleDebouncedUpdate();
    }

    public void dispose() {
        ScheduledFuture<?> pending = pendingDebounce.getAndSet(null);
        if (pending != null) pending.cancel(false);

        debounceExecutor.shutdown();
        imageProcessingExecutor.shutdown();

        try {
            if (!debounceExecutor.awaitTermination(500, TimeUnit.MILLISECONDS)) {
                debounceExecutor.shutdownNow();
            }
            if (!imageProcessingExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
                imageProcessingExecutor.shutdownNow();
            }
        } catch (InterruptedException e) {
            debounceExecutor.shutdownNow();
            imageProcessingExecutor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}
  1. MatToFXImage It is a Task where the mat will be converted into a JavaFX image.

    import java.awt.image.BufferedImage;
    import java.awt.image.DataBufferByte;
    import java.awt.image.WritableRaster;
    import java.io.ByteArrayInputStream;
    import java.lang.foreign.Arena;
    import java.lang.foreign.MemorySegment;
    import java.nio.Buffer;
    import java.nio.ByteBuffer;
    import java.util.Arrays;
    
    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;
    import org.opencv.core.MatOfByte;
    import org.opencv.imgcodecs.Imgcodecs;
    
    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);
        }
    
        public Image fromAddress(long address, int width, int height) {
            try {
                int length = width * height * 6; // 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);
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }
    
        public <T extends Buffer> Image fromBuffer(T buffer, int width, int height, PixelFormat<T> format) {
            var image = new WritableImage(width, height);
            var writer = image.getPixelWriter();
            writer.setPixels(0, 0, width, height, format, buffer, width);
    
            return image;
        }
    
        @Override
        protected Image call() {
    // Works fine
            MatOfByte byteMat = new MatOfByte();
            Imgcodecs.imencode(".bmp", mat, byteMat);
            return new Image(new ByteArrayInputStream(byteMat.toArray()));
    
    // Fatal Error
            // try {
            //     System.out.println(mat.dataAddr());
            //     return fromAddress(mat.dataAddr(), mat.width(), mat.height());
            // }catch (Exception e) {
            //         e.printStackTrace();
            //         return null;
            // }
    
    // Strange result 1
            // try {
            //     int size = (int) (mat.total() * mat.channels());
            //     byte[] byteArray = new byte[size * 5];
            //     mat.get(0, 0, byteArray);
    
            //     ByteBuffer buffer = ByteBuffer.wrap(byteArray);
            //     PixelFormat<ByteBuffer> pixelFormat = PixelFormat.getByteBgraPreInstance();
    
            //     return fromBuffer(buffer, mat.width(), mat.height(), pixelFormat);
            // } catch (Exception e) {
            //     e.printStackTrace();
            //     return null;
            // }
    
    
    // Strange result 2
            // try {
            //     int size = (int) (mat.total() * mat.channels());
            //     System.out.println(mat.channels());
            //     byte[] byteArray = new byte[size * 5];
    
            //     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);
            // } catch (Exception e) {
            //     e.printStackTrace();
            //     return null;
            // }
    
    
    // Works fine
            // 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);
        }
    }
    
    
    
  2. FilterByHSV keeps only the pixels within the specified range and turns the rest into grey.

    package utils;
    
    import org.opencv.core.Core;
    import org.opencv.core.Mat;
    import org.opencv.core.Scalar;
    import org.opencv.imgcodecs.Imgcodecs;
    import org.opencv.imgproc.Imgproc;
    
    public class FilterByHSV {
        private static final int COLOR_CONVERSION_BGR_TO_HSV = Imgproc.COLOR_BGR2HSV;
        private static final int COLOR_CONVERSION_BGR_TO_GRAY = Imgproc.COLOR_BGR2GRAY;
        private static final int COLOR_CONVERSION_GRAY_TO_BGR = Imgproc.COLOR_GRAY2BGR;
    
        static {
            System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        }
    
        Mat image;
        Mat imageInHSV;
        Mat grey;
        Mat mask;
        Mat result;
    
        public FilterByHSV(String imageToFilterPath) {
            imageInHSV = new Mat();
            grey = new Mat();
            mask = new Mat();
            result = new Mat();
    
            image = Imgcodecs.imread(imageToFilterPath);
            Imgproc.cvtColor(image, imageInHSV, COLOR_CONVERSION_BGR_TO_HSV);
            Imgproc.cvtColor(image, grey, COLOR_CONVERSION_BGR_TO_GRAY);
        }
    
        public Mat filterImage(int minHue, int maxHue, int minSaturation, int maxSaturation, int minValue, int maxValue) {
            Core.inRange(imageInHSV, new Scalar(minHue, minSaturation, minValue), new Scalar(maxHue, maxSaturation, maxValue), mask);
            Imgproc.cvtColor(grey, result, COLOR_CONVERSION_GRAY_TO_BGR);
            image.copyTo(result, mask);
    
            return result;
        }
    }
    
    
    
  3. HSVAdaptor convert the HSV values into their real values.

    public class HSVAdaptor {
        public static double adaptHue(double hueValue, double from, double to) {
            return (hueValue / from) * to;
        }
    
        public static double adaptSaturation(double saturation) {
            return saturation * 255;
        }
    
        public static double adaptValue(double value) {
            return value * 255;
        }
    }
    

Now this is what the result should look like (the working parts are commented as // work fine)

enter image description here

For the // fatal error par this the error code I get:

# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x00007ffa1a92059b, pid=49852, tid=13600
#
# JRE version: Java(TM) SE Runtime Environment (25.0+37) (build 25+37-LTS-3491)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (25+37-LTS-3491, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, windows-amd64)
# Problematic frame:
# C  [VCRUNTIME140.dll+0x1059b]
#
# No core dump will be written. Minidumps are not enabled by default on client versions of Windows
#
# An error report file with more information is saved as:
# C:\Users\PC\IdeaProjects\Color Isolator 2\hs_err_pid49852.log
[17.144s][warning][os] Loading hsdis library failed
#
# If you would like to submit a bug report, please visit:
#   https://bugreport.java.com/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.

For // strange result 1 this is what i get

enter image description here

For // strange result 2 this is what i get

enter image description here

In order to use MemorySegment I added the following configiration, so native code can be run

--enable-native-access=javafx.graphics
--enable-native-access=ALL-UNNAMED

I would like to know why both approaches are not working, and what is causing the fatal error and the unusual results when using the byte buffer

5
  • 2
    context: (1) stackoverflow.com/questions/79774713/… (2) stackoverflow.com/questions/79771565/… Commented Sep 27 at 19:00
  • 2
    For this line: int length = width * height * 6. May I ask why you're multiplying by 6 here? The reason my answer to your previous question multiplied by 4 is because the pixel format being used expects 4 channels (BGRA). Your code acts as if there are 6 channels. The number of bytes is imageWidth * imageHeight * numOfChannels. And again, the JavaFX API being used here expects the pixel format to have 4 channels in BGRA order. Multiplying by 6 instead of 4 may mean the MemorySegment is too large, letting Java go out-of-bounds leading to a segmentation fault. Commented Sep 27 at 20:49
  • @Slaw, it does not matter by which number I multiply (4 will also cause the fatal error as well) Commented Sep 27 at 20:52
  • 5
    Off topic, but why on earth would you go to all the trouble of creating a Task to convert the image, and run it in a background thread, only to block the current thread by calling task.get() as soon as you start the new thread. If you’re going to do that, you may as well just do the conversion in the current thread and eliminate a whole bunch of unnecessary complexity. Commented Sep 28 at 14:19
  • 2
    I absolutely understand your point, and, of course, it doesn't make sense to start a background thread that will then be stopped by the get() method later. However, this code was directly copied from an old, early version of it. Now, it overrides the setOnSucceeded() method to display the image after the task finishes using getValue(). Thank you :) Commented Sep 28 at 19:26

1 Answer 1

5

Fatal Error

The access violation means the process tried to access memory that doesn't belong to the process. On most modern operating systems that will cause the process to crash. The cause of the violation in this case is making the length of the MemorySegment too large via the reinterpret call (hence the method being restricted). Which is ultimately a number-of-channels issue. Your code currently assumes there are 6 channels, where it should be at most 4. But you mention even assuming 4 channels leads to the error, which suggests your Mat only has 3 channels. You need to make sure it has 4 channels (BGRA) to work with JavaFX's PixelBuffer.

See the proof-of-concept below for an example.

Strange Results

As for your strange results, I believe there are two issues (which may be compounding in one of the approaches).

  1. You have a number-of-channels issue again. In both cases you're still trying to use a pixel format of BGRA, a format with 4 channels. Which means the length of the byte[] should be:

    int length = imageWidth * imageHeight * 4;
    

    Your current approach is making the byte[] too large.

  2. A mistake I made in my previous answer (since fixed). I said the "scanline stride" was the image's width, but that value is relative to the buffer not the number of pixels. The actual value is typically the image's width multiplied by the number of channels when using a byte[] or ByteBuffer, and typically remains the image's width when using an int[] or IntBuffer.

Getting the pixel data out of the Mat into a byte[] should look like:

int size = Math.toIntExact(mat.total() * mat.channels());
byte[] data = new byte[size];
mat.get(0, 0, data);

Then you can create a JavaFX image one of the following ways:

// (1) Using PixelBuffer. Requires 4 channels (BGRA).
ByteBuffer buffer = ByteBuffer.wrap(data);
PixelFormat<ByteBuffer> format = PixelFormat.getByteBgraPreInstance();
PixelBuffer<ByteBuffer> pixels = new PixelBuffer<>(mat.width(), mat.height(), buffer, format);
Image image = new WritableImage(pixels);

// (2) Using PixelWriter (assuming BGRA)
WritableImage image = new WritableImage(mat.width(), mat.height());
PixelWriter writer = image.getPixelWriter();
PixelFormat<ByteBuffer> format = PixelFormat.getByteBgraInstance();
int scanlineStride = Math.toIntExact(mat.width() * mat.channels());
writer.setPixels(0, 0, mat.width(), mat.height(), format, data, 0, scanlineStride);

For the second approach, use an appropriate 3-channel pixel format when your data only has 3 channels (e.g., BGR).


Proof of Concept

Here's an proof-of-concept using the MemorySegment approach that seems to do what you want. I can't guarantee the color isolation implementation is fully correct or most efficient. But the example does use the MemorySegment approach for displaying images contained in a Mat.

Note you may need to keep a reference to the Mat being displayed for as long as it's being displayed. I don't know if OpenCV's Java bindings release the native memory when the Java object is garbage collected. Nor do I know what will happen on the JavaFX side after the underlying data is released after it's been displayed.

Versions

  • Java 25.0.0 (minimum 22 to use MemorySegment; minimum 23 to use JavaFX 25)
  • JavaFX 25.0.0
  • OpenCV 4.12.0

Configuration

  • Pass --allow-native-access=ALL-UNNAMED,javafx.graphics when running code.

  • OpenCV native library needs to be locatable by System::loadLibrary. One approach is to put the native library on the java.library.path system property.

Test Image

I got the test image from here. I saved it in a local file named bird.png located in the working directory.

PNG of test image of bird.

Source Code

Main.java

Requires the test image to be in a file named bird.png in the working directory.

package com.example;

import java.lang.foreign.MemorySegment;
import java.nio.ByteBuffer;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelBuffer;
import javafx.scene.image.PixelFormat;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Scalar;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

public class Main extends Application {

  private static final String IMAGE_PATH = "bird.png";

  private static final double MIN_H = 10.0;
  private static final double MAX_H = 35.0;
  private static final double MIN_S = 50.0;
  private static final double MAX_S = 255.0;
  private static final double MIN_V = 50.0;
  private static final double MAX_V = 255.0;

  private Mat original;
  private Mat filtered;

  @Override
  public void init() {
    System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
  }

  @Override
  public void start(Stage primaryStage) {
    original = readImageIntoMat();
    filtered = isolateColorRange(original);

    ImageView originalView = new ImageView(matToImage(original));
    ImageView filteredView = new ImageView(matToImage(filtered));

    HBox root = new HBox(originalView, filteredView);
    root.setAlignment(Pos.CENTER);
    root.setSpacing(10);
    root.setPadding(new Insets(10));

    primaryStage.setScene(new Scene(root));
    primaryStage.show();
  }

  private Mat readImageIntoMat() {
    Mat img = Imgcodecs.imread(IMAGE_PATH, Imgcodecs.IMREAD_UNCHANGED);
    if (img.empty()) throw new IllegalStateException("could not load image");

    if (img.channels() != 4) {
      Imgproc.cvtColor(img, img, Imgproc.COLOR_BGR2BGRA);
    }
    return img;
  }

  private Image matToImage(Mat mat) {
    if (mat.empty()) throw new IllegalArgumentException("empty");
    if (mat.channels() != 4) throw new IllegalArgumentException("channels != 4");
    if (!mat.isContinuous()) throw new IllegalArgumentException("not continuous");

    long dataAddress = mat.dataAddr();
    long dataLength = mat.total() * mat.channels();
    MemorySegment data = MemorySegment.ofAddress(dataAddress).reinterpret(dataLength);

    ByteBuffer buffer = data.asByteBuffer();
    PixelFormat<ByteBuffer> format = PixelFormat.getByteBgraPreInstance();
    PixelBuffer<ByteBuffer> pixels = new PixelBuffer<>(mat.width(), mat.height(), buffer, format);

    return new WritableImage(pixels);
  }

  private Mat isolateColorRange(Mat src) {
    if (src.empty()) throw new IllegalArgumentException("empty");
    if (src.channels() != 4) throw new IllegalArgumentException("channels != 4");

    Scalar min = new Scalar(MIN_H, MIN_S, MIN_V);
    Scalar max = new Scalar(MAX_H, MAX_S, MAX_V);

    Mat hsv = new Mat();
    Imgproc.cvtColor(src, hsv, Imgproc.COLOR_BGR2HSV);

    Mat mask = new Mat();
    Core.inRange(hsv, min, max, mask);
    hsv.release();

    Mat dst = new Mat();
    Imgproc.cvtColor(src, dst, Imgproc.COLOR_BGR2GRAY);
    Imgproc.cvtColor(dst, dst, Imgproc.COLOR_GRAY2BGRA);

    src.copyTo(dst, mask);
    mask.release();

    return dst;
  }
}

Output

Here's a screenshot of the example running. The left image is the original bird image and the right image is the color-isolated image. Both images are contained in a OpenCV Mat and displayed using a MemorySegment plus PixelBuffer.

Screenshot of example JavaFX application.

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

7 Comments

Thank you for your answer, but the code only works with images that have 4 channels. And I see the limitation since the PixelFormat accepts only two 4-channel options. in the if (mat.channels() != 4) I added the lineImgproc.cvtColor(mat, mat, Imgproc.COLOR_BGR2BGRA) , it works but now the supposed-to-be-grey pixels are in black
@Starnec Yes, PixelBuffer requires 4 channels. If your image doesn't have 4 channels then you'll have to make it have 4 channels if you want to use PixelBuffer. Can you provide an image where my example results in black instead of gray?
Yes, absolutely, this is what the result look like, and this is the used image. That is after adding this line lineImgproc.cvtColor(mat, mat, Imgproc.COLOR_BGR2BGRA)in the if (mat.channels() != 4)of the matToImage(Mat mat) method you provided.
See updated code (diff). It should now work with 3-channel source images (e.g., JPEGs). I put that Imgproc.COLOR_BGR2BGRA conversion in readImageIntoMat.
you are a lifesaver. It works now just perfectly. I will update the code later so it can follow your answer. I will also recheck if this is the reason why the strange behavior is occurring with the ByteBuffer approach as well. Finally, I will conduct a comprehensive comparison of all four approaches in terms of both RAM efficiency and execution speed. At first glance, it appears that using MemorySegment is significantly faster and more efficient, with approximately 40 MB of memory saved. Thank you for your time, really appreciate it.
@Starnec I updated my answer as I believe I narrowed down the problem in your "strange results" cases.
Thank you, @Slaw. Yes I just checked the doc: scanlineStride - the distance between the pixel data for the start of one row of data in the buffer to the start of the next row of data. Which means we have to multiply the width by the number of channels to make a hop and start at the new offset.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.