数药片
通过图像二值化、轮廓检测等方法来识别药片个数。假设药片在图像中与背景有明显的颜色或灰度差异,且相互之间有一定的间隔。

bash
#include <QCoreApplication>
#include <QDebug>
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char** argv) {
// 读取图像
Mat image = imread("1.png");
if (image.empty()) {
cout << "Could not open or find the image" << endl;
return -1;
}
// 转换为灰度图像
Mat gray;
cvtColor(image, gray, COLOR_BGR2GRAY);
// 图像平滑处理,减少噪声影响
GaussianBlur(gray, gray, Size(5, 5), 0);
// 二值化图像
Mat binary;
threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);
// 寻找轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
// 过滤掉小的轮廓,只保留较大的(假设药片轮廓相对较大)
vector<vector<Point>> validContours;
for (size_t i = 0; i < contours.size(); i++) {
if (contourArea(contours[i]) > 100) {
validContours.push_back(contours[i]);
}
}
// 绘制轮廓
Mat drawing = Mat::zeros(binary.size(), CV_8UC3);
for (size_t i = 0; i < validContours.size(); i++) {
Scalar color = Scalar(0, 255, 0);
drawContours(drawing, validContours, (int)i, color, 2, LINE_8, hierarchy, 0);
}
// 输出药片个数
cout << "药片个数: " << validContours.size() << endl;
// 显示结果
imshow("Original Image", image);
imshow("Binary Image", binary);
imshow("Contours", drawing);
waitKey(0);
return 0;
}
读取图像:使用imread函数读取包含药片的图像。如果图像读取失败,输出错误信息并退出程序。
灰度转换:将彩色图像转换为灰度图像,简化后续处理。
图像平滑:通过GaussianBlur函数对灰度图像进行高斯平滑处理,减少噪声对后续处理的影响。
二值化:利用threshold函数结合THRESH_BINARY_INV + THRESH_OTSU方法自动确定阈值并进行二值化操作,使药片在图像中呈现为白色,背景为黑色。
轮廓检测:使用findContours函数查找图像中的轮廓。RETR_TREE表示轮廓检索模式为树形结构,CHAIN_APPROX_SIMPLE表示轮廓近似方法为保留端点。
轮廓过滤:遍历所有轮廓,通过contourArea函数计算轮廓面积,只保留面积大于 100 的轮廓,以过滤掉小的噪声轮廓。
绘制轮廓:创建一个黑色背景的图像drawing,使用drawContours函数在该图像上绘制经过过滤的有效轮廓。
输出与显示:输出检测到的药片个数,并使用imshow函数显示原始图像、二值化图像和绘制轮廓后的图像,waitKey(0)等待按键输入,保持图像窗口显示。
添加水印

