本文是学习Mask operations on matrices和矩阵的掩码操作过程中的笔记,原本只是想着理解原文,并将源码用C#和OpenCVSharp重新实现,可没想到在实现的过程中,却遇到了几个让自己怀疑人生的问题。(部分代码有Bug,复制需谨慎!)
这里引用矩阵的掩码操作测试用例中的原文(有版权问题的话,请联系删除)
思考一下图像对比度增强的问题。我们可以对图像的每个像素应用下面的公式
上面那种表达法是公式的形式,而下面那种是以掩码矩阵表示的紧凑形式。使用掩码矩阵的时候,我们先把矩阵中心的元素(上面的例子中是(0,0)位置的元素,也就是5)对齐到要计算的目标像素上,再把邻域像素值和相应的矩阵元素值的乘积加起来。虽然这两种形式是完全等价的,但在大矩阵情况下,下面的形式看起来会清楚得多。
现在,我们来看看实现掩码操作的两种方法。一种方法是用基本的像素访问方法,另一种方法是用 filter2D 函数。
先上个官网的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有用指针的例子,还是难不倒我的。
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++算法对比了好久好久,也没发现哪有问题呀(对指针的使用都有些恐惧了)!!!
是不是自己写的图像对比算法有问题呢?
//自写的对比算法(部分)
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,欣喜一阵后,发现生成的图像对比结果还是不一样!!!(不知您有何感想,反正我是几近崩溃,严重怀疑自己的能力了)
C#版无论是根据sharpen算法还是filter2D算法生成的图像都与c++版本生成的图像不一致,只能尝试生成官网的Python版对应的图像了,结果!结果!不一样!!!结果又让我震惊!!!
用filter2D函数生成的图像对比结果:
a)、C++版与Python版一样,但与C#版不一样,
b)、filter2D生成的与Sharpen生成的结果也不一样
用sharpen函数生成的图像对比结果:只有C#Mat.At版与Python版一样,其它都各不相同
对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
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源码一行行的分析(能力有限,还是打住了)!
经过反复的对比与尝试(其中的痛与乐着,只有你有类似经历才能体会到),最终还是发现了问题所在:
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!!!
如果你很快就发现了,给你点赞!暂时没发现的话,我就先卖个关子吧!后面再补充。
虽然犯了一个低级的错误,但最终还是找到了问题所在,有了上面bug的发现与修复,至少把自己从崩溃的边缘拉回来了!
为什么sharpen的对比结果会是Python版与C#版的Mat.At版一致,而与C++版、C#指针版不一致?甚至是C++版与C#的指针版都不一样呢?
C#Mat.At版与Python版是一致,那是不是说明这两张图像应该是正确的,而C++版和C#指针版是有问题的?
将C#Mat.At版与C#指针版的前10x10像素(三块分别对应B,G,R通道的值)输出进行了对比
(在发现问题之前,我已经做了N多次尝试对比、分析,...此处可以省略一万字,还是直接说重点吧,如果你能看到这里,我觉得我写这篇软文,值了!谢谢您的厚爱,给您)
粗一看,上面的两个结果除了0(第一行置为0的)相同,其它都不同,可细细对比,你会发现左边比右边就多出了第二列,后面的数据都是一样的(右边最后一列是多出来的)。
那么结论是:指针操作时,像素位置(地址)有误?!只是C#版指针有误吗?不!与官网的C++版对比,C#版与C++版的功能应该是相同的,官方的源码有BUG?!OpenCV(其实只是Sample)有bug?!我也不敢相信自己会这样怀疑?既然怀疑,那还是分析下官方的源码吧。
一翻研究后,还真找到了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]);
}
眼尖的你,一定发现了关于filter2D函数的Bug是本人一个超低级的错误导致的,在第三个0位置后少了一个“,”,让我抓狂、崩溃的"逗号"!!!
导致核的值变成了
{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;
}
最后,此文献给我未满月的小侄女,愿她健康、快乐地成长!