如何在不更改其他通道的情况下有效地将 cv::Mat 的给定通道设置为给定值?

Posted

技术标签:

【中文标题】如何在不更改其他通道的情况下有效地将 cv::Mat 的给定通道设置为给定值?【英文标题】:How to set given channel of a cv::Mat to a given value efficiently without changing other channels? 【发布时间】:2014-05-07 06:42:51 【问题描述】:

如何在不更改其他通道的情况下有效地将cv::Mat 的给定通道设置为给定值?例如,我想将它的第四个通道(alpha 通道)值设置为120(即半透明),类似于:

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

P.S.:我目前的解决方案是先将mat拆分成多个通道并设置alpha通道,然后再合并回来。

P.S.2:我知道如果我还想更改其他频道,我可以通过以下方式快速做到这一点:

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

用广义解决方案更新:

这两种方法都可以将给定通道的所有垫子值设置为给定值。无论它们是否连续,它们都适用于所有矩阵。

方法一——更高效

-> 基于@Antonio 的回答并由@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;
    

方法 2 - 更优雅

-> 基于@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;

    // 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.:值得注意的是,根据documentation,使用Mat::create() 创建的矩阵总是连续的。但是如果你使用Mat::col()Mat::diag()等提取矩阵的一部分,或者为外部分配的数据构造一个矩阵头,这样的矩阵可能不再具有这个属性。

【问题讨论】:

要查看setTo是如何实现的,在调试模式下构建OpenCV并进入函数体(在core.cpp中) 请查看我的回答。谢谢! 【参考方案1】:

如果您的图像在内存中是连续的,您可以使用以下技巧:

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

如果不是连续的:

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

编辑(感谢 Antonio 的评论):

请注意,这段代码可能是最短的,它没有分配新的内存,但它根本没有效率。它甚至可能比拆分/合并方法更慢。当 OpenCV 应该对连续 1 个像素的非连续矩阵执行操作时,它的效率确实很低。如果时间性能很重要,您应该使用@Antonio 提出的解决方案。

只是对他的解决方案的一个小改进:

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;
    

这节省了 x 的递增操作和寄存器中的少一个值。在资源有限的系统上,它可能会提供约 5% 的加速。否则时间性能将相同。

【讨论】:

如果你想要它回来,你需要什么重塑? +1 表示更优雅的方法,进一步注意使用Mat::create() 创建的矩阵始终是连续的。但是如果你使用Mat::col()Mat::diag()等提取矩阵的一部分,或者为外部分配的数据构造一个矩阵头,这样的矩阵可能不再具有这个属性。我们可以依靠Mat::isContinuous() 来检查。 @thedarkside ofthemoon,reshape 不会改变矩阵的原始标题,而是创建一个新的并返回它。因此,您无需执行任何操作即可将其取回。 +1 我真的很想知道这种实现的效率如何,更一般地说,为什么从事计算机视觉工作的人如此害怕编写通过矩阵和更改字节值的函数。跨度> @Antonio,代码非常简洁,但我想时间性能确实会很糟糕。当 OpenCV 应该对连续 1 个像素的矩阵执行操作时,它的效率确实很低。我将更新我的答案以使其更清楚。谢谢。【参考方案2】:
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
    

注意:这适用于连续和非连续矩阵,根据 opencv 术语的使用:http://docs.opencv.org/modules/core/doc/basic_structures.html#bool%20Mat::isContinuous%28%29%20const

【讨论】:

您的 img.ptr 缺少 大括号,并且在上面的帖子中明确指定了 CV_8UC4 (rgba)。什么是步骤? (是的,我的反对票) 类型大括号不是强制性的,这个函数存根概括了所有图像类型。感谢您注意到缺少的 *(以及额外的 const,顺便说一句),但这不是拒绝投票的理由。 Step 是从一个 alpha 字节跳到下一个字节的步长。 docs.opencv.org/modules/core/doc/basic_structures.html#mat-ptr 不正确,但确实是错误的,因为我做不到*p_row = value;。但是,这些错误在编译后很容易消除。 @berak CV8_UC4 不是 BGRA 吗? OpenCV 将图像作为 BGR 存储在 cv::Mat @thedarkside ofthemoon 通常,图像在opencv中以bgr(CV_8UC3)的形式出现,但herohuyongtao上面的问题是关于如何在BGRA(CV_8UC4)图像中设置第4通道【参考方案3】:

直接 Mat::data 访问怎么样(我很确定 setTo() 或其他 opencv Mat api 使用类似的解决方案):

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;

【讨论】:

谢谢。其实我期待更优雅的方式来做到这一点。 OpenCV不提供获取特定频道的API吗? 就我而言没有。当然它不是那么优雅,但可能比单独拆分和合并通道更快。 这里没有考虑stride可能与cols不同:img.data + (y * img.cols + x)应该是img.data + (y * img.step1() + x) 谢谢,但你部分正确 - 我没有考虑步幅,但使用 step1() 并不能解决这个问题,因为 step1() 只是 (number_of_bytes_in_row / number_of_bytes_per_pixel,在我们的 8_UC4 case step1() 是 4000) 所以如果矩阵不连续,使用 step1() 根本没有帮助。其次,你们当中想使用 step1(),该行应如下所示: *(img.data + y * img.step1() + x * img.channels() + N) = newVal; step1() 确实对 SparseMat 没有帮助,但在所有其他情况下(行填充,从更大的 cv::Mat 中选择的 cv::Mat)。您的代码更正是正确的,谢谢。【参考方案4】:

简单算法:

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;

【讨论】:

以上是关于如何在不更改其他通道的情况下有效地将 cv::Mat 的给定通道设置为给定值?的主要内容,如果未能解决你的问题,请参考以下文章

如何在不轮询的情况下监视页面的更改?

如何在不丢失 Google 索引的情况下正确地将网站从 http 重定向到 https

如何在不包含新列名和类型的情况下更改现有 Hive 表中的列注释?

如何在不产生死锁的情况下拥有一个缓冲通道和多个阅读器?

如何在不更改原始数组的情况下更改函数中的数组? [复制]

如何在不使用 JNDI 的情况下在 ConnectionFactory 中指定主机、端口和通道