How to initialize an image with image data (OpenCV Mat to juce::Image)

Hello folks. I am trying to initalize a Juce Image from a OpenCV Mat. But I am unable to initalize it. I get either pure grey or black window. Can someone help me? Here is my code:

// cvImageMat is cv::Mat
cvImageMat = cv::imread(std::string("C:\\Users\\Bora\\Pictures\\opencv\\brick.png"), cv::IMREAD_COLOR);

// image is juce::Image
image = Image(Image::PixelFormat::RGB, cvImageMat.size().width, cvImageMat.size().height, true);
Image::BitmapData data (image, 0, 0, image.getWidth(), image.getHeight(), Image::BitmapData::readWrite);
data.data = cvImageMat.data;

This code belongs to my constructor. I am painting this image in my paint function:

void paint(Graphics& g) override
{
     g.drawImage(image, 0, 0, getWidth(), getHeight(), 0, 0, image.getWidth(), image.getHeight());
}

I have omitted declaration of variables in header file for brewity here.

I am getting black screen with this setup. Can someone shed light on me? Should I get pixel data from Mat manuelly with for loops?

Simply assigning the data pointers will not work.
If you have the same pixel packing in both images, you can simply copy the memory block:

// some asserts to be safe:
jassert (cvImageMat.cols == image.getWidth());    
jassert (cvImageMat.rows == image.getHeight());
memcpy (image.data, cvImageMat.data, image.getWidth() * image.getHeight() * 4);

If the format is different, you have to convert pixel by pixel…

1 Like

Thank you. However, what do you mean by format? Channel count or the actual file format (png, jpg etc)?

No, the file format doen’t matter here any longer, because the cv::imageread converts it already to an array (they call it Mat / matrix to be mathematically more general I guess). But the raw pixels are aligned in a e certain way, which can be different for various purposes.

If you are lucky the format is per pixel 8 bit alpha, 8 bit red, 8 bit green, 8 bit blue and then the next pixel and so on…

If not, you have to see if you can find something in the OpenCV documentation, how to access the colour values for each pixel and iterate through the pixel data (I am making the openCV up, because I don’t know how you access the components)

for (int y=0; y<data.height; ++y)
    for (int x=0; x<data.width; ++x)
        data.setPixelColour (x, y, Colour::fromRGB (cvImageMat.at(x, y)[0], cvImageMat.at(x, y)[1], cvImageMat.at(x, y)[2]));

It can also be that the pixels are retrieved in a different colourspace, it can be that the colourplanes (red, green, blue alpha) are stored separately. All these combinations find their use somewhere.
But I think there is a method called cv::cvtColor() that should save you from doing it by hand…

But I haven’t used openCV, so that’s grey theory…

Good luck :slight_smile:

2 Likes

I just thank god for sending you to me :slight_smile: I have solved, I can show a cv::Mat as a juce::ımage now.

FYI, if you’re using @daniel’s code verbatim, it will almost definitely be a crazy expensive operation to convert between the datatypes (though it will be guaranteed to work).

It would be worth looking into seeing if there’s a way to assign the low-level image formats in the cv::Mat and juce::Image to be the same (i.e. same pixel depth, data layout) - that way you could just do a direct data copy/move between containers without all the extra overhead.

Indeed, that’s why I said try to use the same format and use memcpy, see my first post.
If all else fails, iterating pixelwise is an option, though very expensive, just as you say…

But then, if the pixels are stored differently, you don’t have much choice, do you?
juce has only 4 byte formats, and only in one order, so if OpenCV doesn’t use the same alignment…

What I would do is to use OpenCV to bring the image into the same alignment suitable for juce using the cvtColor method I linked above and memcpy it over…

But luckily the OP has already solved his problem so enough said… :wink:

Right, didn’t mean to knock your suggestion (I somehow didn’t make the connection to your prior memcpy suggestion when I wrote my post), I just want to make sure @borasemiz knows what he’s getting himself into by using pixel iteration…

Glad the problem has reached a conclusion!

1 Like

No problem. Sorry, seems I was a bit oversensitive…

I found this old thread, because I’m making something that can load HEIC images from iphones.

I decode the images using libheif, but to display them need to put them in a JUCE Image container.
The HEIC is RGB, the JUCE Image is ARGB so I can’t just copy the memory as discussed. At first I tried this:

    image = Image(Image::PixelFormat::RGB, width, height, false);

    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {

            auto r = raw_data[x * 3 + stride * y];
            auto g = raw_data[x * 3 + stride * y + 1];
            auto b = raw_data[x * 3 + stride * y + 2];

            auto colour = Colour::fromRGB(r,g,b);
            image.setPixelAt(x, y, colour);
        }
    }

which took about 3 seconds.