bash
#include <QCoreApplication>
#include <QDebug>
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char** argv) {
// 读取图像
Mat image = imread("1.png");
if (image.empty()) {
cout << "Could not open or find the image" << endl;
return -1;
}
// 转换为灰度图像
Mat gray;
cvtColor(image, gray, COLOR_BGR2GRAY);
// 图像平滑处理,减少噪声影响
GaussianBlur(gray, gray, Size(5, 5), 0);
// 二值化图像
Mat binary;
threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);
// 寻找轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
// 过滤掉小的轮廓,只保留较大的(假设药片轮廓相对较大)
vector<vector<Point>> validContours;
for (size_t i = 0; i < contours.size(); i++) {
if (contourArea(contours[i]) > 100) {
validContours.push_back(contours[i]);
}
}
// 在原图上绘制轮廓并添加编号
for (size_t i = 0; i < validContours.size(); i++) {
Scalar color = Scalar(0, 255, 0);
drawContours(image, validContours, (int)i, color, 2, LINE_8, hierarchy, 0);
// 计算轮廓中心
Moments m = moments(validContours[i]);
if (m.m00 != 0) {
int cX = static_cast<int>(m.m10 / m.m00);
int cY = static_cast<int>(m.m01 / m.m00);
// 添加编号
string label = to_string(i + 1);
putText(image, label, Point(cX, cY), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(255, 255, 255), 2);
}
}
// 输出药片个数
cout << "药片个数: " << validContours.size() << endl;
// 显示结果
imshow("Original Image with Labels", image);
imshow("Binary Image", binary);
waitKey(0);
return 0;
}
读取图像:数据结构)。如果图像读取失败,image.empty() 函数会返回 true,此时程序输出错误信息并返回 -1 表示程序异常结束。
转换为灰度图像:cvtColor 函数将彩色图像 image(通常是 BGR 格式)转换为灰度图像 gray。颜色转换代码 COLOR_BGR2GRAY 表示将 BGR 格式转换为灰度格式。灰度图像在后续处理中计算量较小,并且对于一些基于灰度信息的图像处理算法更为适用。
图像平滑处理:
GaussianBlur 函数对灰度图像 gray 进行高斯模糊处理。高斯模糊通过对图像中的每个像素点与高斯核进行卷积运算,来平滑图像,减少噪声影响。Size(5, 5) 表示高斯核的大小为 5x5,最后一个参数 0 表示根据高斯核大小自动计算标准差。
二值化图像:
threshold 函数对灰度图像 gray 进行二值化处理,将其转换为二值图像 binary。0 和 255 分别表示二值化的下限和上限。THRESH_BINARY_INV + THRESH_OTSU 是两个标志的组合:
THRESH_OTSU 表示使用 Otsu 算法自动确定阈值。Otsu 算法通过分析图像的灰度直方图,找到一个阈值,使得前景和背景之间的类间方差最大。
THRESH_BINARY_INV 表示二值化的方式为反转,即大于阈值的像素设置为 0(黑色),小于阈值的像素设置为 255(白色)。
寻找轮廓:
findContours 函数用于在二值图像 binary 中查找轮廓。contours 是一个 vector,用于存储找到的所有轮廓,每个轮廓是一个 Point 类型的 vector,表示轮廓上的点。hierarchy 是一个 Vec4i 类型的 vector,用于存储轮廓之间的层次关系。RETR_TREE 表示轮廓检索模式为树形结构,它会返回所有的轮廓,并建立轮廓之间的父子关系。CHAIN_APPROX_SIMPLE 表示轮廓近似方法,它会压缩水平、垂直和对角方向的轮廓,只保留端点,从而减少轮廓点的数量。
过滤掉小的轮廓:
这段代码遍历所有找到的轮廓 contours,通过 contourArea 函数计算每个轮廓的面积。如果轮廓面积大于 100,则认为它是一个有效的药片轮廓,将其添加到 validContours 中。这一步是为了过滤掉一些可能由噪声或小的干扰物体产生的小轮廓。
查找和绘制轮廓,并计算面积


