一,预处理
1,噪声消除
如果不去除噪声,就会检测到比预期更多的对象,中值滤波器通常用于去除椒盐噪声。
Mat img_noise;
medianBlur(img, img_noise, 3);
中值模糊函数需要三个参数:
带有1、3或4通道的输入图像,当内核大于5时,图像深度只能是CV_8U
输出图像,与输入图像相同类型和深度,应用中值模糊算法后的结果图像
内核大小,孔径大于1的奇数
2,用光模式移除背景进行分割
如果采用基本阀值得到的图像伪影有很多白噪声,如果采用光模式和背景去除技术,可以获得一个非常好的结果。需要一张没有任何对象的场景图片,该图片从完全相同的位置并在拍摄其他图像的相同光照条件下拍摄,用简单的数学运算就可以去除这种光模式
减法
减法操作是最简单的方法。如果我们有光模式L和图像模式I,则得到的去除结果R是它们的差:R = L - I
除法
除法有点复杂,R = 255*( 1 - ( I / L ))
在这种情况下,将图像模式除以光模式,假设光模式是白色并且对象比背景更暗,则图像像素值总是等于或小于光像素值。I/L获得的结果在0~ 1之间。最后,将该除法的结果反转以获得相同的颜色方向范围,并将它乘以255以获得0~255范围内的值。
Mat removeLight(Mat img, Mat pattern, int method)
{
Mat aux;
//if method is normalization
if (method == 1)
{
//Require change our image to 32 float for division
Mat img32, pattern32;
img.convertTo(img32, CV_32F);
pattern.convertTo(pattern32, CV_32F);
//Divide the image by the pattern
aux = 1 - (img32 / pattern32);
//Convert 8 bits format and scale
aux.convertTo(aux, CV_8U, 255);
}
else
{
aux = pattern - img;
}
return aux;
}
除法需要32位浮点图像,以便分割图像,而不是将数字截断为整数。
convertTo(OutputArray dst, int rtype, double alpha, double beta)
O(x,y) = cast( alpha * I(x,y) + beta )
如果没有光/背景模式,使用过滤器创建一个可以使用的光/背景模式,为了估计背景图像,我们将使用应用于输入图像的大内核尺寸的模糊技术。
Mat calculateLightPattern(Mat img)
{
Mat pattern;
//Basic and effective way to calculate the light pattern from one image
blur(img, pattern, Size(img.cols / 3, img.cols / 3));
return pattern;
}
3,二值化
删除背景后,需要对图像进行二值化,以便进行分割。
Threshold如果像素值大于阀值,则将像素的值设置为最大值(255),如果像素的值低于阀值,则将其设置为最小值(0)
Mat img_thr;
if (method_light != 2)
threshold(img_no_light, img_thr, 30, 255, THRESH_BINARY);
else
threshold(img_no_light, img_thr, 140, 255, THRESH_BINARY_INV);
当移除光/背景时,采用30作为threshold值,因为应用了背景移除所有不感兴趣的区域都是黑色的,当不使用光移除方法时,采用中等的threshold值140,因为有白色背景。
二,分割输入图像
1,连通组件
连通组件算法是一种迭代算法,其目的是采用八个或四个连通像素来标记图像。如果两个像素具有相同的值并且是邻居,则把它们连接起来。
int connectedComponents(InputArray image, OutputArray labels, int connectivity = 8, int ltype = CV_32S);
int connectedComponentsWithStats(InputArray image, OutputArray labels, OutputArray stats, OutputArray centroids, int connectivity = 8, int ltype = CV_32S);
连通组件算法,返回一个整数,其中包含检测到的标签数,标签0代表背景,这两个函数的区别是返回的信息不同。
image输入图像,labels输出标签图像,connectivity采用的连通方法(4或8),ltype返回标签图像的类型,(CV32_S和CV16_U),默认情况CV32_S。stats是一个输出参数,它给出每个标签(包括背景)以下的统计值:
CC_STAT_LEFT:连通组件对象的最左侧x坐标
CC_STAT_TOP:连通组件对象的最顶部y坐标
CC_STAT_WIDTH:由其边界框定义的连通组件对象的宽度
CC_STAT_HEIGHT:由其边界框定义的连通组件对象的高度
CC_STAT_AREA:连通组件对象的像素数量(区域)
centroids指向每个标签的浮点类型,包括为另一个连通组件考虑的背景。
static Scalar randomColor(RNG& rng)
{
int icolor = (unsigned)rng;
return Scalar(icolor & 255, (icolor >> 8) & 255, (icolor >> 16) & 255);
}
void ConnectedComponents(Mat img)
{
//Use connected components to divide our image in multiple connected component objects
Mat labels;
auto num_objects = connectedComponents(img, labels);
//Check the number of objects detected
if (num_objects < 2)
{
cout << "No objects detected" << endl;
return;
}
else
{
cout << "Number of objects detected:" << num_objects - 1 << endl;
}
//Create output image coloring the objects
Mat output = Mat::zeros(img.rows, img.cols, CV_8UC3);
RNG rng(0xFFFFFFFF);
for (auto i = 1; i < num_objects; i++)
{
Mat mask = labels == i;
output.setTo(randomColor(rng), mask);
}
imshow("Result", output);
}
void ConnectedComponentsStats(Mat img)
{
//Use connected components with stats
Mat labels, stats, centroids;
auto num_objects = connectedComponentsWithStats(img, labels, stats, centroids);
//Check the number of objects detected
if (num_objects < 2)
{
cout << "No objects detected" << endl;
return;
}
else
{
cout << "Number of objects detected:" << num_objects - 1 << endl;
}
//Create output image coloring the objects and show area
Mat output = Mat::zeros(img.rows, img.cols, CV_8UC3);
RNG rng(0xFFFFFFFF);
for (auto i = 1; i < num_objects; i++)
{
cout << "Object " << i << " with pos: " << centroids.at(i) << "with area " << stats.at(i, CC_STAT_AREA) << endl;
Mat mask = labels == i;
output.setTo(randomColor(rng), mask);
//draw text with area
stringstream ss;
ss << "area: " << stats.at(i, CC_STAT_AREA);
putText(output, ss.str(), centroids.at(i), FONT_HERSHEY_SIMPLEX, 0.4, Scalar(255, 255, 255));
}
imshow("Result", output);
}
2,查找轮廓
void findContours( InputArray image, OutputArrayOfArrays contours, int mode, int method, Point offset = Point());
image输入图像,contours轮廓的输出,其中每个检测到的轮廓是点的向量,可选的输出向量,用于保存轮廓的层次结构。层次结构表示为四个索引的向量,它们是(下一个轮廓,前一个轮廓,第一个子轮廓,父轮廓)。检索轮廓的模式:
RETR_EXTERNAL 仅检索外部轮廓
RETR_LIST 在不建立层次结构的情况下检索所有轮廓
RETR_CCOMP检索具有两级层次结构(外部和孔)的所有轮廓,如果另一个对象位于一个孔内,则将其放在层次结构的顶部。
RETR_TREE 检索所有轮廓,在轮廓之间创建完整的层次结构
method使我们能用近似方法来检索轮廓的形状
CV_CHAIN_APPROX_NONE 不对轮廓应用任何近似方法并存储轮廓点
CV_CHAIN_APPROX_SIMPLE 压缩所有水平、垂直和对角线段,公存储起点和终点
CV_CHAIN_APPROX_TC89_L1和CV_CHAIN_APPROX_TC89_KCOS应用Telchin链式近似算法
void drawContours( InputOutputArray image, InputArrayOfArrays contours,
int contourIdx, const Scalar& color,
int thickness = 1, int lineType = LINE_8,
InputArray hierarchy = noArray(),
int maxLevel = INT_MAX, Point offset = Point() );
image要绘制轮廓的输出图像,contours轮廓的向量,表示要绘制轮廓的索引,如果是负数则绘制所有轮廓,color绘制轮廓的颜色,thickness如果为负,则使用所选颜色填充轮廓,lineType指定是否要使用消除锯齿方法或其他绘图方法进行绘制,hierarchy可选参数,仅仅在你想绘制某些轮廓时才需要,maxLevel可选参数,hierarchy参数可用时使用该参数,为0仅绘制指定轮廓,为1绘制当前轮廓和嵌套轮廓,为2绘制所有指定轮廓层次结构,offset用于移动轮廓的可选参数
void FindContoursBasic(Mat img)
{
vector> contours;
findContours(img, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
Mat output = Mat::zeros(img.rows, img.cols, CV_8UC3);
//Check the number of objects detected
if (contours.size() == 0)
{
cout << "No objects detected" << endl;
return;
}
else
cout << "Number of objects detected: " << contours.size() << endl;
RNG rng(0xFFFFFFFF);
for (auto i = 0; i < contours.size(); i++)
{
drawContours(output, contours, i, randomColor(rng));
imshow("Result", output);
}
}
main调用
const char* keys =
{
"{help h usage ? | | print this message}"
"{@image || Image to process}"
"{@lightPattern || Image light pattern to apply to image input}"
"{lightMethod | 1 | Method to remove backgroun light, 0 differenec, 1 div, 2 no light removal' }"
"{segMethod | 1 | Method to segment: 1 connected Components, 2 connectec components with stats, 3 find Contours }"
};
int main(int argc, const char** argv)
{
CommandLineParser parser(argc, argv, keys);
//If requires help show
if (parser.has("help"))
{
parser.printMessage();
return 0;
}
String img_file = parser.get(0);
String light_pattern_file = parser.get(1);
auto method_light = parser.get("lightMethod");
auto method_seg = parser.get("segMethod");
// Check if params are correctly parsed in his variables
if (!parser.check())
{
parser.printErrors();
return 0;
}
// Load image to process
Mat img = imread(img_file, 0);
if (img.data == NULL) {
cout << "Error loading image " << img_file << endl;
return 0;
}
// Remove noise
Mat img_noise, img_box_smooth;
medianBlur(img, img_noise, 3);
blur(img, img_box_smooth, Size(3, 3));
// Load image to process
Mat light_pattern = imread(light_pattern_file, 0);
if (light_pattern.data == NULL) {
// Calculate light pattern
light_pattern = calculateLightPattern(img_noise);
}
medianBlur(light_pattern, light_pattern, 3);
//Apply the light pattern
Mat img_no_light;
img_noise.copyTo(img_no_light);
if (method_light != 2) {
img_no_light = removeLight(img_noise, light_pattern, method_light);
}
// Binarize image for segment
Mat img_thr;
if (method_light != 2) {
threshold(img_no_light, img_thr, 30, 255, THRESH_BINARY);
}
else {
threshold(img_no_light, img_thr, 140, 255, THRESH_BINARY_INV);
}
// Show images
//imshow("Light pattern", light_pattern);
//imshow("No Light", img_no_light);
switch (method_seg) {
case 1:
ConnectedComponents(img_thr);
break;
case 2:
ConnectedComponentsStats(img_thr);
break;
case 3:
FindContoursBasic(img_thr);
break;
}
waitKey(0);
return 0;
}