How to set given channel of a cv::Mat to a given value efficiently without changing other channels?
Asked Answered
Q

4

16

How to set given channel of a cv::Mat to a given value efficiently without changing other channels? For example, I want to set its fourth channel (alpha channel) value to 120 (i.e. half transparent), something like:

cv::Mat mat; // with type CV_BGRA
...
mat.getChannel(3) = Scalar(120); // <- this is what I want to do

P.S.: My current solution is first split the mat into multiple channels and set the alpha channel, and then merge them back.

P.S.2: I know that I can do this quickly if I also want to change other channels as well by:

mat.setTo(Scalar(54, 154, 65, 120)); 

Update with generalized solution:

Both methods will work for setting all mat values at given channel to given value. And they will work for all matrices whether they are continuous or not.

Method-1 - more efficient

-> based on @Antonio's answer and further improved by @MichaelBurdinov

// set all mat values at given channel to given value
void setChannel(Mat &mat, unsigned int channel, unsigned char value)
{
    // make sure have enough channels
    if (mat.channels() < channel + 1)
        return;

    const int cols = mat.cols;
    const int step = mat.channels();
    const int rows = mat.rows;
    for (int y = 0; y < rows; y++) {
        // get pointer to the first byte to be changed in this row
        unsigned char *p_row = mat.ptr(y) + channel; 
        unsigned char *row_end = p_row + cols*step;
        for (; p_row != row_end; p_row += step)
            *p_row = value;
    }
}

Method-2 - more elegant

-> based on @MichaelBurdinov's answer

// set all mat values at given channel to given value
void setChannel(Mat &mat, unsigned int channel, unsigned char value)
{
    // make sure have enough channels
    if (mat.channels() < channel+1)
        return;

    // check mat is continuous or not
    if (mat.isContinuous())
        mat.reshape(1, mat.rows*mat.cols).col(channel).setTo(Scalar(value));
    else{
        for (int i = 0; i < mat.rows; i++)
            mat.row(i).reshape(1, mat.cols).col(channel).setTo(Scalar(value));
    }
}

P.S.: It's worthy noting that, according to the documentation, matrices created with Mat::create() are always continuous. But if you extract a part of the matrix using Mat::col(), Mat::diag(), and so on, or constructed a matrix header for externally allocated data, such matrices may no longer have this property.

Quart answered 7/5, 2014 at 6:42 Comment(2)
To see how setTo is implemented, build OpenCV in debug mode and enter the function's body (in core.cpp)Willena
Please kindly review my answer. Thank you!Puparium
F
13

If your image is continuous in memory you can use following trick:

mat.reshape(1,mat.rows*mat.cols).col(3).setTo(Scalar(120));

If it is not continuous:

for(int i=0; i<mat.rows; i++)
    mat.row(i).reshape(1,mat.cols).col(3).setTo(Scalar(120));

Edit (thanks to Antonio for the comment):

Note that this code may be the shortest and it is not allocating new memory but it is not efficient at all. It may be even slower than split/merge approach. OpenCV is really inefficient when it should perform operations on non-continuous matrices with 1 pixel in a row. If time performance is important you should use the solution proposed by @Antonio.

Just a minor improvement to his solution:

const int cols = img.cols;
const int step = img.channels();
const int rows = img.rows;
for (int y = 0; y < rows; y++) {
    unsigned char* p_row = img.ptr(y) + SELECTED_CHANNEL_NUMBER; //gets pointer to the first byte to be changed in this row, SELECTED_CHANNEL_NUMBER is 3 for alpha
    unsigned char* row_end = p_row + cols*step;
    for(; p_row != row_end; p_row += step)
         *p_row = value;
    }
}

This saves increment operation for x and one less value in register. On system with limited resources it may give ~5% speedup. Otherwise time performance will be the same.

