OpenCVSharp学习笔记4--矩阵的掩码操作(从崩溃到收获)

本文是学习Mask operations on matrices和矩阵的掩码操作过程中的笔记,原本只是想着理解原文,并将源码用C#和OpenCVSharp重新实现,可没想到在实现的过程中,却遇到了几个让自己怀疑人生的问题。(部分代码有Bug,复制需谨慎!)

1、测试用例

这里引用矩阵的掩码操作测试用例中的原文(有版权问题的话,请联系删除)

思考一下图像对比度增强的问题。我们可以对图像的每个像素应用下面的公式

OpenCVSharp学习笔记4--矩阵的掩码操作(从崩溃到收获)_第1张图片

上面那种表达法是公式的形式,而下面那种是以掩码矩阵表示的紧凑形式。使用掩码矩阵的时候,我们先把矩阵中心的元素(上面的例子中是(0,0)位置的元素,也就是5)对齐到要计算的目标像素上,再把邻域像素值和相应的矩阵元素值的乘积加起来。虽然这两种形式是完全等价的,但在大矩阵情况下,下面的形式看起来会清楚得多。

现在,我们来看看实现掩码操作的两种方法。一种方法是用基本的像素访问方法,另一种方法是用 filter2D 函数。

2、用Mat.At方式实现

先上个官网的c++源码吧(留个纪念,如果您有心看完,就知道为什么了)

//! [basic_method]
void Sharpen(const Mat& myImage,Mat& Result)
{
  //! [8_bit]
    CV_Assert(myImage.depth() == CV_8U);  // accept only uchar images
  //! [8_bit]

  //! [create_channels]
    const int nChannels = myImage.channels();
    Result.create(myImage.size(),myImage.type());
  //! [create_channels]

  //! [basic_method_loop]
    for(int j = 1 ; j < myImage.rows-1; ++j)
    {
        const uchar* previous = myImage.ptr(j - 1);
        const uchar* current  = myImage.ptr(j    );
        const uchar* next     = myImage.ptr(j + 1);

        uchar* output = Result.ptr(j);

        for(int i= nChannels;i < nChannels*(myImage.cols-1); ++i)
        {
            *output++ = saturate_cast(5*current[i]
                         -current[i-nChannels] - current[i+nChannels] - previous[i] - next[i]);
        }
    }
  //! [basic_method_loop]

  //! [borders]
    Result.row(0).setTo(Scalar(0));
    Result.row(Result.rows-1).setTo(Scalar(0));
    Result.col(0).setTo(Scalar(0));
    Result.col(Result.cols-1).setTo(Scalar(0));
  //! [borders]
}
//! [basic_method]

按着原文与C++源码的理解,首先用Mat.At的方法重新实现了Sharpen函数(毕竟指针用不惯)

internal static void SharpenByAt(Mat myImage, Mat Result)
{
    Debug.Assert(myImage.Depth() == MatType.CV_8U);
    var nChannels = myImage.Channels();
    Result.Create(myImage.Size(), myImage.Type());
    if (nChannels == 1)
    {
        for (int j = 1; j < myImage.Rows - 1; ++j)
        {
            for (int c = 1; c < myImage.Cols - 1; ++c)
            {
                Result.At(j, c) = saturated(5 * myImage.At(j, c) - myImage.At(j - 1, c) - myImage.At(j + 1, c)
                                                - myImage.At(j, c - 1) - myImage.At(j, c + 1));
            }
        }
    }
    else
    {
        for (int j = 1; j < myImage.Rows - 1; ++j)
        {
            for (int c = 1; c < myImage.Cols - 1; ++c)
            {

                var cur = myImage.At(j, c);//中心点
                var curU = myImage.At(j - 1, c);//上
                var curD = myImage.At(j + 1, c);//下
                var curL = myImage.At(j, c - 1);//左
                var curR = myImage.At(j, c + 1);//右

                var b = new Vec3b(saturated(5 * cur[0] - curU[0] - curD[0] - curL[0] - curR[0]),
                                                   saturated(5 * cur[1] - curU[1] - curD[1] - curL[1] - curR[1]),
                                                   saturated(5 * cur[2] - curU[2] - curD[2] - curL[2] - curR[2]));
                Result.At(j, c) = b;
            }
        }
    }

    Result.Row(0).SetTo(new Scalar(0));
    Result.Row(Result.Rows - 1).SetTo(new Scalar(0));
    Result.Col(0).SetTo(new Scalar(0));
    Result.Col(Result.Cols - 1).SetTo(new Scalar(0));
}

