题目要求:提取一张背景简单的图像中物体的信息,包括面积,质心等特征;考虑图像存在噪声,首先对图像进行预处理,然后使用分割算法,在图像中标注对应信息;可能的话考虑在一个窗口中显示图像处理过程结果;
分析:
1)对图像降噪的基本流程包括:噪声消除、光消除、二值化;
2)常用的分割算法包括:轮廓检测findContours()和连通域分析connectedComponent();
3)考虑设计多窗口类MWindow,支持初始化定义窗口大小、子窗口数量设计;
4)考虑到光消除算法可以提供背景图,也可以不提供背景图,分割算法可以之获取分割图像,可以获取连通域面积特征,也可以获取连通域质心或轮廓特征,因此设计支持命令行控制的CommandLineParser对象;
代码实现:
- MWindow类设计:
// mwindow.hpp
/*
@File :mwindow.hpp
@Description: :multiwindow to displace multi images
@Date :2021/12/22 15:56:02
@Author :xieyin
@version :1.0
*/
#pragma once
#include<iostream>
#include<string>
#include<vector>
using namespace std;
#include<opencv2/core.hpp>
#include<opencv2/highgui.hpp>
using namespace cv;
class MWindow{
public:
// consturtor
MWindow(string windowTitle, int rows, int cols, int flags);
// add image into canvas
int addImage(string title, Mat img, bool render = false);
// remove image from canvas
void removeImage(int pos);
// adjust all image size in canvas
void render();
private:
int mRows, mCols;
string mWindowTitle;
Mat mCanvas;
vector<string> mSubTitles;
vector<Mat> mSubImages;
};
MWindow::MWindow(string windowTitle, int rows, int cols, int flags):mWindowTitle(windowTitle), mRows(rows), mCols(cols){
/*
@description : MWindow constructor
@param :
windowTitle : whole window title
rows : sub window rows
cols : sub window cols
flags : namedWindow flags (eg, WINDOW_AUTOSIZE)
@Returns :
*/
// create canvas
namedWindow(mWindowTitle, flags);
mCanvas = Mat(700, 1500, CV_8UC3);
imshow(mWindowTitle, mCanvas);
}
int MWindow::addImage(string title, Mat img, bool render){
/*
@description : add title and image into canvas
@param :
title : sub image title
img : image to be added
render : render(flag) whether need to adjust the image for canvas
@Returns :
index : sub image index in total mRows * mCols
*/
mSubTitles.push_back(title);
mSubImages.push_back(img);
if(render){
MWindow::render();
}
return mSubImages.size() - 1;
}
void MWindow::removeImage(int pos){
/*
@description : remove image from canvas based on index
@param :
pos : sub image index in total mRows * mCols
@Returns :
None
*/
mSubTitles.erase(mSubTitles.begin() + pos);
mSubImages.erase(mSubImages.begin() + pos);
}
void MWindow::render(){
/*
@description : fill title and image into canvas in suitable way
@param :
None
@Returns :
None
*/
mCanvas.setTo(Scalar(20, 20, 20));
// get sub canvas size
int cellH = mCanvas.rows / mRows;
int cellW = mCanvas.cols / mCols;
int margin = 10;
// set total number of images to load
int numImgs = mSubImages.size() > mRows * mCols ? mRows * mCols : mSubImages.size();
for(int i = 0; i < numImgs; i++){
// get title
string title = mSubTitles[i];
// get sub canvas top left location
int cellX = (cellW) * ((i) % mCols);
int cellY = (cellH) * floor( (i) / (float) mCols);
Rect mask(cellX, cellY, cellW, cellH);
// set subcanvas size
rectangle(mCanvas, Rect(cellX, cellY, cellW, cellH), Scalar(200, 200, 200), 1);
Mat cell(mCanvas, mask);
Mat imgResz;
// get cell aspect
double cellAspect = (double) cellW / (double) cellH;
// get image
Mat img = mSubImages[i];
// get image aspect
double imgAspect = (double) img.cols / (double) img.cols;
double wAspect = (double) cellW / (double) img.cols;
double hAspect = (double) cellH / (double) img.rows;
// get suitable aspect and resize image
double aspect = cellAspect < imgAspect ? wAspect : hAspect;
resize(img, imgResz, Size(0, 0), aspect, aspect);
// if gray image, convert to BGR
if(imgResz.channels() == 1){
cvtColor(imgResz, imgResz, COLOR_GRAY2BGR);
}
Mat subCell(mCanvas, Rect(cellX, cellY, imgResz.cols, imgResz.rows));
imgResz.copyTo(subCell);
putText(cell, title, Point(20, 20), FONT_HERSHEY_SIMPLEX, 0.6, Scalar(255, 0, 0));
}
// show total canvas
imshow(mWindowTitle, mCanvas);
}
- 主函数:
/*
@File :main.cpp
@Description: :do connected component demo in a multi window
@Date :2021/12/22 15:56:53
@Author :xieyin
@version :1.0
*/
#include<iostream>
#include<string>
#include<cmath>
#include<memory>
using namespace std;
#include<opencv2/core/utility.hpp>
#include<opencv2/highgui.hpp>
#include<opencv2/imgproc.hpp>
using namespace cv;
#include"mwindow.hpp"
shared_ptr<MWindow> myWin;
Scalar randColor(RNG& rng){
/*
@description : genert randow color
@param :
rng : random number generator object
@Returns :
Sacalar() : BGR scalar
*/
auto iColor = (unsigned)rng;
return Scalar(iColor&255, (iColor >> 8)&255, (iColor >> 16)&255);
}
Mat calLigthPattern(Mat img){
/*
@description : get source image's light pattern
@param :
img : source BGR image or Gray image
@Returns :
pattern : the light pattern
*/
Mat pattern;
blur(img, pattern, Size(img.cols / 3, img.cols / 3));
return pattern;
}
Mat removeLight(Mat img, Mat pattern, int methodLight){
/*
@description : remove light between img and pattern based on method light
@param :
img : source BGR/Gray image
pattern : pattern BGR/Gray image
methodLight : choise options: 0 difference, 1 div
@Returns :
aux : light removed BGR/Gray image
*/
Mat aux;
if(methodLight == 1){
// div operation in float 32 format CV_32F
Mat img32, pattern32;
img.convertTo(img32, 5);
pattern.convertTo(pattern32, 5);
aux = 1.0 - (img32 / pattern32);
// covert to CV_8U and clip
aux.convertTo(aux, 0, 255);
}
else{
// difference
aux = pattern - img;
}
return aux;
}
void connectedComponents(Mat img_thr){
/*
@description : opencv connnected components
@param :
img : threshold image
@Returns :
None
*/
Mat labels;
auto num_objs = connectedComponents(img_thr, labels);
if(num_objs < 2){
cout << "no object is detected. " << endl;
return ;
}
Mat res = Mat::zeros(img_thr.rows, img_thr.cols, CV_8UC3);
RNG rng(0xFFFFFFFF);
for(auto i = 1; i < num_objs; i++){
Mat mask = labels == i;
res.setTo(randColor(rng), mask);
}
// imshow("result", res);
myWin -> addImage("result", res);
}
void connectedComponentsWithStats(Mat img_thr){
/*
@description : connnected components with stats
@param :
img : threshold image
@Returns :
None
*/
Mat labels, stats, centroids;
auto num_objs = connectedComponentsWithStats(img_thr, labels, stats, centroids);
if(num_objs < 2){
cout << "no object is detected. " << endl;
return ;
}
Mat res = Mat::zeros(img_thr.rows, img_thr.cols, CV_8UC3);
RNG rng(0xFFFFFFFF);
for(auto i = 1; i < num_objs; i++){
Mat mask = labels == i;
res.setTo(randColor(rng), mask);
stringstream ss;
ss << "area: " << stats.at<int>(i, CC_STAT_AREA);
// add text info
putText(res, ss.str(), centroids.at<Point2d>(i), FONT_HERSHEY_SIMPLEX, 0.3, Scalar(0, 255, 0));
}
myWin -> addImage("result", res);
}
void findContours(Mat img_thr){
/*
@description : find contours and put text
@param :
img : threshold image
@Returns :
None
*/
vector<vector<Point>> contours;
findContours(img_thr, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
if(contours.size() == 0){
cout << "no contours are found ." << endl;
return;
}
RNG rng(0xFFFFFFFF);
Mat res = Mat::zeros(img_thr.rows, img_thr.cols, CV_8UC3);
// calculate moments
vector<Moments> mu(contours.size());
for (int i = 0; i < contours.size(); i++)
{
mu[i] = moments(contours[i], false);
}
// calculate centroids
vector<Point2f> mc(contours.size());
for (int i = 0; i < contours.size(); i++)
{
mc[i] = Point2d(mu[i].m10 / mu[i].m00, mu[i].m01 / mu[i].m00);
}
for(auto i = 0; i < contours.size(); i++){
drawContours(res, contours, i, randColor(rng));
putText(res, "*", Point(mc[i].x, mc[i].y), FONT_HERSHEY_SIMPLEX, 0.4, Scalar(255, 0, 255), 1);
}
myWin -> addImage("result", res);
}
const char* keys = {
"{help h usage ? | | print this message}"
"{@image | | Image to process}"
"{@lightPattern | | Image light pattern applied to image}"
"{lightMethod | 1 | Method to remove image background, 0 diff, 1 div, 2 no light removal}"
"{segMethod | 2 | Method to segment: 1 connected components, 2 connected components with states, 3 find contours}"
};
int main(int argc, const char** argv){
CommandLineParser parser(argc, argv, keys);
if(parser.has("help")){
parser.printMessage();
return 0;
}
// load source img and light pattern img, set light and segment method
String imgFile = parser.get<String>(0);
String lightPat = parser.get<String>(1);
auto methodLight = parser.get<int>("lightMethod");
auto methodSeg = parser.get<int>("segMethod");
// check commoand format
if(!parser.check()){
parser.printErrors();
return 0;
}
// read source image
Mat img = imread(imgFile, 0);
if(img.data == NULL){
cout << "Error load image file . " << endl;
return 0;
}
myWin = make_shared<MWindow>("Main Window", 2, 3, WINDOW_AUTOSIZE);
// create denoise image and box_smooth img
Mat img_noise, img_box_smooth;
medianBlur(img, img_noise, 3);
blur(img, img_box_smooth, Size(3, 3));
// read light image
Mat imgLightPat = imread(lightPat, 0);
if(imgLightPat.data == NULL){
// use blur with large kernel to get light pattern
imgLightPat = calLigthPattern(img_noise);
}
medianBlur(imgLightPat, imgLightPat, 3);
// for image without light pattern
Mat img_no_light;
img_noise.copyTo(img_no_light);
if(methodLight != 2){
img_no_light = removeLight(img_noise, imgLightPat, methodLight);
}
Mat img_thread;
if(methodLight != 2){
threshold(img_no_light, img_thread, 30, 255, THRESH_BINARY);
}else{
threshold(img_no_light, img_thread, 140, 255, THRESH_BINARY_INV);
}
// show image
myWin->addImage("Input", img);
myWin->addImage("Input without noise", img_noise);
myWin->addImage("light pattern", imgLightPat);
myWin->addImage("no light", img_no_light);
myWin->addImage("thread", img_thread);
// add result image
switch (methodSeg){
case 1:
connectedComponents(img_thread);
break;
case 2:
connectedComponentsWithStats(img_thread);
break;
case 3:
findContours(img_thread);
default:
break;
}
myWin->render();
waitKey(0);
return 0;
}
结果展示:
./main -h #命令行查看使用方法
./main ../data/test.pgm -lightMethod=2 -segMethod=2 # 无需背景图片输入
./main ../data/test.pgm ../data/light.pgm -lightMethod=2 -segMethod=2 # 一张原始图像和一张背景图像进行分割
./main ../data/test.pgm ../data/light.pgm -lightMethod=0 -segMethod=3 # 根据原始图像和背景图像进行轮廓和质心提取