[opencv][cpp] 学习手册3:案例泛红填充,分水岭
- 感觉学习效率有些低,跟着视频学习代码其实不是明智之举。
- 练习代码还是应该回归本质:思路+代码(函数+POP/OOP)+调试。
- 练习时需要结合 opencv doc。随时翻阅。
16_泛洪填充.cpp
17_图像分水岭.cpp
文章原理、概念部分摘抄自课件,侵删!
16_泛洪填充.cpp
原理+概念
- 泛洪填充算法又称洪水填充算法,最熟悉不过就是 windows paint的油漆桶功能
- 算法的原理:
- 从一个点开始,将附近像素点填充成新的颜色,直到封闭区域内的所有像素点都被填充新颜色为止。
- 常见泛红填充:
- 四邻域像素填充法,
- 八邻域像素填充法,
- 基于扫描线的像素填充方法
cv函数
联通区域由邻近像素的颜色亮度接进性决定。
点(x,y)属于上色区域,如果(x’,y’) 已知的属于联通区域的点,满足公式
已知点的亮度/颜色值 - 最大低亮度差 <= 当前点的亮度/颜色值 <= 已知点的亮度/颜色 + 最大高亮度差
代码
//
// Created by jacob on 12/31/20.
//
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/utils/logger.hpp>
using namespace std;
namespace cvlog = cv::utils::logging;
/***-------------------- global variable --------------------***/
cv::Mat src;
cv::RNG rng(12345);
/***-------------------- callback function --------------------***/
/*
* use ctrl + mouseLeft to check function parameters
* aim: display points on img where mouse clicked
*/
void onMouseClick(int event, int x, int y, int flags, void *userdata) {
// CV_LOG_INFO(NULL, "hello") // continuous logging
if (event == cv::EVENT_LBUTTONDOWN) {
cv::Point seedPoint(x, y);
CV_LOG_INFO(MOUSE, "mPt(x, y): " << seedPoint.x << ", " << seedPoint.y)
cv::Rect rect;
cv::Scalar rand_scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
cv::Scalar loDiff(20, 20, 20);
cv::Scalar upDiff(60, 60, 60);
// do flood filling
cv::floodFill(src, seedPoint, rand_scalar, &rect, loDiff, upDiff, cv::FLOODFILL_FIXED_RANGE);
// dispaly image
cv::imshow("src", src);
}
}
/***-------------------- main function --------------------***/
int main(int argc, char **argv) {
cvlog::setLogLevel(cvlog::LOG_LEVEL_INFO);
CV_LOG_INFO(NULL, "16_floodFill.cpp")
// 1. read src
string filename = "../img/zp.jpg";
src = cv::imread(filename, cv::IMREAD_COLOR);
CV_LOG_INFO(NULL, "src.size: " << src.size() << "src.type: " << src.type())
cv::imshow("src", src);
// test mouse callback
cv::setMouseCallback("src", onMouseClick, 0);
// wait & terminate
cv::waitKey(0);
return 0;
}
解释说明
- 案例使用鼠标点击功能,涉及到两个函数
- cv::setMouseCallback(“src”, onMouseClick, 0);
- void onMouseClick(int event, int x, int y, int flags, void *userdata);
其中,onMouseClick 回调函数的参数可以在 IDE 中通过 ctrl + 鼠标左键点击 setMouseCallback 的第二个函数类型获得。
在此案例中,由于需要在 onMouseClick 函数中显示对图像 src 处理过的结果,但是,定义在主函数中的 src 不在 onMouseClick 函数作用范围,需要将 src 定义成外部变量(C语言的外部变量)。
- 使用 cv::RNG 生成填充颜色的随机数
这里使用 uniform 生成均匀分布随机数。也可以生成高斯分布等随机数。cv::RNG rng(12345); cv::Scalar rand_scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255));
- onMouseClick 回调函数会不停的接收信号
会不停的输出 hello。void onMouseClick(int event, int x, int y, int flags, void *userdata) { CV_LOG_INFO(NULL, "hello") // continuous logging }
运行结果
17_图像分水岭.cpp
原理+概念
- 任何灰度图像都可以看做是地形表面,把高强度看做山峰和丘陵,低强度看做山谷。
- 分水岭图像分割步骤+注意事项:
- 用不同显色的水(label)充满每个单独的山谷(局部最小值)
- 随着水位的上升,根据山谷附近的山峰高度(梯度),那些来自不同山谷的不同颜色的水开始合并
- 要通过在这些合并的位置建立阻隔,来避免合并的发生。
- 不断地加水,并修建阻隔,直到所有的山峰被水覆盖。如此,我们创建的阻隔所分割出的结果即是分水岭。
- OpenCV提供了基于marker的分水岭算法,需要将确定的背景和前景标记出来。
- 参考:The Watershed Transformation
- 通过标记控制的分水岭 marker-controlled watershed可以防止过度细分 over-segmentation。
cv函数
使用正整数标记区域(1, 2, 3,…),因此每一个意向的联通区域由像素值1,2,3,… 等表示。
输出矩阵:每一个不同的区域由对应的正整数标记填充(1,2,3,… 来填充三个区域),区域之间的边界由 -1 填充
代码1:熟悉函数
- 目的:
设计一段代码学习cv::waterShed 函数- 实现
创建四个cv::Mat 矩阵
- src:原始图像矩阵,CV_8UC3
- markers:注水区域矩阵,尺寸同src,CV_32S
- markers_show:初始为 src 的拷贝,显示注水区域,CV_8UC3
- wshed:显示分水岭图像矩阵
//
// Created by jacob on 12/31/20.
//
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/utils/logger.hpp>
using namespace std;
namespace cvlog = cv::utils::logging;
/***-------------------- global variable --------------------***/
cv::Mat src, markers, markers_show;
cv::RNG rng(12345);
int n = 1;
/***-------------------- main function --------------------***/
int main(int argc, char **argv) {
cvlog::setLogLevel(cvlog::LOG_LEVEL_INFO);
CV_LOG_INFO(NULL, "17_watershed_test.cpp")
// 1. read src
string filename = "../img/waternet7.png";
src = cv::imread(filename, cv::IMREAD_COLOR);
CV_LOG_INFO(NULL, "src.size: " << src.size() << ", src.type: " << src.type())
cv::imshow("src", src);
// do waterShed,实现分水岭
src.copyTo(markers_show);
markers = cv::Mat::zeros(src.size(), CV_32S); // (265, 201), (75, 110)
int x1 = 70;
int y1 = 50;
int x2 = 130;
int y2 = 10;
int r = 10;
cv::circle(markers, cv::Point(x1, y1), r, cv::Scalar(n++), -1);
cv::circle(markers, cv::Point(x2, y2), r, cv::Scalar(n++), -1);
cv::imwrite("../img/markers.png", markers);
cv::circle(markers_show, cv::Point(x1, y1), r, cv::Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), -1);
cv::circle(markers_show, cv::Point(x2, y2), r, cv::Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), -1);
cv::imshow("marker_show", markers_show);
CV_LOG_INFO(NULL, "markers before: \n" << markers)
cv::watershed(src, markers);
CV_LOG_INFO(NULL, "markers after: \n" << markers)
// 定义随机颜色列表,三个区域
vector<cv::Vec3b> colors = {
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
};
cv::Mat wshed = cv::Mat::zeros(src.size(), src.type());
for (int row = 0; row < markers.rows; ++row) {
for (int col = 0; col < markers.cols; ++col) {
// 获取当前位置的标记
int label = markers.at<int>(row, col);
if (label > 0 && label < n) { // 1,2
// CV_LOG_INFO(NULL, "label: " << label)
// CV_LOG_INFO(NULL, "color: " << colors[label])
wshed.at<cv::Vec3b>(row, col) = colors[label];
}
}
}
cv::imshow("wshed", wshed);
// wait & terminate
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
解释说明
- 在标记注水点矩阵 markers 上手动标记两个注水区域(1,2),并用圆形标记区域
cv::circle(markers, cv::Point(x1, y1), r, cv::Scalar(n++), -1); cv::circle(markers, cv::Point(x2, y2), r, cv::Scalar(n++), -1);
- 在显示注水点矩阵 markers_show 上绘制注水点
cv::circle(markers_show, cv::Point(x1, y1), r, cv::Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), -1); cv::circle(markers_show, cv::Point(x2, y2), r, cv::Scalar(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), -1);
- 定义两个区域的随机颜色,并使用 for 循环进行颜色填充
vector<cv::Vec3b> colors = { cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)), };
运行结果
进行分水岭函数运算之前的 marker
进行分水岭函数运算之前的 marker
代码2:案例
目的:
- 实现代码1中所有功能
- 引入 cv 的鼠标操作
- 鼠标左键按下用来标记注水点
- 鼠标右键按下用来执行分水岭函数
//
// Created by jacob on 12/31/20.
//
#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/core/utils/logger.hpp>
using namespace std;
namespace cvlog = cv::utils::logging;
/***-------------------- global variable --------------------***/
cv::Mat src, markers, markers_show;
cv::RNG rng(12345);
int n = 1; // store max_no. of region
vector<cv::Vec3b> colors;
/***-------------------- callback function --------------------***/
/*
* use ctrl + mouseLeft to check function parameters
* aim: display points on img where mouse clicked
*/
void onMouseClick(int event, int x, int y, int flags, void *userdata) {
vector<cv::Vec3b> color = *(vector<cv::Vec3b> *)userdata;
// 当用户点击鼠标左键的时候,打标记
if (event == cv::EVENT_LBUTTONDOWN) {
int label = n;
cv::Point seedPoint(x, y);
CV_LOG_INFO(MOUSE, "mPt(x, y): " << seedPoint.x << ", " << seedPoint.y)
circle(markers, cv::Point(x, y), 10, cv::Scalar(label), -1);
circle(markers_show, cv::Point(x, y), 10, colors[label], -1);
cv::imshow("markers_show", markers_show);
n++;
}
// 当用户点击鼠标右键的时候,执行分水岭算法
if (event == cv::EVENT_RBUTTONDOWN) {
// do watershed
CV_LOG_INFO(NULL, "markers before: \n" << markers)
cv::watershed(src, markers);
CV_LOG_INFO(NULL, "markers after: \n" << markers)
cv::Mat wShed = cv::Mat::zeros(src.size(), src.type());
for (int row = 0; row < markers.rows; ++row) {
for (int col = 0; col < markers.cols; ++col) {
// 获取当前位置的标记
int label = markers.at<int>(row, col);
if (label > 0 && label < n) {
wShed.at<cv::Vec3b>(row, col) = colors[label];
}
}
}
cv::imshow("wShed", wShed);
}
}
/***-------------------- main function --------------------***/
int main(int argc, char **argv) {
cvlog::setLogLevel(cvlog::LOG_LEVEL_INFO);
CV_LOG_INFO(NULL, "17_watershed.cpp")
// 1. read src
string filename = "../img/wan.png";
src = cv::imread(filename, cv::IMREAD_COLOR);
CV_LOG_INFO(NULL, "src.size: " << src.size() << "src.type: " << src.type())
cv::imshow("src", src);
// test mouse callback
markers = cv::Mat(src.size(), CV_32S);
src.copyTo(markers_show);
colors = {
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
};
cv::setMouseCallback("src", onMouseClick, &colors);
// wait & terminate
cv::waitKey(0);
return 0;
}
运行结果
传值给回调函数
{
colors = {
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
cv::Vec3b(rng.uniform(0, 255), rng.uniform(0, 255), rng.uniform(0, 255)),
};
cv::setMouseCallback("src", onMouseClick, &colors);
}
void onMouseClick(int event, int x, int y, int flags, void *userdata) {
vector<cv::Vec3b> color = *(vector<cv::Vec3b> *) userdata;
}
opencv sample tutorial 的代码写的很好,应该学习。
&&_参考
链接:C语言的外部变量
链接:The Watershed Transformation
链接:Clion 添加命令行参数并调试运行