1.VM的像素标定流程很简单、如下:

2.拿到标定结果对将图像尺寸转换成物理尺寸
使用了两种方法进行转换,实际证明单位转换更加准确。
使用opencv/c++进行标定,我的圆标定板的直径是7.5mm,圆心之间的距离是15.0mm
简单的像素标定:
cpp
void xsCeli::calculatePixelEquivalent(const cv::Mat& src, float real_distance_mm)
{
// 加载图像
//cv::Mat src = cv::imread("circle_pattern.jpg");
if (src.empty()) {
std::cerr << "Could not load image!" << std::endl;
return;
}
cv::Mat gray;
cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY); // 转灰度图
// 霍夫变换检测圆
std::vector<cv::Vec3f> circles;
cv::HoughCircles(gray, circles, cv::HOUGH_GRADIENT,
1, // dp 分辨率比例
50, // minDist 圆心最小距离
100, 30, // Canny 边缘检测阈值
10, 100); // 半径范围
// 只处理前两个圆(假设是已知间距的两个圆)
if (circles.size() < 2) {
std::cerr << "Not enough circles detected!" << std::endl;
return;
}
// 提取前两个圆的圆心坐标
cv::Point2f p1(circles[0][0], circles[0][1]);
cv::Point2f p2(circles[1][0], circles[1][1]);
// 计算像素距离
float pixel_distance = cv::norm(p1 - p2);
// 已知这两个圆的实际物理距离(单位:mm)
//float real_distance_mm = 15.0; // 举例:20mm
// 像素当量:mm per pixel
float mm_per_pixel = real_distance_mm / pixel_distance;
std::cout << "Pixel distance: " << pixel_distance << " pixels" << std::endl;
std::cout << "Real distance: " << real_distance_mm << " mm" << std::endl;
std::cout << "Scale: " << mm_per_pixel << " mm/pixel" << std::endl;
// 可视化结果
for (size_t i = 0; i < circles.size(); ++i) {
cv::Point center(cvRound(circles[i][0]), cvRound(circles[i][1]));
int radius = cvRound(circles[i][2]);
cv::circle(src, center, radius, cv::Scalar(0, 255, 0), 2);
cv::circle(src, center, 2, cv::Scalar(0, 0, 255), 3); // 圆心
}
cv::imshow("Detected Circles", src);
cv::waitKey(0);
return;
}
我使用的银行卡进行的测试、
银行卡的长度通常是固定的,遵循国际标准ISO/IEC 7810 ID-1,这是针对识别卡物理特性的标准。根据这一标准,银行卡(包括信用卡和借记卡)的尺寸应为:
- 长度:85.60毫米(约3.37英寸)
- 宽度:53.98毫米(约2.13英寸)
- 厚度:0.76毫米(约0.03英寸)
cpp
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
// 全局变量:标定系数(像素/毫米)
double pixelToMm = 24.066;
// 函数声明
cv::Mat preprocessImage(const cv::Mat& input);
std::vector<cv::Point> findTargetContour(const cv::Mat& processedImg);
cv::RotatedRect getBoundingRect(const std::vector<cv::Point>& contour);
double calculateWidth(const cv::RotatedRect& rect);
void calibrate(const cv::Mat& calibImg, double knownWidth);
int main() {
// 加载图像
//cv::Mat src = cv::imread("circle_pattern.jpg");
//if (src.empty()) {
// std::cerr << "Could not load image!" << std::endl;
// return -1;
//}
//xsCeli celi;
//celi.calculatePixelEquivalent(src, 15.0f); // 假设实际距离为15mm
//return 0;
// 1. 读取图像(替换为实际相机获取或文件读取)
cv::Mat image = cv::imread("23.jpg");
if (image.empty()) {
std::cerr << "无法读取图像!" << std::endl;
return -1;
}
// 设置最大显示宽度和高度
int max_width = 800;
int max_height = 600;
cv::Mat resized;
double scale = std::min(max_width / (double)image.cols, max_height / (double)image.rows);
if (scale < 1.0) { // 只有当图像大于 max_width/max_height 时才缩放
cv::resize(image, resized, cv::Size(), scale, scale, cv::INTER_AREA);
}
else {
resized = image; // 否则直接显示原图
}
image = resized; // 使用缩放后的图像
// 2. 图像预处理
cv::Mat processedImg = preprocessImage(image);
// 3. 提取目标轮廓(假设已完成标定)
std::vector<cv::Point> targetContour = findTargetContour(processedImg);
if (targetContour.empty()) {
std::cerr << "未找到目标轮廓!" << std::endl;
return -1;
}
// 4. 获取最小包围矩形
cv::RotatedRect boundingRect = getBoundingRect(targetContour);
// 5. 计算宽度(基于标定系数)
double widthMm = calculateWidth(boundingRect);
widthMm = widthMm / scale;
std::cout << "目标产品宽度:" << widthMm << " 毫米" << std::endl;
// 6. 可视化结果
cv::Mat resultImg = image.clone();
// 1. 检查resultImg是否有效
if (resultImg.empty()) {
std::cerr << "Error: resultImg is empty!" << std::endl;
return -1;
}
// 2. 检查targetContour是否有效
if (targetContour.empty()) {
std::cerr << "Error: targetContour is empty!" << std::endl;
return -1;
}
// 3. 输出轮廓信息(可选)
std::cout << "Drawing contour with " << targetContour.size() << " points" << std::endl;
cv::drawContours(resultImg, std::vector<std::vector<cv::Point>>{ targetContour }, 0, cv::Scalar(0, 255, 0), 2);
cv::ellipse(resultImg, boundingRect, cv::Scalar(0, 0, 255), 2);
// 标注宽度
cv::Point textPos(boundingRect.center.x - 50, boundingRect.center.y - 50);
std::string widthText = "Width: " + std::to_string(widthMm) + " mm";
cv::putText(resultImg, widthText, textPos, cv::FONT_HERSHEY_SIMPLEX, 0.7,
cv::Scalar(255, 0, 0), 2);
// 1. 画轮廓
std::vector<std::vector<cv::Point>> contours{ targetContour };
cv::drawContours(resultImg, contours, 0, cv::Scalar(0, 255, 0), 2); // 绿色轮廓
// 2. 获取并画旋转矩形
cv::Point2f vertices[4];
boundingRect.points(vertices);
for (int i = 0; i < 4; ++i) {
cv::line(resultImg, vertices[i], vertices[(i + 1) % 4], cv::Scalar(0, 0, 255), 2); // 红色矩形
}
cv::imshow("测量结果", resultImg);
cv::waitKey(0);
return 0;
}
// 图像预处理:灰度转换、滤波、二值化
cv::Mat preprocessImage(const cv::Mat& input) {
cv::Mat gray, blurred, binary;
// 1. 灰度转换
cv::cvtColor(input, gray, cv::COLOR_BGR2GRAY);
// 2. 高斯滤波去噪(根据实际噪声调整参数)
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);
// 3. 自适应阈值二值化(适应不均匀光照)
cv::adaptiveThreshold(blurred, binary, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C,
cv::THRESH_BINARY_INV, 11, 2);
// 4. 形态学操作:闭运算填充小孔,开运算去除噪点
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::morphologyEx(binary, binary, cv::MORPH_CLOSE, kernel);
cv::morphologyEx(binary, binary, cv::MORPH_OPEN, kernel);
//cv::imshow("Processed Image", binary);
// cv::waitKey(0);
return binary;
}
// 提取目标轮廓(筛选带倒圆角的矩形)
std::vector<cv::Point> findTargetContour(const cv::Mat& processedImg) {
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
// 1. 查找轮廓
cv::findContours(processedImg, contours, hierarchy, cv::RETR_EXTERNAL,
cv::CHAIN_APPROX_SIMPLE);
if (contours.empty()) return {};
// 2. 筛选目标轮廓(根据面积、形状因子筛选)
std::vector<cv::Point> targetContour;
double maxArea = 0;
std::cout << "目标轮廓个数:" << contours.size() << std::endl;
std::sort(contours.begin(), contours.end(),
[](const auto& a, const auto& b) {
return cv::contourArea(a) > cv::contourArea(b);
});
//for (const auto& contour : contours) {
// double area = cv::contourArea(contour);
// if (area < 1000) continue; // 过滤小面积噪声
// // 计算轮廓近似(关键:调整epsilon适应倒圆角)
// std::vector<cv::Point> approx;
// double epsilon = 0.02 * cv::arcLength(contour, true); // 2%的轮廓周长
// cv::approxPolyDP(contour, approx, epsilon, true);
// // 带倒圆角的矩形近似后应为8个顶点(4个边+4个圆角弧段)
// if (approx.size() > 6 && approx.size() < 12) {
// double areaRatio = area / (cv::boundingRect(contour).width *
// cv::boundingRect(contour).height);
// if (areaRatio > 0.8) { // 面积比接近矩形
// targetContour = contour;
// maxArea = area;
// }
// }
//}
targetContour = contours[0];
return targetContour;
}
// 获取最小包围旋转矩形
cv::RotatedRect getBoundingRect(const std::vector<cv::Point>& contour) {
return cv::minAreaRect(contour);
}
// 计算宽度(基于标定系数)
double calculateWidth(const cv::RotatedRect& rect) {
// 旋转矩形的宽高可能需要根据角度判断哪条是宽度
double width = rect.size.width;
double height = rect.size.height;
// 银行卡宽度通常小于长度,假设宽度是较短边
return (width < height ? width : height) / pixelToMm;
}
// 标定函数(使用已知宽度的标准物)
void calibrate(const cv::Mat& calibImg, double knownWidth) {
cv::Mat processed = preprocessImage(calibImg);
std::vector<cv::Point> calibContour = findTargetContour(processed);
if (calibContour.empty()) {
std::cerr << "标定图像轮廓提取失败!" << std::endl;
return;
}
cv::RotatedRect calibRect = cv::minAreaRect(calibContour);
double calibPixelWidth = (calibRect.size.width < calibRect.size.height
? calibRect.size.width : calibRect.size.height);
// 计算像素/毫米系数
pixelToMm = calibPixelWidth / knownWidth;
std::cout << "标定完成,像素/毫米系数:" << pixelToMm << std::endl;
}
得到以下结果:
测量结果有一定的误差、可以验证这样写是没有问题的。
这是标定图片与银行卡的图片连接,如有需要自取。