c++opencvimage-processing

OpenCV and re-ordering bytes from different channels


In OpenCV, when using functions such as cv::imread() to read JPEG files, you get back a cv::Mat where the data is stored as B, G, R, B, G, R, etc...

What I need is to keep the channels together instead of ... "interlacing" the values. So instead of B, G, R, ..., my cv::Mat object needs to be formatted as B, B, B, ..., G, G, G, ..., R, R, R, ... (Because of a library I'm calling which expects the image data in that order.)

Problem is, I don't even know what the industry term for this is called, so I don't know what to google. "Interlacing" and "de-interlacing" is the only thing I can think of, but I don't think those are the right terms.

Regardless of the industry term, there are 2 ways I can think of doing this:

  1. Calling cv::split() to split the 3 BGR channels into different cv::Mat objects, and then concatenating the results. Because I need to do this to many large images, I figured I'd see if OpenCV has a different solution that doesn't require 3 unnecessary copies into temporary mat objects.
  2. I could for() loop over the cv::Mat objects, and copy the bytes as need to "de-interlace" the channels. But again, if OpenCV already has a function to do this kind of work, I'd rather call existing OpenCV functions.

(To be clear, this is not a question about cv::cvtColor(src, dst, cv::BGR2RGB). I'm not looking to swap 2 channels, I'm looking to keep the channels together.)


Solution

  • Based on your description of desired result

    formatted as B, B, B, ..., G, G, G, ..., R, R, R, ...

    I assume that the output image should be a single-channel Mat, of the same witdh, but 3 times the height of the input image. Basically 3 grayscale frames concatenated on top of each other, first representing blue channel, second green, third red.

    Your initial idea of using cv::split() is a good one, what you missed is the way to implement it so that it "doesn't require 3 unnecessary copies into temporary mat objects". That is actually quite easy to accomplish, and I'll explain how, but first, let's go over some background.


    First of all, most OpenCV functions have you provide a destination array. Often this can be an empty generic cv::Mat (or perhaps a vector of them), and the function will allocate appropriately shaped and typed Mats it needs to store the output. However, if an already allocated Mat is provided, and it has the correct shape and datatype, it will be re-used (the output will be written to that Mat).

    Second, it's important to understand how cv::Mat works. It's basically a smart pointer, with some metadata and a pointer to an array of bytes. The metadata tells it how to interpret the array. This means that you can have multiple Mats pointing to the same data, or sub-sections of that data (a.k.a. "views").


    With that in mind, the strategy is clear.

    1. Allocate a single channel result image, of same width and 3 times the height of the BGR input.
    2. Create a sequence of colour plane views of result that we can pass to cv::split as an OutputArrayOfArrays. In this case it means a std::vector<cv::Mat> with 3 elements, each of the elements being a view of the particular colour plane of the result. We can obtain the necessary views using cv::Mat::rowrange and simple arithmetic.
    3. Call cv::split.

    Here is a trivial example demonstrating the above mentioned strategy:

    #include <opencv2/opencv.hpp>
    
    int main()
    {
        uint8_t data[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 };
        cv::Mat bgr(2, 2, CV_8UC3, data);
    
        cv::Mat result = cv::Mat1b(bgr.rows * 3, bgr.cols);
    
        std::vector<cv::Mat> views = {
            result.rowRange(0, bgr.rows)
            , result.rowRange(bgr.rows, bgr.rows * 2)
            , result.rowRange(bgr.rows * 2, bgr.rows * 3)
        };
        cv::split(bgr, views);
    
        std::cout << bgr << '\n';
    
        std::cout << result << '\n';
    
        return 0;
    }
    

    We create a 2x2 BGR image, with pixel values set to:

    Then we split it according to your requirements, so we should get:


    The above program outputs the following:

    [  0,   1,   2,   3,   4,   5;
       6,   7,   8,   9,  10,  11]
    [  0,   3;
       6,   9;
       1,   4;
       7,  10;
       2,   5;
       8,  11]