bash
#include <QCoreApplication>
#include <QDebug>
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
int main(int argc, char** argv) {
// 读取图像
Mat image = imread("2.png");
if (image.empty()) {
cout << "Could not open or find the image" << endl;
return -1;
}
// 转换为灰度图像
Mat gray;
cvtColor(image, gray, COLOR_BGR2GRAY);
// 图像平滑处理,减少噪声影响
GaussianBlur(gray, gray, Size(5, 5), 0);
// 二值化图像
Mat binary;
threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);
// 寻找轮廓
vector<vector<Point>> contours;
vector<Vec4i> hierarchy;
findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);
// 创建一个用于绘制轮廓的图像
Mat drawing = Mat::zeros(image.size(), CV_8UC3);
// 遍历所有轮廓,绘制轮廓并计算面积
for (size_t i = 0; i < contours.size(); i++) {
// 绘制轮廓
Scalar color = Scalar(0, 255, 0);
drawContours(drawing, contours, (int)i, color, 2, LINE_8, hierarchy, 0);
// 计算轮廓面积
double area = contourArea(contours[i]);
cout << "Contour " << i << " area: " << area << endl;
}
// 显示结果
imshow("Original Image", image);
imshow("Binary Image", binary);
imshow("Contours", drawing);
waitKey(0);
return 0;
}
读取图像:
Mat image = imread("2.png"); 使用 OpenCV 的 imread 函数从指定路径读取图像,并将其存储为 Mat 对象(OpenCV 中用于表示图像的数据结构)。这里尝试读取路径为 2.png 的图像。
if (image.empty()) {... } 检查图像是否成功读取。如果 image.empty() 返回 true,表示图像读取失败,程序会在控制台输出 Could not open or find the image,然后返回 -1 以表示程序异常结束。
转换为灰度图像:
Mat gray; 声明一个 Mat 对象 gray,用于存储灰度图像。
cvtColor(image, gray, COLOR_BGR2GRAY); 使用 cvtColor 函数将彩色图像 image(通常是 BGR 格式)转换为灰度图像 gray。COLOR_BGR2GRAY 是颜色转换代码,指定将 BGR 格式转换为灰度格式。灰度图像在后续处理中计算量相对较小,并且许多基于灰度信息的图像处理算法更适用于灰度图像。
图像平滑处理:
GaussianBlur(gray, gray, Size(5, 5), 0); 使用 GaussianBlur 函数对灰度图像 gray 进行高斯模糊处理。高斯模糊通过对图像中的每个像素点与高斯核进行卷积运算,来平滑图像,减少噪声影响。Size(5, 5) 表示高斯核的大小为 5x5,最后一个参数 0 表示根据高斯核大小自动计算标准差。经过平滑处理后,图像中的噪声点会被模糊,使得后续的轮廓查找更加准确。
二值化图像:
Mat binary; 声明一个 Mat 对象 binary,用于存储二值化后的图像。
threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU); 使用 threshold 函数对灰度图像 gray 进行二值化处理,将其转换为二值图像 binary。0 和 255 分别表示二值化的下限和上限。THRESH_BINARY_INV + THRESH_OTSU 是两个标志的组合:
THRESH_OTSU 表示使用 Otsu 算法自动确定阈值。Otsu 算法通过分析图像的灰度直方图,找到一个阈值,使得前景和背景之间的类间方差最大。
THRESH_BINARY_INV 表示二值化的方式为反转,即大于阈值的像素设置为 0(黑色),小于阈值的像素设置为 255(白色)。通过二值化,图像中的物体(前景)和背景被清晰地分离,便于后续的轮廓查找。
寻找轮廓:
vector<vector> contours; 声明一个二维 vector contours,用于存储找到的所有轮廓。每个轮廓是一个 Point 类型的 vector,其中 Point 表示轮廓上的点。
vector hierarchy; 声明一个 Vec4i 类型的 vector hierarchy,用于存储轮廓之间的层次关系。Vec4i 是一个包含 4 个整数的向量,分别表示当前轮廓的后一个轮廓、前一个轮廓、子轮廓和父轮廓的索引。
findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE); 使用 findContours 函数在二值图像 binary 中查找轮廓。RETR_TREE 表示轮廓检索模式为树形结构,这种模式会返回所有的轮廓,并建立轮廓之间的父子关系,适用于需要了解轮廓层次结构的场景。CHAIN_APPROX_SIMPLE 表示轮廓近似方法,它会压缩水平、垂直和对角方向的轮廓,只保留端点,从而减少轮廓点的数量,提高处理效率。
创建绘制轮廓的图像:
Mat drawing = Mat::zeros(image.size(), CV_8UC3); 创建一个与原始图像 image 大小相同的黑色图像 drawing,其数据类型为 CV_8UC3,表示 8 位无符号整数,3 通道(即彩色图像,可用于绘制彩色轮廓)。这个图像将用于绘制找到的轮廓。
遍历轮廓并绘制和计算面积:
for (size_t i = 0; i < contours.size(); i++) {... } 遍历所有找到的轮廓。
Scalar color = Scalar(0, 255, 0); 定义一个颜色 color,这里设置为绿色(在 OpenCV 中,颜色通常以 BGR 顺序表示,Scalar(0, 255, 0) 表示绿色),用于绘制轮廓。
drawContours(drawing, contours, (int)i, color, 2, LINE_8, hierarchy, 0); 使用 drawContours 函数在 drawing 图像上绘制第 i 个轮廓。参数依次为:目标图像 drawing,轮廓数组 contours,要绘制的轮廓索引 (int)i,轮廓颜色 color,轮廓线的粗细 2,线型 LINE_8(表示 8 邻域连接的线条),轮廓层次结构 hierarchy,轮廓绘制的偏移量 0(这里未使用)。
double area = contourArea(contours[i]); 使用 contourArea 函数计算第 i 个轮廓的面积,并将结果存储在 area 变量中。
cout << "Contour " << i << " area: " << area << endl; 在控制台输出第 i 个轮廓的面积。
利用凸缺陷检测来识别手势