Favin answered 7/5, 2014 at 13:10 Comment(5)
And if you want it back, what reshape you need?Willena
+1 for the more elegant approach, further note that Matrices created with Mat::create() are always continuous. But if you extract a part of the matrix using Mat::col(), Mat::diag() , and so on, or constructed a matrix header for externally allocated data, such matrices may no longer have this property. And we can rely on Mat::isContinuous() to check that.Quart
@thedarkside ofthemoon, reshape is not changing original header of matrix, but create new one and return it. As a result you don't need to do anything to get it back.Favin
+1 I really wonder though what will be the efficiency of this implementation, and more in general why people working in computer vision are so afraid of writing functions going through a matrix and changing bytes values.Mythomania
@Antonio, the code is very short and clean but I guess time performance will indeed be quite terrible. OpenCV is really inefficient when it should perform operation on matrices with 1 pixel in a row. I will update my answer to make it clearer. Thanks.Favin
M
8
Mat img;
[...]
const int cols = img.cols;
const int step = img.channels();
const int rows = img.rows;
for (int y = 0; y < rows; y++) {
    unsigned char* p_row = img.ptr(y) + SELECTED_CHANNEL_NUMBER; //gets pointer to the first byte to be changed in this row, SELECTED_CHANNEL_NUMBER is 3 for alpha
    for (int x = 0; x < cols; x++) {
         *p_row = value;
         p_row += step; //Goes to the next byte to be changed
    }
}

Note: This works both for continuous and uncontinuous matrices, according to the use of the term for opencv: http://docs.opencv.org/modules/core/doc/basic_structures.html#bool%20Mat::isContinuous%28%29%20const

Mythomania answered 7/5, 2014 at 8:55 Comment(5)
your img.ptr is lacking the <type> braces, and CV_8UC4 (rgba) was explicitly specified in the post above. and what is step ? (yes, my downvote)Frogmouth
type braces is not mandatory, this function stub generalizes on all image types. Thank you for noticing the missing * (and the extra const, by the way), but that's no reason to downvote. Step is the step in byte to jump from one alpha byte to the next. docs.opencv.org/modules/core/doc/basic_structures.html#mat-ptrMythomania
Not true, but indeed it was wrong as I could not do *p_row = value;. However, these are errors easily removed after compiling.Mythomania
@Frogmouth CV8_UC4 isn't it BGRA? OpenCV stores images as BGR in cv::MatWillena
@thedarkside ofthemoon usually, images come as bgr(CV_8UC3) in opencv, but herohuyongtao's question above is about how to set the 4th channel in a BGRA(CV_8UC4) imageFrogmouth
K
2

What about direct Mat::data access (I'm quite sure setTo() or oother opencv Mat api use similar solution):

template<int N>
void SetChannel(Mat &img, unsigned char newVal) {   
    for(int x=0;x<img.cols;x++) {
        for(int y=0;y<img.rows;y++) {
            *(img.data + (y * img.cols + x) * img.channels() + N) = newVal;
        }
    }
}


int main() {
    Mat img = Mat::zeros(1000, 1000, CV_8UC4);
    SetChannel<3>(img, 120);
    imwrite("out.jpg", img);

    return 0;
}
Klenk answered 7/5, 2014 at 7:32 Comment(5)
Thanks. Actually I'm expecting for more elegant way to do this. Doesn't OpenCV provide the API to get specific channel?Quart
As far as I it does not. Of course it is not so elegant, but might be faster than splitting and merging the channels separately.Klenk
This does not take into account that stride might be different from cols: img.data + (y * img.cols + x) should be img.data + (y * img.step1() + x)Mythomania
Thanks, but you are partially right - I don't take into account the stride, but using step1() does not solve this problem because step1() is just (number_of_bytes_in_row / number_of_bytes_per_pixel, in our 8_UC4 case step1() is 4000) so if the matrix is not continuous, using step1() does not help at all. And second, of you would like to use step1(), the line should look like: *(img.data + y * img.step1() + x * img.channels() + N) = newVal;Klenk
step1() indeed does not help for SparseMat, but in all other cases (row padding, cv::Mat selected from a bigger cv::Mat). Your code correction is right, thanks.Mythomania
P
1

Simple algorithm:

void SetChannel(Mat mat, uint channel, uchar value)
{
    const uint channels = mat.channels();
    if (channel > channels - 1)
        return;

    uchar * data = mat.data;
    uint N = mat.rows * mat.step / mat.elemSize1();

    for (uint i = channel; i < N; i += channels)
        data[i] = value;
}
Puparium answered 8/4, 2019 at 15:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.