//部分主函数中filter2D实现代码
 using var dst1 = new Mat();
 Mat kernel = new Mat(3, 3, new float[] { 0,-1,0
                                                 -1,5,-1,
                                                 0,-1,0});

 Cv2.Filter2D(src, dst1, src.Depth(), kernel);

运行后,用c#生成的图像(包含sharpen与filter2D方式生成的)与C++生成的图像一对比(强烈建议,如果你不是使用c++学习OpenCV的话,还是尝试编译、运行下C++版本的OpenCV,一定会有很多收获),不一样!!!好吧,不一样就不一样,那就用指针的方式也实现一遍。

3、用C#指针方式实现

虽然不习惯用指针,但笔记3有用指针的例子,还是难不倒我的。

internal unsafe static void Sharpen(Mat myImage, Mat Result)
{
    Debug.Assert(myImage.Depth() == MatType.CV_8U);
    var nChannels = myImage.Channels();
    Result.Create(myImage.Size(), myImage.Type());
    for (int j = 1; j < myImage.Rows - 1; ++j)
    {
        var previous = (byte*)(myImage.Ptr(j - 1).ToPointer());
        var current = (byte*)(myImage.Ptr(j).ToPointer());
        var next = (byte*)(myImage.Ptr(j + 1).ToPointer());

        var output = (byte*)(Result.Ptr(j).ToPointer());
        for (int i = nChannels; i < nChannels * (myImage.Cols - 1); ++i)
        {
            *output++ = saturated((5 * current[i] - current[i - nChannels] - current[i + nChannels] - previous[i] - next[i]));
        }
    }
    Result.Row(0).SetTo(new Scalar(0));
    Result.Row(Result.Rows - 1).SetTo(new Scalar(0));
    Result.Col(0).SetTo(new Scalar(0));
    Result.Col(Result.Cols - 1).SetTo(new Scalar(0));
}

对运行结果图像进行对比,还是不一样???问题在哪?上面算法与原文C++算法对比了好久好久,也没发现哪有问题呀(对指针的使用都有些恐惧了)!!!

4、怀疑图像对比算法

是不是自己写的图像对比算法有问题呢?

//自写的对比算法(部分)
var nChannels=matA.Channels();
var sMatA = matA.Split();
var sMatB = matB.Split();
var xorMat = new Mat[nChannels];
var isSame = true;
for (int i = 0;i < nChannels; i++)
{
    xorMat[i] = sMatA[i].Xor(sMatB[i]);
    isSame = Cv2.CountNonZero(xorMat[i]) == 0;
    if (!isSame) break;//有差异
}
return isSame;

在学习与搜索之后,发现原来OpenCV有现成的对比函数absdiff,欣喜一阵后,发现生成的图像对比结果还是不一样!!!(不知您有何感想,反正我是几近崩溃,严重怀疑自己的能力了)

5、C++、Python和C#版对比

C#版无论是根据sharpen算法还是filter2D算法生成的图像都与c++版本生成的图像不一致,只能尝试生成官网的Python版对应的图像了,结果!结果!不一样!!!结果又让我震惊!!!

用filter2D函数生成的图像对比结果:

a)、C++版与Python版一样,但与C#版不一样,

b)、filter2D生成的与Sharpen生成的结果也不一样

OpenCVSharp学习笔记4--矩阵的掩码操作(从崩溃到收获)_第2张图片

用sharpen函数生成的图像对比结果:只有C#Mat.At版与Python版一样,其它都各不相同

OpenCVSharp学习笔记4--矩阵的掩码操作(从崩溃到收获)_第3张图片

