C++/OpenCV 中的局部阈值实现
local thresholding implementation in C++ / OpenCV
我想实现一个本地阈值算法,我需要你的专业知识。
我的图像已调整为 600x400,灰度。
关于本地化的基本思考过程:
- 使用在每个像素处拍摄的 9x9 ROI 分割图像并计算该区域的最大强度。
创建一个 9x9 内核。
条件:
- 如果遮罩的中心像素高于最大强度的 50%,则将中心像素设置为真。(应用遮罩)
我想问你的问题:
我应该如何选择我的kernel/mask?
cv::Mat ROI;
cv::Mat mask(input.size(),CV_8UC1, cv::Scalar::all(0)); // create mask of 0s at first
const int kerneldepth = 1;
const int kernelsize = 9;
cv::Mat kernel = cv::Mat::ones( kernelsize, kernelsize, CV_8UC1 );
//take ROI of 9x9 and apply a threshold
for( double x = 9; x < input.cols -9; x++ ){
for( double y = 9 ; y < input.rows - 9 ; y++ ){
try{
double x_left = x - 4;
double x_right = x + 4;
double y_up = y + 4;
double y_down = y - 4;
double maxVal;
double minVal;
cv::Point anchor(kernelsize/2,kernelsize/2);
cv::Rect ROI = cv::Rect(x_left,y_down,9,9);
cv::Mat ROI_Mat = input(ROI); // a new matrix for ROI
cv::Scalar avgPixelIntensity = cv::mean( ROI_Mat ); // calculate mean
cv::minMaxLoc(ROI_Mat,&minVal,&maxVal);
if( input.at<uchar>(x,y) >= 0.5*maxVal){
cv::filter2D(input,mask,-1,kernel,anchor,0);
} else { break;}
}
catch (cv::Exception &e){
e.what();
}
}
*****************************UPDATED CODE: ******************************************
applyLocalThresh(cv::Mat &src, cv::Mat& out){
double maxVal, minVal;
cv::Mat output;
int top, bottom, left , right;
int borderType = cv::BORDER_CONSTANT;
cv::Scalar value;
top = (int) (9); bottom = (int) (9);
left = (int) (9); right = (int) (9);
output = src;
out = src;
value = 0;
cv::copyMakeBorder(src,output,top,bottom,left,right,borderType,value);
for(int y = 9; y < src.rows; y++) {
for(int x = 9; x < src.cols; x ++) {
cv::Mat ROI = src(cv::Rect(cv::Point(x-4,y-4),cv::Size(9,9)));
cv::minMaxLoc(ROI,&minVal,&maxVal);
if(src.at<uchar>(cv::Point(x-4,y-4)) >= 0.6*maxVal){
out.at<uchar>(cv::Point(x-4,y-4)) = 255;
}else{
out.at<uchar>(cv::Point(x-4,y-4));
}
}
}
}
恐怕这种做法并不完全正确。让我解释一下:对于涉及内核的操作,必须小心地将 内核的中心 放在要变换的像素的顶部。这是因为 3x3、5x5、7x7、9x9 (...) 内核仅计算图像中 一个像素 的值,也就是位于中心 [0,0] 的像素内核。
如果您考虑如何计算图像第一个像素的值,9x9 内核的中心将位于坐标 [0,0] .这意味着 3/4 的内核将被放置在负坐标处,即引用不存在的像素的坐标:
[-4,-4][-3,-4][-2,-4][-1,-4][ 0,-4][ 1,-4][ 2,-4][ 3,-4][ 4,-4]
[-4,-3][-3,-3][-2,-3][-1,-3][ 0,-3][ 1,-3][ 2,-3][ 3,-3][ 4,-3]
[-4,-2][-3,-2][-2,-2][-1,-2][ 0,-2][ 1,-2][ 2,-2][ 3,-2][ 4,-2]
[-4,-1][-3,-1][-2,-1][-1,-1][ 0,-1][ 1,-1][ 2,-1][ 3,-1][ 4,-1]
[-4, 0][-3, 0][-2, 0][-1, 0][ 0, 0][ 1, 0][ 2, 0][ 3, 0][ 4, 0]
[-4, 1][-3, 1][-2, 1][-1, 1][ 0, 1][ 1, 1][ 2, 1][ 3, 1][ 4, 1]
[-4, 2][-3, 2][-2, 2][-1, 2][ 0, 2][ 1, 2][ 2, 2][ 3, 2][ 4, 2]
[-4, 3][-3, 3][-2, 3][-1, 3][ 0, 3][ 1, 3][ 2, 3][ 3, 3][ 4, 3]
[-4, 4][-3, 4][-2, 4][-1, 4][ 0, 4][ 1, 4][ 2, 4][ 3, 4][ 4, 4]
图像边界附近的像素总是会发生这种情况。因此对于第一个像素的计算,我们必须将计算限制为 1/4 kernel的,指的是目标图像中的有效坐标:
[ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ 0, 0][ 1, 0][ 2, 0][ 3, 0][ 4, 0]
[ ][ ][ ][ ][ 0, 1][ 1, 1][ 2, 1][ 3, 1][ 4, 1]
[ ][ ][ ][ ][ 0, 2][ 1, 2][ 2, 2][ 3, 2][ 4, 2]
[ ][ ][ ][ ][ 0, 3][ 1, 3][ 2, 3][ 3, 3][ 4, 3]
[ ][ ][ ][ ][ 0, 4][ 1, 4][ 2, 4][ 3, 4][ 4, 4]
所以您当前方法的问题在于,在某些时候您将设置一个将具有负坐标的 ROI,并且在执行这些指令时您会看到一个很好的崩溃:
cv::Mat ROI_Mat = input(ROI); // crash
解决方案是不使用 ROI,而是自己实现该算法。我只是看不到这种自定义计算与 cv::filter2D()
一起工作。这里有一些可以帮助您入门的小东西:
void local_threshold(const cv::Mat& input, cv::Mat& output)
{
if (input.channels() != 1)
{
std::cout << "local_threshold !!! input image must be single channel" << std::endl;
return;
}
output = cv::Mat(input.rows, input.cols, CV_8UC1);
double min_val = 0, max_val = 0;
for (int i = 0; i < input.rows; i++)
for (int j = 0; j < input.cols; j++)
{
cv::Mat kernel = Mat::zeros(9, 9, output.type());
// Implement logic to fill the 9x9 kernel with
// values from the input Mat, respecting boundaries.
cv::Scalar avg_intensity = cv::mean(kernel);
cv::minMaxLoc(kernel, &min_val,&max_val);
if (input.at<uchar>(i,j) > (max_val / 2))
output.at<unsigned char>(i,j) = 255;
else
output.at<unsigned char>(i,j) = 0;
}
}
您可以在 OpenCV 中先进行扩张然后进行比较;
im = load image here;
di = dilate im with a 9x9 kernel;
bw = im > (di * 0.5); // in OpenCV, pixels of bw are set to 255 or 0
在 Matlab/Octave:
中使用 4x6 图像和 3x3 内核来说明这一点的简单示例
我=
1 2 3 4 5 6
2 3 4 5 6 7
3 4 5 6 7 8
4 5 6 7 8 9
二=
3 4 5 6 7 7
4 5 6 7 8 8
5 6 7 8 9 9
5 6 7 8 9 9
th = di * .5
第 个 =
1.5000 2.0000 2.5000 3.0000 3.5000 3.5000
2.0000 2.5000 3.0000 3.5000 4.0000 4.0000
2.5000 3.0000 3.5000 4.0000 4.5000 4.5000
2.5000 3.0000 3.5000 4.0000 4.5000 4.5000
bw = im > th
体重=
0 0 1 1 1 1
0 1 1 1 1 1
1 1 1 1 1 1
1 1 1 1 1 1
在进一步思考并找出如何利用我的编程基础知识之后,我想出了这段代码,虽然效率不高,但可以完成工作。
我的方法的主要问题是什么? :
- 主要问题之一的边界像素以及内核和掩码之间的整个索引操作引起了轻微的头痛。
我解决这个问题的方法是什么?:
- 我的阈值表示需要相对高的强度级别才能设置真实像素。因此,我用一些假想的负像素填充了图像,并使我的算法从原始图像的第一个像素开始。并将结果保存到遮罩中。
结果:
- 成功!
代码:
double maxVal, minVal;
cv::Mat output;
int top, bottom, left , right;
int borderType = cv::BORDER_CONSTANT;
cv::Scalar value;
top = (int) (4); bottom = (int) (4);
left = (int) (4); right = (int) (4);
output = src;
out = src;
value = 0;
cv::copyMakeBorder(src,output,top,bottom,left,right,borderType,value);
for(int y = 4; y < output.rows - 4; y++) {
for(int x = 4; x < output.cols - 4; x ++) {
// apply local ROI
cv::Mat ROI = output(cv::Rect(cv::Point(x-4,y-4),cv::Size(9,9)));
cv::minMaxLoc(ROI,&minVal,&maxVal); // extract max intensity values in the ROI
if(src.at<uchar>(cv::Point(x-4,y-4)) >= 0.5*maxVal){ // apply local threshold w.r.t highest intensity level
out.at<uchar>(cv::Point(x-4,y-4)) = 255; // change pixel value in mask if true
}else{
out.at<uchar>(cv::Point(x-4,y-4)) = 0;
}
}
}
}
我知道它需要一些清理,但希望这会帮助其他人了解一些想法。
我想实现一个本地阈值算法,我需要你的专业知识。
我的图像已调整为 600x400,灰度。
关于本地化的基本思考过程:
- 使用在每个像素处拍摄的 9x9 ROI 分割图像并计算该区域的最大强度。
创建一个 9x9 内核。
条件:
- 如果遮罩的中心像素高于最大强度的 50%,则将中心像素设置为真。(应用遮罩)
我想问你的问题:
我应该如何选择我的kernel/mask?
cv::Mat ROI; cv::Mat mask(input.size(),CV_8UC1, cv::Scalar::all(0)); // create mask of 0s at first const int kerneldepth = 1; const int kernelsize = 9; cv::Mat kernel = cv::Mat::ones( kernelsize, kernelsize, CV_8UC1 ); //take ROI of 9x9 and apply a threshold for( double x = 9; x < input.cols -9; x++ ){ for( double y = 9 ; y < input.rows - 9 ; y++ ){ try{ double x_left = x - 4; double x_right = x + 4; double y_up = y + 4; double y_down = y - 4; double maxVal; double minVal; cv::Point anchor(kernelsize/2,kernelsize/2); cv::Rect ROI = cv::Rect(x_left,y_down,9,9); cv::Mat ROI_Mat = input(ROI); // a new matrix for ROI cv::Scalar avgPixelIntensity = cv::mean( ROI_Mat ); // calculate mean cv::minMaxLoc(ROI_Mat,&minVal,&maxVal); if( input.at<uchar>(x,y) >= 0.5*maxVal){ cv::filter2D(input,mask,-1,kernel,anchor,0); } else { break;} } catch (cv::Exception &e){ e.what(); } } *****************************UPDATED CODE: ****************************************** applyLocalThresh(cv::Mat &src, cv::Mat& out){ double maxVal, minVal; cv::Mat output; int top, bottom, left , right; int borderType = cv::BORDER_CONSTANT; cv::Scalar value; top = (int) (9); bottom = (int) (9); left = (int) (9); right = (int) (9); output = src; out = src; value = 0; cv::copyMakeBorder(src,output,top,bottom,left,right,borderType,value); for(int y = 9; y < src.rows; y++) { for(int x = 9; x < src.cols; x ++) { cv::Mat ROI = src(cv::Rect(cv::Point(x-4,y-4),cv::Size(9,9))); cv::minMaxLoc(ROI,&minVal,&maxVal); if(src.at<uchar>(cv::Point(x-4,y-4)) >= 0.6*maxVal){ out.at<uchar>(cv::Point(x-4,y-4)) = 255; }else{ out.at<uchar>(cv::Point(x-4,y-4)); } } } }
恐怕这种做法并不完全正确。让我解释一下:对于涉及内核的操作,必须小心地将 内核的中心 放在要变换的像素的顶部。这是因为 3x3、5x5、7x7、9x9 (...) 内核仅计算图像中 一个像素 的值,也就是位于中心 [0,0] 的像素内核。
如果您考虑如何计算图像第一个像素的值,9x9 内核的中心将位于坐标 [0,0] .这意味着 3/4 的内核将被放置在负坐标处,即引用不存在的像素的坐标:
[-4,-4][-3,-4][-2,-4][-1,-4][ 0,-4][ 1,-4][ 2,-4][ 3,-4][ 4,-4]
[-4,-3][-3,-3][-2,-3][-1,-3][ 0,-3][ 1,-3][ 2,-3][ 3,-3][ 4,-3]
[-4,-2][-3,-2][-2,-2][-1,-2][ 0,-2][ 1,-2][ 2,-2][ 3,-2][ 4,-2]
[-4,-1][-3,-1][-2,-1][-1,-1][ 0,-1][ 1,-1][ 2,-1][ 3,-1][ 4,-1]
[-4, 0][-3, 0][-2, 0][-1, 0][ 0, 0][ 1, 0][ 2, 0][ 3, 0][ 4, 0]
[-4, 1][-3, 1][-2, 1][-1, 1][ 0, 1][ 1, 1][ 2, 1][ 3, 1][ 4, 1]
[-4, 2][-3, 2][-2, 2][-1, 2][ 0, 2][ 1, 2][ 2, 2][ 3, 2][ 4, 2]
[-4, 3][-3, 3][-2, 3][-1, 3][ 0, 3][ 1, 3][ 2, 3][ 3, 3][ 4, 3]
[-4, 4][-3, 4][-2, 4][-1, 4][ 0, 4][ 1, 4][ 2, 4][ 3, 4][ 4, 4]
图像边界附近的像素总是会发生这种情况。因此对于第一个像素的计算,我们必须将计算限制为 1/4 kernel的,指的是目标图像中的有效坐标:
[ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ ][ ][ ][ ][ ]
[ ][ ][ ][ ][ 0, 0][ 1, 0][ 2, 0][ 3, 0][ 4, 0]
[ ][ ][ ][ ][ 0, 1][ 1, 1][ 2, 1][ 3, 1][ 4, 1]
[ ][ ][ ][ ][ 0, 2][ 1, 2][ 2, 2][ 3, 2][ 4, 2]
[ ][ ][ ][ ][ 0, 3][ 1, 3][ 2, 3][ 3, 3][ 4, 3]
[ ][ ][ ][ ][ 0, 4][ 1, 4][ 2, 4][ 3, 4][ 4, 4]
所以您当前方法的问题在于,在某些时候您将设置一个将具有负坐标的 ROI,并且在执行这些指令时您会看到一个很好的崩溃:
cv::Mat ROI_Mat = input(ROI); // crash
解决方案是不使用 ROI,而是自己实现该算法。我只是看不到这种自定义计算与 cv::filter2D()
一起工作。这里有一些可以帮助您入门的小东西:
void local_threshold(const cv::Mat& input, cv::Mat& output)
{
if (input.channels() != 1)
{
std::cout << "local_threshold !!! input image must be single channel" << std::endl;
return;
}
output = cv::Mat(input.rows, input.cols, CV_8UC1);
double min_val = 0, max_val = 0;
for (int i = 0; i < input.rows; i++)
for (int j = 0; j < input.cols; j++)
{
cv::Mat kernel = Mat::zeros(9, 9, output.type());
// Implement logic to fill the 9x9 kernel with
// values from the input Mat, respecting boundaries.
cv::Scalar avg_intensity = cv::mean(kernel);
cv::minMaxLoc(kernel, &min_val,&max_val);
if (input.at<uchar>(i,j) > (max_val / 2))
output.at<unsigned char>(i,j) = 255;
else
output.at<unsigned char>(i,j) = 0;
}
}
您可以在 OpenCV 中先进行扩张然后进行比较;
im = load image here;
di = dilate im with a 9x9 kernel;
bw = im > (di * 0.5); // in OpenCV, pixels of bw are set to 255 or 0
在 Matlab/Octave:
中使用 4x6 图像和 3x3 内核来说明这一点的简单示例我=
1 2 3 4 5 6
2 3 4 5 6 7
3 4 5 6 7 8
4 5 6 7 8 9
二=
3 4 5 6 7 7
4 5 6 7 8 8
5 6 7 8 9 9
5 6 7 8 9 9
th = di * .5
第个 =
1.5000 2.0000 2.5000 3.0000 3.5000 3.5000
2.0000 2.5000 3.0000 3.5000 4.0000 4.0000
2.5000 3.0000 3.5000 4.0000 4.5000 4.5000
2.5000 3.0000 3.5000 4.0000 4.5000 4.5000
bw = im > th
体重=
0 0 1 1 1 1
0 1 1 1 1 1
1 1 1 1 1 1
1 1 1 1 1 1
在进一步思考并找出如何利用我的编程基础知识之后,我想出了这段代码,虽然效率不高,但可以完成工作。
我的方法的主要问题是什么? :
- 主要问题之一的边界像素以及内核和掩码之间的整个索引操作引起了轻微的头痛。
我解决这个问题的方法是什么?:
- 我的阈值表示需要相对高的强度级别才能设置真实像素。因此,我用一些假想的负像素填充了图像,并使我的算法从原始图像的第一个像素开始。并将结果保存到遮罩中。
结果:
- 成功!
代码:
double maxVal, minVal;
cv::Mat output;
int top, bottom, left , right;
int borderType = cv::BORDER_CONSTANT;
cv::Scalar value;
top = (int) (4); bottom = (int) (4);
left = (int) (4); right = (int) (4);
output = src;
out = src;
value = 0;
cv::copyMakeBorder(src,output,top,bottom,left,right,borderType,value);
for(int y = 4; y < output.rows - 4; y++) {
for(int x = 4; x < output.cols - 4; x ++) {
// apply local ROI
cv::Mat ROI = output(cv::Rect(cv::Point(x-4,y-4),cv::Size(9,9)));
cv::minMaxLoc(ROI,&minVal,&maxVal); // extract max intensity values in the ROI
if(src.at<uchar>(cv::Point(x-4,y-4)) >= 0.5*maxVal){ // apply local threshold w.r.t highest intensity level
out.at<uchar>(cv::Point(x-4,y-4)) = 255; // change pixel value in mask if true
}else{
out.at<uchar>(cv::Point(x-4,y-4)) = 0;
}
}
}
}
我知道它需要一些清理,但希望这会帮助其他人了解一些想法。