bash
#include <QCoreApplication>
#include <QDebug>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace cv;
using namespace std;
// 手势类型枚举
enum GestureType {
UNKNOWN,
FIST,
ONE_FINGER,
TWO_FINGER,
THREE_FINGER,
FOUR_FINGER,
FIVE_FINGER,
OK_SIGN,
THUMB_UP,
PEACE_SIGN
};
class HandGestureRecognizer {
private:
Mat image;
Mat gray;
Mat binary;
vector<vector<Point>> contours;
vector<Point> handContour;
vector<Point> hull;
vector<Vec4i> defects;
vector<int> hullIndices;
// 参数
int blurSize = 5;
int minContourArea = 5000;
double defectDepthThreshold = 20.0;
double fingerAngleThreshold = 80.0; // 角度阈值,用于筛选手指
public:
HandGestureRecognizer() {}
// 设置参数
void setParameters(int blur, int minArea, double depthThresh, double angleThresh) {
blurSize = blur;
minContourArea = minArea;
defectDepthThreshold = depthThresh;
fingerAngleThreshold = angleThresh;
}
// 预处理图像
bool preprocess(const Mat& input) {
image = input.clone();
// 转换为灰度图
cvtColor(image, gray, COLOR_BGR2GRAY);
// 高斯模糊
GaussianBlur(gray, gray, Size(blurSize, blurSize), 0);
// 自适应阈值或Otsu阈值
// adaptiveThreshold(gray, binary, 255, ADAPTIVE_THRESH_GAUSSIAN_C,
// THRESH_BINARY_INV, 11, 2);
// Otsu阈值
threshold(gray, binary, 0, 255, THRESH_BINARY_INV + THRESH_OTSU);
// 形态学操作(可选)
Mat kernel = getStructuringElement(MORPH_ELLIPSE, Size(5, 5));
morphologyEx(binary, binary, MORPH_CLOSE, kernel);
morphologyEx(binary, binary, MORPH_OPEN, kernel);
return true;
}
// 检测手部轮廓
bool detectHandContour() {
contours.clear();
vector<Vec4i> hierarchy;
// 查找轮廓
findContours(binary, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
if (contours.empty()) {
cout << "未检测到轮廓" << endl;
return false;
}
// 找到最大轮廓(假设是手)
int maxIdx = -1;
double maxArea = 0;
for (size_t i = 0; i < contours.size(); i++) {
double area = contourArea(contours[i]);
if (area > maxArea && area > minContourArea) {
maxArea = area;
maxIdx = i;
}
}
if (maxIdx == -1) {
cout << "未找到有效的手部轮廓" << endl;
return false;
}
handContour = contours[maxIdx];
// 简化轮廓(可选)
vector<Point> approx;
double epsilon = 0.002 * arcLength(handContour, true);
approxPolyDP(handContour, approx, epsilon, true);
handContour = approx;
return true;
}
// 计算凸包和凸缺陷
bool computeConvexFeatures() {
if (handContour.size() < 10) {
cout << "轮廓点太少,无法计算凸包" << endl;
return false;
}
// 计算凸包
hullIndices.clear();
convexHull(handContour, hullIndices, false, false);
// 获取凸包点
hull.clear();
for (int idx : hullIndices) {
hull.push_back(handContour[idx]);
}
// 计算凸缺陷
defects.clear();
if (handContour.size() > 3 && hullIndices.size() > 3) {
convexityDefects(handContour, hullIndices, defects);
}
return true;
}
// 计算两点间距离
double distanceBetweenPoints(Point p1, Point p2) {
return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
}
// 计算角度(余弦定理)
double calculateAngle(Point a, Point b, Point c) {
double ab = distanceBetweenPoints(a, b);
double bc = distanceBetweenPoints(b, c);
double ac = distanceBetweenPoints(a, c);
if (ab == 0 || bc == 0) return 180.0;
double angle = acos((ab * ab + bc * bc - ac * ac) / (2 * ab * bc));
return angle * 180 / CV_PI;
}
// 识别手指
vector<Point> detectFingers() {
vector<Point> fingerTips;
if (defects.empty() || hull.empty()) {
return fingerTips;
}
// 找到手腕点(假设在底部)
Point wristPoint = findWristPoint();
// 筛选有效的凸缺陷(手指间凹陷)
vector<Vec4i> validDefects;
for (const Vec4i& defect : defects) {
int startIdx = defect[0];
int endIdx = defect[1];
int farIdx = defect[2];
int depth = defect[3] / 256;
Point startPt = handContour[startIdx];
Point endPt = handContour[endIdx];
Point farPt = handContour[farIdx];
// 计算角度,筛选手指
double angle = calculateAngle(startPt, farPt, endPt);
// 筛选条件:深度足够,角度合理
if (depth > defectDepthThreshold && angle < fingerAngleThreshold) {
validDefects.push_back(defect);
// 手指尖通常是startPt或endPt中离手腕更远的点
double dist1 = distanceBetweenPoints(startPt, wristPoint);
double dist2 = distanceBetweenPoints(endPt, wristPoint);
if (dist1 > dist2) {
fingerTips.push_back(startPt);
} else {
fingerTips.push_back(endPt);
}
}
}
// 去重和排序
// fingerTips = removeClosePoints(fingerTips, 30);
// sortFingerTips(fingerTips);
return fingerTips;
}
// 找到手腕点(简单实现)
Point findWristPoint() {
if (handContour.empty()) return Point(0, 0);
// 找到轮廓的最低点作为手腕(假设手在图像底部)
Point wrist(0, 0);
for (const Point& p : handContour) {
if (p.y > wrist.y) {
wrist = p;
}
}
// 或者使用凸包的最低点
for (const Point& p : hull) {
if (p.y > wrist.y) {
wrist = p;
}
}
return wrist;
}
// 移除过于接近的点
vector<Point> removeClosePoints(const vector<Point>& points, double minDist) {
vector<Point> result;
vector<bool> keep(points.size(), true);
for (size_t i = 0; i < points.size(); i++) {
if (!keep[i]) continue;
for (size_t j = i + 1; j < points.size(); j++) {
if (distanceBetweenPoints(points[i], points[j]) < minDist) {
keep[j] = false;
}
}
result.push_back(points[i]);
}
return result;
}
// 按顺时针方向排序手指尖
void sortFingerTips(vector<Point>& fingerTips) {
if (fingerTips.empty()) return;
// 计算中心点
Point center(0, 0);
for (const Point& p : fingerTips) {
center.x += p.x;
center.y += p.y;
}
center.x /= fingerTips.size();
center.y /= fingerTips.size();
// 按角度排序
sort(fingerTips.begin(), fingerTips.end(),
[center](const Point& a, const Point& b) {
double angleA = atan2(a.y - center.y, a.x - center.x);
double angleB = atan2(b.y - center.y, b.x - center.x);
return angleA < angleB;
});
}
// 识别手势
GestureType recognizeGesture() {
if (!detectHandContour()) {
return UNKNOWN;
}
if (!computeConvexFeatures()) {
return UNKNOWN;
}
vector<Point> fingerTips = detectFingers();
int fingerCount = fingerTips.size();
// 根据手指数量判断手势
switch (fingerCount) {
case 0:
return FIST;
case 1:
return ONE_FINGER;
case 2:
// 检查是否是胜利手势(V字)
if (isPeaceSign(fingerTips)) {
return PEACE_SIGN;
}
return TWO_FINGER;
case 3:
return THREE_FINGER;
case 4:
return FOUR_FINGER;
case 5:
return FIVE_FINGER;
default:
return UNKNOWN;
}
}
// 检查是否是胜利手势
bool isPeaceSign(const vector<Point>& fingerTips) {
if (fingerTips.size() != 2) return false;
// 计算两个手指尖的角度
Point wrist = findWristPoint();
Point p1 = fingerTips[0];
Point p2 = fingerTips[1];
// 计算向量
double angle = atan2(p2.y - p1.y, p2.x - p1.x) * 180 / CV_PI;
angle = abs(angle);
// 胜利手势通常手指分开较大角度
return (angle > 30 && angle < 150);
}
// 获取结果图像
Mat getResultImage() {
Mat result = image.clone();
if (handContour.empty()) {
return result;
}
// 绘制轮廓
drawContours(result, vector<vector<Point>>{handContour}, 0,
Scalar(0, 255, 0), 2);
// 绘制凸包
if (!hull.empty()) {
vector<vector<Point>> hulls = {hull};
drawContours(result, hulls, 0, Scalar(255, 0, 0), 2);
}
// 检测手指并绘制
vector<Point> fingerTips = detectFingers();
// 绘制手指尖
for (const Point& tip : fingerTips) {
circle(result, tip, 8, Scalar(0, 0, 255), -1);
circle(result, tip, 10, Scalar(255, 255, 255), 2);
}
// 绘制凸缺陷点
for (const Vec4i& defect : defects) {
int startIdx = defect[0];
int endIdx = defect[1];
int farIdx = defect[2];
int depth = defect[3] / 256;
Point startPt = handContour[startIdx];
Point endPt = handContour[endIdx];
Point farPt = handContour[farIdx];
if (depth > defectDepthThreshold) {
// 绘制缺陷点
circle(result, farPt, 6, Scalar(255, 0, 255), -1);
line(result, startPt, farPt, Scalar(200, 200, 0), 2);
line(result, endPt, farPt, Scalar(200, 200, 0), 2);
}
}
// 绘制手腕点
Point wrist = findWristPoint();
circle(result, wrist, 10, Scalar(255, 255, 0), -1);
// 添加文本信息
GestureType gesture = recognizeGesture();
string gestureText = getGestureText(gesture);
putText(result, "Gesture: " + gestureText, Point(10, 30),
FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 0, 255), 2);
putText(result, "Fingers: " + to_string(fingerTips.size()),
Point(10, 70), FONT_HERSHEY_SIMPLEX, 1, Scalar(0, 255, 0), 2);
return result;
}
// 获取手势文本
string getGestureText(GestureType gesture) {
switch (gesture) {
case FIST: return "Fist";
case ONE_FINGER: return "One Finger";
case TWO_FINGER: return "Two Fingers";
case THREE_FINGER: return "Three Fingers";
case FOUR_FINGER: return "Four Fingers";
case FIVE_FINGER: return "Five Fingers";
case PEACE_SIGN: return "Peace Sign";
case OK_SIGN: return "OK Sign";
case THUMB_UP: return "Thumb Up";
default: return "Unknown";
}
}
// 显示所有处理步骤
void displayAllSteps() {
if (!image.empty()) imshow("1. Original Image", image);
if (!gray.empty()) imshow("2. Gray Image", gray);
if (!binary.empty()) imshow("3. Binary Image", binary);
Mat contourImg = Mat::zeros(image.size(), CV_8UC3);
if (!handContour.empty()) {
drawContours(contourImg, vector<vector<Point>>{handContour},
0, Scalar(0, 255, 0), 2);
imshow("4. Hand Contour", contourImg);
}
Mat result = getResultImage();
if (!result.empty()) {
imshow("5. Final Result", result);
}
}
};
// 主函数
int main(int argc, char** argv) {
// 读取图像
string imagePath = "5.png";
Mat image = imread(imagePath);
if (image.empty()) {
cout << "无法加载图像: " << imagePath << endl;
cout << "请使用摄像头捕获..." << endl;
// 尝试使用摄像头
VideoCapture cap(0);
if (!cap.isOpened()) {
cout << "无法打开摄像头" << endl;
return -1;
}
cout << "按空格键捕获图像,ESC键退出" << endl;
HandGestureRecognizer recognizer;
recognizer.setParameters(5, 5000, 20.0, 80.0);
while (true) {
Mat frame;
cap >> frame;
if (frame.empty()) break;
// 预处理
recognizer.preprocess(frame);
// 识别手势
GestureType gesture = recognizer.recognizeGesture();
// 获取结果图像
Mat result = recognizer.getResultImage();
// 显示结果
imshow("Hand Gesture Recognition", result);
// 按键处理
int key = waitKey(30);
if (key == 32) { // 空格键
imwrite("captured_hand.jpg", frame);
cout << "图像已保存为 captured_hand.jpg" << endl;
}
if (key == 27) { // ESC键
break;
}
}
cap.release();
} else {
// 处理单张图像
HandGestureRecognizer recognizer;
recognizer.setParameters(5, 5000, 20.0, 80.0);
// 预处理
recognizer.preprocess(image);
// 识别手势
GestureType gesture = recognizer.recognizeGesture();
// 显示所有处理步骤
recognizer.displayAllSteps();
// 等待按键
waitKey(0);
}
destroyAllWindows();
return 0;
}
图像预处理:
输入彩色图像 → 灰度转换 → 高斯模糊 → 二值化 → 形态学处理
关键操作
THRESH_BINARY_INV + THRESH_OTSU:反相二值化+自动阈值
形态学闭操作:填充小孔洞
形态学开操作:去除小噪点
轮廓检测:
findContours() → 筛选最大轮廓 → 轮廓简化
筛选逻辑:
只保留面积最大的轮廓(假设是手)
使用approxPolyDP简化轮廓,减少计算量
过滤面积过小的轮廓(minContourArea参数)
凸包与凸缺陷计算:
convexHull() → 获取凸包点 → convexityDefects() → 获取凸缺陷
凸缺陷是什么?
轮廓与凸包之间的凹陷区域
每个缺陷包含3个关键点:起点、终点、最深点
手指间的凹陷就是凸缺陷
手指检测算法
关键判断条件:
深度条件:缺陷深度 > defectDepthThreshold
角度条件:起点-最深点-终点的角度 < fingerAngleThreshold
手指尖确定:
比较缺陷起点和终点到手腕的距离
距离手腕更远的点作为手指尖
手腕点:轮廓或凸包的最低点(假设手在图像底部)
核心算法示意图:
bash
原始图像
↓
预处理(灰度化→模糊→二值化)
↓
轮廓提取 → 最大轮廓筛选
↓
凸包计算 → 凸缺陷检测
↓
├── 深度筛选
├── 角度筛选
└── 手腕距离比较
↓
手指尖定位 → 计数
↓
手势类型映射
↓
可视化输出
数字水印基本原理:
数字水印是一种将特定信息(如版权信息、认证信息等)嵌入到数字媒体(如图像、音频、视频等)中的技术,旨在不影响原始媒体正常使用的前提下,提供诸如版权保护、内容认证、篡改提示等功能。其基本原理是利用数字媒体数据的冗余性和人类感知系统的局限性,通过特定算法将水印信息隐藏在载体数据中。例如,在图像中,可以利用图像像素值的微小变化来嵌入水印,由于人类视觉系统对微小变化不敏感,所以不会影响图像的视觉效果。
浮现式和隐藏式数字水印:
浮现式数字水印:这种水印在正常情况下可见,通常以半透明的标志、图案或文字形式直接显示在数字媒体上。比如电视频道的台标,它始终出现在画面的某个角落,明确展示版权或频道信息。其优点是直观、易于识别,缺点是可能会对原始内容的视觉效果产生一定干扰。
隐藏式数字水印:隐藏式水印在正常情况下不可见,需要通过特定的提取算法才能检测到。它利用数字媒体数据的特性,将水印信息以不可察觉的方式嵌入。例如在图像的频域或空域中,通过微调像素值或系数来嵌入水印。优点是不影响原始内容的外观,对内容的完整性和视觉效果影响小;缺点是提取过程相对复杂,且可能受到信号处理、噪声等因素影响。
嵌入和提取过程:
嵌入过程:选择合适的嵌入位置(空域或频域等),根据水印信息和原始载体数据的特点,使用特定算法将水印信息嵌入到载体数据中。例如在空域嵌入时,可能直接修改像素值;在频域嵌入时,可能调整频率系数。
提取过程:根据嵌入算法和水印特性,对含有水印的载体数据进行分析和处理,提取出嵌入的水印信息。这可能涉及到逆运算、特征提取等操作。
bitwise_and、bitwise_or 和 bitwise_xor 函数:
bitwise_and函数:对两个数组(通常是图像的像素值)按位进行与运算。即对于两个对应位置的二进制位,只有当两者都为 1 时,结果位才为 1,否则为 0。
bitwise_or函数:对两个数组按位进行或运算。只要两个对应位置的二进制位中有一个为 1,结果位就为 1,只有当两者都为 0 时,结果位才为 0。
bitwise_xor函数:对两个数组按位进行异或运算。当两个对应位置的二进制位不同时,结果位为 1,相同时结果位为 0。
实现基于bitwise_xor的数字水印嵌入和提取的代码
bash
#include <QCoreApplication>
#include <QDebug>
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace std;
using namespace cv;
// 嵌入水印
Mat embedWatermark(Mat coverImage, Mat watermarkImage) {
// 确保水印图像和载体图像大小相同
if (coverImage.size() != watermarkImage.size()) {
resize(watermarkImage, watermarkImage, coverImage.size());
}
Mat watermarkedImage;
bitwise_xor(coverImage, watermarkImage, watermarkedImage);
return watermarkedImage;
}
// 提取水印
Mat extractWatermark(Mat watermarkedImage, Mat coverImage) {
Mat extractedWatermark;
bitwise_xor(watermarkedImage, coverImage, extractedWatermark);
return extractedWatermark;
}
int main() {
Mat coverImage = imread("D:/ljl/FPGA/QT_openCV_pro/pro_01/untitled_04/png.png", IMREAD_GRAYSCALE);
Mat watermarkImage = imread("D:/ljl/FPGA/QT_openCV_pro/pro_01/untitled_04/1.png", IMREAD_GRAYSCALE);
if (coverImage.empty() || watermarkImage.empty()) {
cout << "Could not open or find the images" << endl;
return -1;
}
Mat watermarkedImage = embedWatermark(coverImage, watermarkImage);
Mat extractedWatermark = extractWatermark(watermarkedImage, coverImage);
imshow("Cover Image", coverImage);
imshow("Watermark Image", watermarkImage);
imshow("Watermarked Image", watermarkedImage);
imshow("Extracted Watermark", extractedWatermark);
waitKey(0);
return 0;
}
embedWatermark 函数将水印图像通过 bitwise_xor 运算嵌入到载体图像中。
extractWatermark 函数从含水印图像中提取水印,同样使用 bitwise_xor 运算。
在 main 函数中,读取载体图像和水印图像,调用嵌入和提取函数,并显示相关图像。