6、filter2D对比结果分析

对Sharpen方式的结果暂时没有头绪,那就先分析分析filter2D方式吧。

C++版

  Mat kernel = (Mat_(3,3) <<  0, -1,  0,
                                   -1,  5, -1,
                                    0, -1,  0);
  t = (double)getTickCount();
  filter2D( src, dst1, src.depth(), kernel );

Python版

kernel = np.array([[0, -1, 0],
                   [-1, 5, -1],
                   [0, -1, 0]], np.float32)  # kernel should be floating point type
    
dst1 = cv.filter2D(src, -1, kernel)

C#版

using var dst1 = new Mat();
Mat kernel = new Mat(3, 3, new int[] { 0,-1,0
                                             -1,5,-1,
                                             0,-1,0});

Cv2.Filter2D(src, dst1, src.Depth(), kernel);

官方的c++版与Python版的运行结果是一致,那说明这两个版本生成的图像应该没问题了,可为什么自己用C#写的结果会与另外两个版本不一致呢?

看到Python版的“# kernel should be floating point type”,怀疑是自己数据类型定义的问题,反复尝试定义为Mat、Mat,Mat类型,对比生成的结果图像,在这里Mat的数据类型的不同并没有影响生成的结果图像,这三个类型的结果图像是一样的!!!

depth()不一样?-1?结果还是一样的!!!

F12(转到定义),还有看看filter2D函数介绍吧

CV_EXPORTS_W void filter2D( InputArray src, OutputArray dst, int ddepth,
                            InputArray kernel, Point anchor = Point(-1,-1),
                            double delta = 0, 
                            int borderType = BORDER_DEFAULT );
public static void Filter2D(InputArray src, OutputArray dst, MatType ddepth, 
InputArray kernel, Point? anchor = null, double delta = 0.0, 
BorderTypes borderType = BorderTypes.Reflect101)

C++与c#版的borderType参数不一样(喜出望外)?编译,生成,失望!!!

borderType = BorderTypes.Reflect101
borderType = BORDER_DEFAULT

稍不那么兴奋的话也会细看函数说明,Reflect101与BORDER_DEFAULT是一样的

BORDER_REFLECT_101 = 4, //!< `gfedcb|abcdefgh|gfedcba`

BORDER_REFLECT101  = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101
BORDER_DEFAULT     = BORDER_REFLECT_101, //!< same as BORDER_REFLECT_101

尝试anchor、delta等等参数,生成结果图像就是不一样!!!

开始怀疑是不是OpenCV的版本不同导致生成的结果不同?如果是版本不同导致这么一个基础的OpenCV函数的生成结果不一样,那是多恐怖的事,还怎么升级版本?如果只是OpenCVSharp与c++和Python不一致,那OpenCVSharp还有谁敢用吗?

尝试使用相同的OpenCV的版本,但结果就是不一样!!!

即然“不可能,绝对不可能”是OpenCV的问题,那么还是继续苦逼分析分析代码吧...

开始对源图、结果图像的Mat对象像素级的输出、打印对比,输入的原图是一样的,可输出结果也确实是不一样的!!!恨不得对filter2D源码一行行的分析(能力有限,还是打住了)!

7、山重水复疑无路,柳暗花明又一村

经过反复的对比与尝试(其中的痛与乐着,只有你有类似经历才能体会到),最终还是发现了问题所在:

Mat kernel = new Mat(3, 3, new float[] { 0,-1,0
                                                -1,5,-1,
                                                 0,-1,0});
Mat kernel = new Mat(3, 3, new float[] { 0,-1,0,
                                                -1,5,-1,
                                                 0,-1,0});
//![kern]
Mat kernel = (Mat_(3, 3) << 0, -1, 0,
                                  -1, 5, -1,
                                   0, -1, 0);
//![kern]

不知眼尖的你有没有找到bug,反正,我一开始是没有怀疑过,简简单单的核函数定义会有bug!!!

如果你很快就发现了,给你点赞!暂时没发现的话,我就先卖个关子吧!后面再补充。

