OpenCV Sobel 过滤器 - 为什么它看起来很糟糕,尤其是与 Gimp 相比?
OpenCV's Sobel filter - why does it look so bad, especially compared to Gimp?
我正在尝试使用 OpenCV 重建我之前在 Gimp 中完成的一些预处理。第一阶段是用于边缘检测的 Sobel 滤波器。它在 Gimp 中运行良好:
现在这是我对 OpenCV 的尝试:
opencv_imgproc.Sobel(/* src = */ scaled, /* dst = */ sobel,
/* ddepth = */ opencv_core.CV_32F,
/* dx = */ 1, /* dy = */ 1, /* ksize = */ 5, /* scale = */ 0.25,
/* delta = */ 0.0, /* borderType = */ opencv_core.BORDER_REPLICATE)
看起来很糟糕,基本上是突出点而不是轮廓:
那么我哪里做错了,或者说 Gimp 是如何取得这么好的结果的,我怎样才能在 OpenCV 中复制它?
Sobel 常用于 X 和 Y 方向,然后结合起来为每个像素生成一个 2D 向量。也就是说,它给出了 2D 中每个像素的渐变(如果你已经明白了,我深表歉意,但它让我要说的更清楚)。
二维矢量在单个像素中的准确表示方式有待解释。从这些图像中,看起来 OpenCV 比 Gimp 更突出水平线,而 Gimp 比 OpenCV 更突出垂直线。
鉴于您的图像是彩色的,因此可以对 RGB 中的矢量进行一些解释。我会比较图像之间 RGB space 中单个像素的值,以查看它们是如何建模的。您可能只需要移动组件。
信息
使用的图像来自 https://www.pexels.com/photo/brown-wooden-flooring-hallway-176162/ ("Free for personal and commercial use")。
解决方案 TL;DR
通过 Sobel 过滤器进行边缘检测需要 两个单独的过滤器 操作。它不能一步完成。
两个独立步骤的结果必须结合起来形成边缘检测的最终结果。
信息:为了简单起见,我使用浮动图像 (CV_32F)
。
代码中的解决方案:
// Load example image
std::string path = "C:\Temp\SobelTest\Lobby2\";
std::string filename = "pexels-photo-176162 scaled down.jpeg";
std::string fqn = path + filename;
cv::Mat img = cv::imread(fqn, CV_LOAD_IMAGE_COLOR); // Value range: 0 - 255
// Convert to float and adapt value range (for simplicity)
img.convertTo(img, CV_32F, 1.f/255); // Value range: 0.0 - 1.0
// Build data for 3x3 vertical Sobel kernel
float sobelKernelHorizontalData[3][3] =
{
{-1, 0, 1},
{-2, 0, 2},
{-1, 0, 1}
};
// Calculate normalization divisor/factor
float sobelKernelNormalizationDivisor = 4.f;
float sobelKernelNormalizationFactor = 1.f / sobelKernelNormalizationDivisor;
// Generate cv::Mat for vertical filter kernel
cv::Mat sobelKernelHorizontal =
cv::Mat(3,3, CV_32F, sobelKernelHorizontalData); // Value range of filter result (if it is used for filtering): 0 - 4*255 or 0.0 - 4.0
// Apply filter kernel normalization
sobelKernelHorizontal *= sobelKernelNormalizationFactor; // Value range of filter result (if it is used for filtering): 0 - 255 or 0.0 - 1.0
// Generate cv::Mat for horizontal filter kernel
cv::Mat sobelKernelVertical;
cv::transpose(sobelKernelHorizontal, sobelKernelVertical);
// Apply two distinct Sobel filtering steps
cv::Mat imgFilterResultVertical;
cv::Mat imgFilterResultHorizontal;
cv::filter2D(img, imgFilterResultVertical, CV_32F, sobelKernelVertical);
cv::filter2D(img, imgFilterResultHorizontal, CV_32F, sobelKernelHorizontal);
// Build overall filter result by combining the previous results
cv::Mat imgFilterResultMagnitude;
cv::magnitude(imgFilterResultVertical, imgFilterResultHorizontal, imgFilterResultMagnitude);
// Write images to HDD. Important: convert back to uchar, otherwise we get black images
std::string filenameFilterResultVertical = path + "imgFilterResultVertical" + ".jpeg";
std::string filenameFilterResultHorizontal = path + "imgFilterResultHorizontal" + ".jpeg";
std::string filenameFilterResultMagnitude = path + "imgFilterResultMagnitude" + ".jpeg";
cv::Mat imgFilterResultVerticalUchar;
cv::Mat imgFilterResultHorizontalUchar;
cv::Mat imgFilterResultMagnitudeUchar;
imgFilterResultVertical.convertTo(imgFilterResultVerticalUchar, CV_8UC3, 255);
imgFilterResultHorizontal.convertTo(imgFilterResultHorizontalUchar, CV_8UC3, 255);
imgFilterResultMagnitude.convertTo(imgFilterResultMagnitudeUchar, CV_8UC3, 255);
cv::imwrite(filenameFilterResultVertical, imgFilterResultVerticalUchar);
cv::imwrite(filenameFilterResultHorizontal, imgFilterResultHorizontalUchar);
cv::imwrite(filenameFilterResultMagnitude, imgFilterResultMagnitudeUchar);
// Show images
cv::imshow("img", img);
cv::imshow("imgFilterResultVertical", imgFilterResultVertical);
cv::imshow("imgFilterResultHorizontal", imgFilterResultHorizontal);
cv::imshow("imgFilterResultMagnitude", imgFilterResultMagnitude);
cv::waitKey();
请注意,此代码等同于:
cv::Sobel(img, imgFilterResultVertical, CV_32F, 1, 0, 3, sobelKernelNormalizationFactor);
cv::Sobel(img, imgFilterResultHorizontal, CV_32F, 0, 1, 3, sobelKernelNormalizationFactor);
cv::magnitude(imgFilterResultVertical, imgFilterResultHorizontal, imgFilterResultMagnitude);
结果图片
源图、垂直滤波结果、水平滤波结果、组合滤波结果(量级)
关于 OpenCV 的数据类型和取值范围的简短信息
- 使用浮动图像(图像类型
CV_32F
)通常非常有用,有时甚至更简单。但是,使用浮动图像
也较慢,因为使用了 4 倍的数据(与 uchar 相比)。所以如果你想要正确性和高性能,
您将不得不仅使用 uchar 图像,并始终将正确的除数(参数 "alpha")传递给 OpenCV 函数。
但是,这更容易出错,并且您的值可能会在您没有意识到的情况下溢出。
- 8 位图像 (uchar, CV_8UC) 的取值范围为 0 - 255。32 位浮点图像 (CV_32F) 的取值范围为 0.0 - 1.0(值越大比 1.0 将显示为与 1.0 相同)。使用 32 位图像通常更容易,因为不太可能发生溢出(但是缩放比例不佳,例如 1.0 以上的值可能会发生)。
计算核归一化因子
内核的归一化因子可以通过以下公式计算:
f = max(abs(sumNegative), abs(sumPositive))
其中 sumNegative 是内核中负值的总和,sumPositive 是内核中正值的总和。
警告:这不等于 float normalizationDivisor = cv::sum(cv::abs(kernel))(0)
,您必须为此编写一个自定义函数。
其他提示
- 边缘检测取决于分辨率以及边缘厚度。
如果要检测的边缘比较粗,可以使用较大的 Sobel 滤波器内核
尺寸(参见 Sobel filter kernel of large size ,但不要使用已接受的答案。
而是使用 Adam Bowen 的答案(很可能是正确的答案)。
当然,您也可以缩小图像并使用默认的 3x3 Sobel 过滤器来检测厚边。
- 使用较大的滤波器内核会导致不同的归一化除数/因数。
- Sobel 过滤器只是关于邻域距离的粗略近似。 Scharr 滤波器代表了对 Sobel 滤波器的改进,因为它 "improves rotational invariance" [http://johncostella.com/edgedetect/ ]
- 要保存彩色浮点图像,您必须使用 convertTo
将它们转换(和缩放)回 uchar
彩色图像的边缘检测
在彩色图像上应用边缘检测过滤器通常是没有意义的。
让图像显示哪个颜色通道(B、G、R)对边缘检测有多大贡献,并将此结果 "encoding" 显示为彩色像素是一个非常具体且不常见的过程。
当然,如果您的目标只是让图像看起来 "cool",那就继续吧。在这种情况下,大多数规则都不适用。
更新2018-04-24
在反复反思我多年来所写的内容和图像过滤方面的工作之后,我不得不承认:彩色图像的边缘检测很有用是有非常有效和重要的原因的。
简单地说:如果图像中存在灰度图像中不可见的边缘,则您需要对彩色图像进行边缘检测。
显然,这就是(两个)不同颜色区域之间的边缘,其中颜色相当可辨,而它们的灰度值(大致)相同。
这可能会以非直觉的方式发生,因为作为人类,我们习惯于看到颜色。
如果您的应用程序希望在此类用例中保持稳健,您应该更喜欢使用彩色图像而不是灰色图像进行边缘检测。
由于彩色图像的过滤步骤会产生 3 通道边缘图像,因此必须将结果合理地转换为单个代表性边缘图像。
这个转换步骤可以通过多种方式完成:
- 简单平均
- 以与手动计算图像亮度时加权B、G、R通道(0.11、0.59、0.30)相同的方式进行加权计算(这将导致边缘图像已经非常接近人类感知)
- 通过加权计算各个颜色之间人类感知的对比度(可能有一些基于 LAB 的方法……)
- 使用 3 个通道中每个像素的最大值
- 等等
这取决于你到底想达到什么目的,以及你想为此付出多少努力。
通常,平均或 RGB-/BGR-based 加权就足够了。
我正在尝试使用 OpenCV 重建我之前在 Gimp 中完成的一些预处理。第一阶段是用于边缘检测的 Sobel 滤波器。它在 Gimp 中运行良好:
现在这是我对 OpenCV 的尝试:
opencv_imgproc.Sobel(/* src = */ scaled, /* dst = */ sobel,
/* ddepth = */ opencv_core.CV_32F,
/* dx = */ 1, /* dy = */ 1, /* ksize = */ 5, /* scale = */ 0.25,
/* delta = */ 0.0, /* borderType = */ opencv_core.BORDER_REPLICATE)
看起来很糟糕,基本上是突出点而不是轮廓:
那么我哪里做错了,或者说 Gimp 是如何取得这么好的结果的,我怎样才能在 OpenCV 中复制它?
Sobel 常用于 X 和 Y 方向,然后结合起来为每个像素生成一个 2D 向量。也就是说,它给出了 2D 中每个像素的渐变(如果你已经明白了,我深表歉意,但它让我要说的更清楚)。
二维矢量在单个像素中的准确表示方式有待解释。从这些图像中,看起来 OpenCV 比 Gimp 更突出水平线,而 Gimp 比 OpenCV 更突出垂直线。
鉴于您的图像是彩色的,因此可以对 RGB 中的矢量进行一些解释。我会比较图像之间 RGB space 中单个像素的值,以查看它们是如何建模的。您可能只需要移动组件。
信息
使用的图像来自 https://www.pexels.com/photo/brown-wooden-flooring-hallway-176162/ ("Free for personal and commercial use")。
解决方案 TL;DR
通过 Sobel 过滤器进行边缘检测需要 两个单独的过滤器 操作。它不能一步完成。 两个独立步骤的结果必须结合起来形成边缘检测的最终结果。
信息:为了简单起见,我使用浮动图像 (CV_32F)
。
代码中的解决方案:
// Load example image
std::string path = "C:\Temp\SobelTest\Lobby2\";
std::string filename = "pexels-photo-176162 scaled down.jpeg";
std::string fqn = path + filename;
cv::Mat img = cv::imread(fqn, CV_LOAD_IMAGE_COLOR); // Value range: 0 - 255
// Convert to float and adapt value range (for simplicity)
img.convertTo(img, CV_32F, 1.f/255); // Value range: 0.0 - 1.0
// Build data for 3x3 vertical Sobel kernel
float sobelKernelHorizontalData[3][3] =
{
{-1, 0, 1},
{-2, 0, 2},
{-1, 0, 1}
};
// Calculate normalization divisor/factor
float sobelKernelNormalizationDivisor = 4.f;
float sobelKernelNormalizationFactor = 1.f / sobelKernelNormalizationDivisor;
// Generate cv::Mat for vertical filter kernel
cv::Mat sobelKernelHorizontal =
cv::Mat(3,3, CV_32F, sobelKernelHorizontalData); // Value range of filter result (if it is used for filtering): 0 - 4*255 or 0.0 - 4.0
// Apply filter kernel normalization
sobelKernelHorizontal *= sobelKernelNormalizationFactor; // Value range of filter result (if it is used for filtering): 0 - 255 or 0.0 - 1.0
// Generate cv::Mat for horizontal filter kernel
cv::Mat sobelKernelVertical;
cv::transpose(sobelKernelHorizontal, sobelKernelVertical);
// Apply two distinct Sobel filtering steps
cv::Mat imgFilterResultVertical;
cv::Mat imgFilterResultHorizontal;
cv::filter2D(img, imgFilterResultVertical, CV_32F, sobelKernelVertical);
cv::filter2D(img, imgFilterResultHorizontal, CV_32F, sobelKernelHorizontal);
// Build overall filter result by combining the previous results
cv::Mat imgFilterResultMagnitude;
cv::magnitude(imgFilterResultVertical, imgFilterResultHorizontal, imgFilterResultMagnitude);
// Write images to HDD. Important: convert back to uchar, otherwise we get black images
std::string filenameFilterResultVertical = path + "imgFilterResultVertical" + ".jpeg";
std::string filenameFilterResultHorizontal = path + "imgFilterResultHorizontal" + ".jpeg";
std::string filenameFilterResultMagnitude = path + "imgFilterResultMagnitude" + ".jpeg";
cv::Mat imgFilterResultVerticalUchar;
cv::Mat imgFilterResultHorizontalUchar;
cv::Mat imgFilterResultMagnitudeUchar;
imgFilterResultVertical.convertTo(imgFilterResultVerticalUchar, CV_8UC3, 255);
imgFilterResultHorizontal.convertTo(imgFilterResultHorizontalUchar, CV_8UC3, 255);
imgFilterResultMagnitude.convertTo(imgFilterResultMagnitudeUchar, CV_8UC3, 255);
cv::imwrite(filenameFilterResultVertical, imgFilterResultVerticalUchar);
cv::imwrite(filenameFilterResultHorizontal, imgFilterResultHorizontalUchar);
cv::imwrite(filenameFilterResultMagnitude, imgFilterResultMagnitudeUchar);
// Show images
cv::imshow("img", img);
cv::imshow("imgFilterResultVertical", imgFilterResultVertical);
cv::imshow("imgFilterResultHorizontal", imgFilterResultHorizontal);
cv::imshow("imgFilterResultMagnitude", imgFilterResultMagnitude);
cv::waitKey();
请注意,此代码等同于:
cv::Sobel(img, imgFilterResultVertical, CV_32F, 1, 0, 3, sobelKernelNormalizationFactor);
cv::Sobel(img, imgFilterResultHorizontal, CV_32F, 0, 1, 3, sobelKernelNormalizationFactor);
cv::magnitude(imgFilterResultVertical, imgFilterResultHorizontal, imgFilterResultMagnitude);
结果图片
源图、垂直滤波结果、水平滤波结果、组合滤波结果(量级)
关于 OpenCV 的数据类型和取值范围的简短信息
- 使用浮动图像(图像类型
CV_32F
)通常非常有用,有时甚至更简单。但是,使用浮动图像 也较慢,因为使用了 4 倍的数据(与 uchar 相比)。所以如果你想要正确性和高性能, 您将不得不仅使用 uchar 图像,并始终将正确的除数(参数 "alpha")传递给 OpenCV 函数。 但是,这更容易出错,并且您的值可能会在您没有意识到的情况下溢出。 - 8 位图像 (uchar, CV_8UC) 的取值范围为 0 - 255。32 位浮点图像 (CV_32F) 的取值范围为 0.0 - 1.0(值越大比 1.0 将显示为与 1.0 相同)。使用 32 位图像通常更容易,因为不太可能发生溢出(但是缩放比例不佳,例如 1.0 以上的值可能会发生)。
计算核归一化因子
内核的归一化因子可以通过以下公式计算:
f = max(abs(sumNegative), abs(sumPositive))
其中 sumNegative 是内核中负值的总和,sumPositive 是内核中正值的总和。
警告:这不等于 float normalizationDivisor = cv::sum(cv::abs(kernel))(0)
,您必须为此编写一个自定义函数。
其他提示
- 边缘检测取决于分辨率以及边缘厚度。 如果要检测的边缘比较粗,可以使用较大的 Sobel 滤波器内核 尺寸(参见 Sobel filter kernel of large size ,但不要使用已接受的答案。 而是使用 Adam Bowen 的答案(很可能是正确的答案)。 当然,您也可以缩小图像并使用默认的 3x3 Sobel 过滤器来检测厚边。
- 使用较大的滤波器内核会导致不同的归一化除数/因数。
- Sobel 过滤器只是关于邻域距离的粗略近似。 Scharr 滤波器代表了对 Sobel 滤波器的改进,因为它 "improves rotational invariance" [http://johncostella.com/edgedetect/ ]
- 要保存彩色浮点图像,您必须使用 convertTo 将它们转换(和缩放)回 uchar
彩色图像的边缘检测
在彩色图像上应用边缘检测过滤器通常是没有意义的。
让图像显示哪个颜色通道(B、G、R)对边缘检测有多大贡献,并将此结果 "encoding" 显示为彩色像素是一个非常具体且不常见的过程。
当然,如果您的目标只是让图像看起来 "cool",那就继续吧。在这种情况下,大多数规则都不适用。
更新2018-04-24
在反复反思我多年来所写的内容和图像过滤方面的工作之后,我不得不承认:彩色图像的边缘检测很有用是有非常有效和重要的原因的。
简单地说:如果图像中存在灰度图像中不可见的边缘,则您需要对彩色图像进行边缘检测。 显然,这就是(两个)不同颜色区域之间的边缘,其中颜色相当可辨,而它们的灰度值(大致)相同。 这可能会以非直觉的方式发生,因为作为人类,我们习惯于看到颜色。 如果您的应用程序希望在此类用例中保持稳健,您应该更喜欢使用彩色图像而不是灰色图像进行边缘检测。
由于彩色图像的过滤步骤会产生 3 通道边缘图像,因此必须将结果合理地转换为单个代表性边缘图像。
这个转换步骤可以通过多种方式完成: - 简单平均 - 以与手动计算图像亮度时加权B、G、R通道(0.11、0.59、0.30)相同的方式进行加权计算(这将导致边缘图像已经非常接近人类感知) - 通过加权计算各个颜色之间人类感知的对比度(可能有一些基于 LAB 的方法……) - 使用 3 个通道中每个像素的最大值 - 等等
这取决于你到底想达到什么目的,以及你想为此付出多少努力。 通常,平均或 RGB-/BGR-based 加权就足够了。