I then tired this:

    image = Image(Image::PixelFormat::RGB, width, height, false);
    Image::BitmapData dest_data (image, 0, 0, image.getWidth(), image.getHeight(), Image::BitmapData::readWrite);

    for (int y = 0; y < height; ++y) {
        for (int x = 0; x < width; ++x) {

            auto r = raw_data[x * 3 + stride * y];
            auto g = raw_data[x * 3 + stride * y + 1];
            auto b = raw_data[x * 3 + stride * y + 2];

            auto colour = PixelARGB((uint8)255,r,g,b);
            auto pixel = (PixelARGB*)dest_data.getPixelPointer(x,y);

            pixel->set (colour);
            
        }
    }

where stride is the number of bytes per row, which takes about 800ms. Hope someone finds it helpful.

I wonder if doing a memcopy on each pixel would be even faster?

Hey there

I was wondering have you tried doing a cv::mixchannels to merge your RGB into an ARGB cv matrix? That way doing a std::memcpy for the whole mat goes across nice and easily.

Cheers

Jeff

I’m currently working on a project where I need to capture a webcam (cross platform) using OpenCV and display it in a juce::Image. After not finding a satisfactory, quick-to-calculate solution, I had to delve deeper into the topic and would like to share my findings with you in case someone else needs a solution in the future.

As the previously stated, the OpenCV data can be copied via Image::BitmapData and std::memcpy is a relatively fast option to copy the data compared to the double for-loop.
However, the main problem arises when the OpenCV data has to be converted into the correct pixel format, as this will lead to errors, if the memory layout does not match.
The juce pixel formats RGB/ARGB suggest that the layout in memory looks exactly like that, but this is in fact only partially true, because the layout is platform-specific and differs.
For example: the juce::Image pixel memory layout is on a little endian macOS 64bit machine not ARGB but actually BGRA! Knowing that, the image can be correctly copied with following code:

juce::Image img (juce::Image::ARGB, 1920, 1080, true); // ::RGB or ::ARGB make no difference
...
// capture the next frame from the webcam
cv::Mat frame;
camera >> frame;

// get image bitmap
juce::Image::BitmapData imgData (img, 0, 0, img.getWidth(), img.getHeight(), juce::Image::BitmapData::readWrite);

cv::Mat bgra;
cv::cvtColor(frame, bgra, cv::COLOR_BGR2BGRA); // convert to pixel format
std::memcpy(imgData.data, bgra.data, bgra.total() * bgra.elemSize()); // copy to image

As @splintersilk suggested, the cv::mixchannelst is in fact an option, depending on how the conversion must be done. Following all conversions for the different platform/endianness. Please not that I have only tested the non-Android little endian case on a macOS 64bit.

juce::Image img (juce::Image::ARGB, 1920, 1080, true); // ::RGB or ::ARGB make no difference
...
// capture the next frame from the webcam
cv::Mat frame;
camera >> frame;

juce::Image::BitmapData imgData (img, 0, 0, img.getWidth(), img.getHeight(), juce::Image::BitmapData::readWrite);

// Note: depending on system the pixel format convertion might differ // a hint on the layouts can be found in juce_PixelFormats.h @292 ff
#if JUCE_ANDROID
 #if JUCE_BIG_ENDIAN
            // A = 0, R = 3, G = 2, B = 1; todo: needs verification!
            cv::Mat rgba;
            cv::cvtColor(frame, rgba, cv::COLOR_BGR2RGBA);
            cv::Mat out (rgba.size(), rgba.type());
            int from_to[] = { 0,3, 1,2, 2,1, 3,0 };
            cv::mixChannels(&rgba, 1, &out, 1, from_to, 4);
            std::memcpy(imgData.data, out.data, out.total() * out.elemSize());
 #else
            // A = 3, R = 0, G = 1, B = 2; todo: needs verification!
            cv::Mat rgba;
            cv::cvtColor(frame, rgba, cv::COLOR_BGR2RGBA);
            std::memcpy(imgData.data, rgba.data, rgba.total() * rgba.elemSize());
 #endif
#else
 #if JUCE_BIG_ENDIAN
            // A = 0, R = 1, G = 2, B = 3;  todo: needs verification!
            cv::Mat rgba;
            cv::cvtColor(frame, rgba, cv::COLOR_BGR2RGBA);
            cv::Mat out (rgba.size(), rgba.type());
            int from_to[] = { 0,1, 1,2, 2,3, 3,0 };
            cv::mixChannels(&rgba, 1, &out, 1, from_to, 4);
            std::memcpy(imgData.data, out.data, out.total() * out.elemSize());
 #else
            // A = 3, R = 2, G = 1, B = 0
            cv::Mat bgra;
            cv::cvtColor(frame, bgra, cv::COLOR_BGR2BGRA);
            std::memcpy(imgData.data, bgra.data, bgra.total() * bgra.elemSize());
 #endif
#endif

Hope the helps, cheers Julian.

1 Like

Hey Julian

This was great of you to share! Thanks :slight_smile: It’s been a while since I was looking at this but great to know of alternative and likely better ways of handling the issue than my suggestion.

Cheers

Jeff