8、sharpen对比结果分析

虽然犯了一个低级的错误,但最终还是找到了问题所在,有了上面bug的发现与修复,至少把自己从崩溃的边缘拉回来了!

为什么sharpen的对比结果会是Python版与C#版的Mat.At版一致,而与C++版、C#指针版不一致?甚至是C++版与C#的指针版都不一样呢?

C#Mat.At版与Python版是一致,那是不是说明这两张图像应该是正确的,而C++版和C#指针版是有问题的?

将C#Mat.At版与C#指针版的前10x10像素(三块分别对应B,G,R通道的值)输出进行了对比

OpenCVSharp学习笔记4--矩阵的掩码操作(从崩溃到收获)_第4张图片

(在发现问题之前,我已经做了N多次尝试对比、分析,...此处可以省略一万字,还是直接说重点吧,如果你能看到这里,我觉得我写这篇软文,值了!谢谢您的厚爱,给您)

粗一看,上面的两个结果除了0(第一行置为0的)相同,其它都不同,可细细对比,你会发现左边比右边就多出了第二列,后面的数据都是一样的(右边最后一列是多出来的)。

那么结论是:指针操作时,像素位置(地址)有误?!只是C#版指针有误吗?不!与官网的C++版对比,C#版与C++版的功能应该是相同的,官方的源码有BUG?!OpenCV(其实只是Sample)有bug?!我也不敢相信自己会这样怀疑?既然怀疑,那还是分析下官方的源码吧。

OpenCVSharp学习笔记4--矩阵的掩码操作(从崩溃到收获)_第5张图片

9、OpenCV官网的Pull Request

一翻研究后,还真找到了Bug所在,源码只是取了行的首地址,然后将计算的结果赋给了行首地址(第1列,位置为0的列),正确的操作,应该是赋给第2列(位置为1的列,图像四周最后置为0),欣喜若狂,发现了一个OpenCV官方源码的Bug,此时此刻的心情和哥伦布发现新大陆一样开心、抓狂!赶紧去GitHub一顿神操作后提交了一个Pull Request(本人对Git和GitHub也是门都没入,见笑了)。

//fix by me 
output += nChannels;
for (int i = nChannels; i < nChannels * (myImage.cols - 1); ++i)
{
    /*bug
    *output++ = saturate_cast(5 * current[i] - current[i - nChannels] - current[i + nChannels] - previous[i] - next[i]);
    */
    //fix by Alexander Alekhin  https://github.com/alalek
    output[i] = saturate_cast(5 * current[i]
        - current[i - nChannels] - current[i + nChannels] - previous[i] - next[i]);
}

10、总结

眼尖的你,一定发现了关于filter2D函数的Bug是本人一个超低级的错误导致的,在第三个0位置后少了一个“,”,让我抓狂、崩溃的"逗号"!!!

OpenCVSharp学习笔记4--矩阵的掩码操作(从崩溃到收获)_第6张图片

导致核的值变成了

{0,-1,-1,
 5,-1,0,
-1,0 ,0}

各方式输出图像不一样的原因总结:

对比内容

原因分析

C#版filter2D与C++版、Python版输出图像不一致

C#的核值有误

C#指针版、C++版与Mat.At版、Python版输出图像不一致

指针地址有误

C#指针版Sharpen与C++版输出图像不一致

用Create创建的Mat的初始值不一样

filter2D函数与sharpen函数图像不一致

对边的处理方式不一样

一个Bug会让你和系统崩溃,修复一个Bug也能让你收获颇丰!

最后补上修复后的部分源码

public override void Run()
{
    //读取图像
    using var src = Cv2.ImRead("lena.jpg", ImreadModes.Color);

    if (src.Empty()) throw new Exception("图像打开有误!");

    using var dstAt = new Mat();
    SharpenByAt(src, dstAt);

    using var dst0 = new Mat();

    Sharpen(src, dst0);

    using var dst1 = new Mat();
    Mat kernel = new Mat(3, 3, new float[] { 0,-1,0,
                                               -1,5,-1,
                                                0,-1,0});

    Cv2.Filter2D(src, dst1, src.Depth(), kernel);
    //为了与Sharpen处理效果一致
    dst1.Row(0).SetTo(new Scalar(0));
    dst1.Row(dst1.Rows - 1).SetTo(new Scalar(0));
    dst1.Col(0).SetTo(new Scalar(0));
    dst1.Col(dst1.Cols - 1).SetTo(new Scalar(0));

    Cv2.ImShow("Sharpen", dst0);

    Cv2.ImShow("Filter2D", dst1);

    Cv2.WaitKey();
    Cv2.DestroyAllWindows();
}

private unsafe void Sharpen(Mat myImage, Mat Result)
{
    Debug.Assert(myImage.Depth() == MatType.CV_8U);
    var nChannels = myImage.Channels();
    Result.Create(myImage.Size(), myImage.Type());            

    for (int j = 1; j < myImage.Rows - 1; ++j)
    {
        var previous = (byte*)(myImage.Ptr(j - 1).ToPointer());
        var current = (byte*)(myImage.Ptr(j).ToPointer());
        var next = (byte*)(myImage.Ptr(j + 1).ToPointer());

        var output = (byte*)(Result.Ptr(j).ToPointer());
        //Fix by me
        //output += nChannels;
        for (int i = nChannels; i < nChannels * (myImage.Cols - 1); ++i)
        {
            //bug
            //*output++ = saturated((5 * current[i] - current[i - nChannels] - current[i + nChannels] - previous[i] - next[i]));
            //fix by Alexander Alekhin  https://github.com/alalek
            output[i] = saturated((5 * current[i] - current[i - nChannels] - current[i + nChannels] - previous[i] - next[i]));
        }
    }
    Result.Row(0).SetTo(new Scalar(0));
    Result.Row(Result.Rows - 1).SetTo(new Scalar(0));
    Result.Col(0).SetTo(new Scalar(0));
    Result.Col(Result.Cols - 1).SetTo(new Scalar(0));
}

private void SharpenByAt(Mat myImage, Mat Result)
{
    Debug.Assert(myImage.Depth() == MatType.CV_8U);
    var nChannels = myImage.Channels();
    Result.Create(myImage.Size(), myImage.Type());
    if (nChannels == 1)
    {
        for (int j = 1; j < myImage.Rows - 1; ++j)
        {
            for (int c = 1; c < myImage.Cols - 1; ++c)
            {
                Result.At(j, c) = saturated(5 * myImage.At(j, c) - myImage.At(j - 1, c) - myImage.At(j + 1, c)
                                                - myImage.At(j, c - 1) - myImage.At(j, c + 1));
            }
        }
    }
    else
    {
        for (int j = 1; j < myImage.Rows - 1; ++j)
        {
            for (int c = 1; c < myImage.Cols - 1; ++c)
            {

                var cur = myImage.At(j, c);//中心点
                var curU = myImage.At(j - 1, c);//上
                var curD = myImage.At(j + 1, c);//下
                var curL = myImage.At(j, c - 1);//左
                var curR = myImage.At(j, c + 1);//右

                var b = new Vec3b(saturated(5 * cur[0] - curU[0] - curD[0] - curL[0] - curR[0]),
                                                   saturated(5 * cur[1] - curU[1] - curD[1] - curL[1] - curR[1]),
                                                   saturated(5 * cur[2] - curU[2] - curD[2] - curL[2] - curR[2]));
                Result.At(j, c) = b;
            }
        }
    }

    Result.Row(0).SetTo(new Scalar(0));
    Result.Row(Result.Rows - 1).SetTo(new Scalar(0));
    Result.Col(0).SetTo(new Scalar(0));
    Result.Col(Result.Cols - 1).SetTo(new Scalar(0));
}
private byte saturated(int b)
{
    if (b > 255) return 255;
    if (b < 0) return 0;
    return (byte)b;
}

最后,此文献给我未满月的小侄女,愿她健康、快乐地成长!

你可能感兴趣的:(OpenCVSharp学习,opencv,c#)