保存时间:2026/4/2 11:27:29
import cv2
import numpy as np
import onnxruntime as ort
# 加载量化后的Tiny YOLOv4模型
model = ort.InferenceSession("go_board_detector_int8.onnx")
def detect_go_board(image):
# 图像预处理
input_shape = (416, 416)
blob = cv2.dnn.blobFromImage(image, 1/255.0, input_shape, swapRB=True, crop=False)
# 模型推理
outputs = model.run(None, {'images': blob})
# 后处理:过滤低置信度检测结果
boxes, confidences, class_ids = [], [], []
for output in outputs:
for detection in output:
scores = detection[5:]
class_id = np.argmax(scores)
confidence = scores[class_id]
if confidence > 0.5:
center_x = int(detection[0] * image.shape[1])
center_y = int(detection[1] * image.shape[0])
w = int(detection[2] * image.shape[1])
h = int(detection[3] * image.shape[0])
x = int(center_x - w / 2)
y = int(center_y - h / 2)
boxes.append([x, y, w, h])
confidences.append(float(confidence))
class_ids.append(class_id)
# 非极大值抑制
indices = cv2.dnn.NMSBoxes(boxes, confidences, 0.5, 0.4)
# 解析检测结果
results = []
for i in indices:
i = i.item()
x, y, w, h = boxes[i]
label = "black" if class_ids[i] == 0 else "white"
results.append({
'label': label,
'confidence': confidences[i],
'box': (x, y, w, h)
})
return results
# 主循环:读取摄像头并实时检测
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
# 检测围棋棋盘和棋子
results = detect_go_board(frame)
# 在图像上绘制检测结果
for result in results:
x, y, w, h = result['box']
color = (0, 0, 255) if result['label'] == 'black' else (255, 255, 255)
cv2.rectangle(frame, (x, y), (x + w, y + h), color, 2)
cv2.putText(frame, f"{result['label']}: {result['confidence']:.2f}",
(x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
cv2.imshow('Go Board Detection', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
def preprocess_image(image):
# 转换为HSV颜色空间
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
# 自适应直方图均衡化
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
l, a, b = cv2.split(lab)
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))
cl = clahe.apply(l)
limg = cv2.merge((cl,a,b))
balanced = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
# 高斯模糊
blurred = cv2.GaussianBlur(balanced, (5, 5), 0)
return blurred
| 模型 | 参数量 | 准确率(测试集) | 树莓派 4B FPS | 内存占用 |
|---|---|---|---|---|
| Tiny YOLOv4 | 6.2 MB | 96.8% | 2.1 | 150 MB |
| NanoDet-Plus | 4.1 MB | 95.2% | 3.5 | 120 MB |
| MobileNetV3 + SSD | 10.4 MB | 94.5% | 1.8 | 180 MB |
| 光照条件 | Tiny YOLOv4 | NanoDet-Plus | MobileNetV3 |
|---|---|---|---|
| 均匀自然光 | 98.5% | 97.2% | 96.1% |
| 侧光(阴影明显) | 94.3% | 92.8% | 90.5% |
| 低光环境 | 91.7% | 89.4% | 87.2% |
| 强光反光 | 88.2% | 85.7% | 82.3% |
| 设备 | 处理器 | RAM | Tiny YOLOv4 FPS | 功耗(识别时) |
|---|---|---|---|---|
| 树莓派 4B (2GB) | BCM2711 (1.5GHz) | 2 GB | 2.1 | 3.2W |
| Jetson Nano | Cortex-A57 | 4 GB | 5.8 | 5.5W |
| NanoPi NEO2 | H5 (1.5GHz) | 1 GB | 1.8 | 2.1W |
| RockPi S | RK3308 (1.3GHz) | 512MB | 1.2 | 1.8W |
| 优化方法 | 推理速度提升 | 准确率变化 | 模型大小变化 |
|---|---|---|---|
| INT8 量化 | +42% | -0.5% | -60% |
| 输入分辨率降至 320×320 | +58% | -2.3% | - |
| 棋盘区域预裁剪 | +35% | +0.7% | - |
| 多线程并行处理 | +28% | - | - |
findChessboardCorners)# 将Darknet模型转换为ONNX(需安装OpenCV和ONNX Runtime)
git clone https://github.com/opencv/opencv.git
cd opencv/samples/dnn
python3 darknet_to_onnx.py --cfg yolov4-tiny.cfg --weights yolov4-tiny.weights --output yolov4-tiny.onnx
# 在树莓派上安装依赖
sudo apt-get update
sudo apt-get install -y python3-opencv python3-pip
pip3 install onnxruntime numpy
import onnx
from onnxruntime.quantization import quantize_dynamic, QuantType
# 下载预训练模型(需替换为实际URL)
!wget https://example.com/go_board_detector.onnx -O model.onnx
# 模型量化(INT8),减小体积并加速推理
quantized_model = quantize_dynamic(
"model.onnx",
"model_int8.onnx",
weight_type=QuantType.QUInt8
)
import cv2
import numpy as np
import onnxruntime as ort
# 加载量化后的模型
session = ort.InferenceSession("model_int8.onnx")
def detect_go_board(image):
# 图像预处理
input_shape = (416, 416)
blob = cv2.dnn.blobFromImage(
image, 1/255.0, input_shape, swapRB=True, crop=False)
# 模型推理
outputs = session.run(None, {"images": blob})
# 后处理(非极大值抑制等)
boxes, confidences, class_ids = [], [], []
for output in outputs:
# 解析检测结果(具体逻辑取决于模型输出格式)
pass
return boxes, confidences, class_ids
# 主循环:读取摄像头并检测
cap = cv2.VideoCapture(0)
while True:
ret, frame = cap.read()
if not ret:
break
# 检测围棋棋盘和棋子
boxes, confidences, class_ids = detect_go_board(frame)
# 可视化结果
for box, conf, cls in zip(boxes, confidences, class_ids):
x, y, w, h = box
label = "black" if cls == 0 else "white"
cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2)
cv2.putText(frame, f"{label}: {conf:.2f}", (x, y-10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
cv2.imshow("Go Board Detection", frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
export OPENCV_ENABLE_NEON=ON1.22.3 可能不是有效的 NumPy 版本。python --version
numpy<1.22)。pip install numpy -U
requirements.txt 中修改 NumPy 版本要求,例如:numpy>=1.22.0,<1.23.0 # 允许1.22.x系列的任意版本
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
1.22.3,可尝试从源码编译:pip install numpy==1.22.3 --no-binary :all:
import numpy as np
print(np.__version__) # 应输出1.22.3或其他安装的版本
venv 或 conda)隔离 Python 环境。cv::circle函数的最后一个参数是thickness(线条粗细)和lineType(线条类型)的组合参数,用于控制圆的绘制样式。以下是详细说明:cv::circle函数原型void cv::circle(
InputOutputArray img, // 输入/输出图像
Point center, // 圆心坐标
int radius, // 半径
const Scalar& color, // 颜色
int thickness = 1, // 线条粗细(默认1)
int lineType = LINE_8, // 线条类型(默认8连通线)
int shift = 0 // 坐标点的小数点位数
);
thickness(线条粗细)cv::circle(img, center, radius, color, 2); // 绘制2像素宽的轮廓线
cv::circle(img, center, radius, color, -1); // 填充圆
lineType(线条类型)LINE_8(默认值):8 连通线,速度快但转角处不平滑。LINE_4:4 连通线,适用于简单图形。LINE_AA:抗锯齿线,使用高斯滤波使线条更平滑,视觉效果更好,但性能稍低。例如:cv::circle(img, center, radius, color, 2, cv::LINE_AA); // 抗锯齿轮廓线
// 示例1:绘制2像素宽的抗锯齿轮廓线
cv::circle(img, center, 50, cv::Scalar(0, 255, 0), 2, cv::LINE_AA);
// 示例2:填充抗锯齿圆形
cv::circle(img, center, 50, cv::Scalar(0, 0, 255), -1, cv::LINE_AA);
LINE_4、LINE_8或LINE_AA控制线条质量。cv::moveWindow 函数用于将指定窗口移动到屏幕上的特定位置。以下是其参数和用法说明:void cv::moveWindow(
const String& winname, // 窗口名称(必须与创建窗口时的名称一致)
int x, // 窗口左上角在屏幕上的x坐标(像素)
int y // 窗口左上角在屏幕上的y坐标(像素)
);
winnameconst String&cv::namedWindow 或 cv::imshow 时指定的窗口名称完全一致(区分大小写)。"GoBoardDetection"。xintyint#include <opencv2/opencv.hpp>
int main() {
// 创建窗口
cv::namedWindow("GoBoard", cv::WINDOW_NORMAL);
// 移动窗口到屏幕坐标 (100, 50)
cv::moveWindow("GoBoard", 100, 50);
// 显示图像
cv::Mat image = cv::imread("go_board.jpg");
cv::imshow("GoBoard", image);
cv::waitKey(0);
return 0;
}
cv::namedWindow 或 cv::imshow 创建窗口,才能使用 moveWindow 移动它。(0, 0):屏幕左上角(1920, 0):1920×1080 分辨率屏幕的右上角(0, 1080):屏幕左下角cv::WINDOW_FULLSCREEN),moveWindow 可能无效。cv::moveWindow,你可以灵活控制 OpenCV 窗口在屏幕上的位置,提升可视化体验。cv::moveWindow,窗口会根据系统默认策略显示,可能受以下因素影响:cv::namedWindow("GoBoard", cv::WINDOW_NORMAL);
cv::moveWindow("GoBoard", 100, 50); // 强制移动到(100, 50)
cv::WND_PROP_POS 属性(OpenCV 4.0+)cv::namedWindow("GoBoard", cv::WINDOW_NORMAL);
cv::setWindowProperty("GoBoard", cv::WND_PROP_POS, cv::Point(100, 50));
(1920, 0) 开始)。cv::namedWindow("GoBoard", cv::WINDOW_FULLSCREEN); // 忽略坐标,覆盖整个屏幕
cv::namedWindow("GoBoard", cv::WINDOW_AUTOSIZE); // 窗口大小由内容决定,但位置仍可移动
cv::moveWindow,确保在不同系统上行为一致。例如:// 创建并定位窗口
cv::namedWindow("GoBoard", cv::WINDOW_NORMAL);
cv::resizeWindow("GoBoard", 800, 600); // 调整大小
cv::moveWindow("GoBoard", 100, 100); // 固定位置
// 显示图像
cv::imshow("GoBoard", image);
(开头,以)结尾。文件由节点组成,节点之间用分号;分隔,节点可以有多个属性,属性格式为属性名(属性值)。例如,(;GM(1)FF(4)SZ(19))表示一个包含围棋游戏类型、SGF版本和棋盘大小信息的节点。FF表示文件遵循的SGF规范版本;GM用于指定游戏类型,围棋中属性值为1;SZ表示棋盘大小,如SZ(19)代表19路棋盘。DT表示比赛日期,格式为YYYY-MM-DD;PB表示黑方姓名;PW表示白方姓名;BR表示黑方段位;WR表示白方段位;GN或EV表示比赛名称;RE表示比赛结果,如B+3.5表示黑胜3.5目,B+R表示黑中盘胜。HA表示让子数,KM表示贴目数。B表示黑方落子,W表示白方落子,属性值为棋子在棋盘上的位置,用两个小写字母表示,第一个字母指定列数(从左往右),第二个指定行数(从上往下),如b(ab)表示黑方在棋盘的A列第2行落子。C用于添加注释内容,对棋局进行说明、分析等。()内,使用一对()来包含一段变化内容。同一层级的变化图并列排列,变化图内还可能嵌套变化图,以记录不同的走法变化。B和W)不能和设置属性处于同一个节点中。文件中所有的属性标识及属性值使用ASCII字符集,除单文本和文本外,单文本和文本使用的字符集由CA属性决定。(;SZ(19) // 棋盘大小为19路
GM(1) // 游戏类型为围棋
;B(aa);W(ab) // 黑方在aa位置落子,白方在ab位置落子
(;B(ac) // 开始一个变化图,黑方在ac位置落子
W(ad) // 白方在ad位置落子
(;B(ae) // 嵌套的变化图,黑方在ae位置落子
W(af) // 白方在af位置落子
)
(;B(ag) // 另一个嵌套的变化图,黑方在ag位置落子
W(ah) // 白方在ah位置落子
)
)
(;B(ba) // 另一个同级变化图,黑方在ba位置落子
W(bb) // 白方在bb位置落子
)
)
()包含了整个棋局信息。在;B(aa);W(ab)之后,有两个并列的变化图,分别是(;B(ac)...)和(;B(ba)...)。在(;B(ac)...)这个变化图中,又嵌套了两个变化图(;B(ae)...)和(;B(ag)...),用于记录不同的走法变化。cv::waitKey(delay) 的参数 delay 表示等待用户按键的时间(毫秒),而非秒。具体解释如下:delay = 1:等待 1 毫秒,如果在此期间用户按下任意键,则返回该键的 ASCII 码(如 27 对应 ESC 键);否则返回 -1。delay = 0:无限等待,直到用户按键。delay > 0:等待指定的毫秒数。cv::imshow("Image", img);
cv::waitKey(1); // 刷新窗口并继续执行后续代码
waitKey,窗口可能不会显示图像,因为 GUI 事件循环未被触发。while (true) {
cv::imshow("Video", frame);
if (cv::waitKey(30) == 27) { // 每30ms刷新一次,按ESC退出
break;
}
}
30ms 对应约 33FPS(1000ms/30ms ≈ 33.3)的帧率。waitKey 的实际等待时间可能超过指定值。waitKey 只影响通过 cv::namedWindow 或 cv::imshow 创建的窗口,对命令行或无头环境无效。waitKey(1) 可能无法及时响应按键,需增大延迟(如 waitKey(10))。waitKey(1) 是 1 秒:实际是 1 毫秒。waitKey 会暂停程序:仅暂停 GUI 事件处理,其他代码仍可能继续执行(如后台线程)。// 示例1:显示图像1秒后自动关闭
cv::imshow("Image", img);
cv::waitKey(1000); // 等待1000ms(1秒)
// 示例2:按任意键继续
cv::imshow("Image", img);
cv::waitKey(0); // 无限等待,直到按键
// 示例3:视频流中控制帧率
while (cap.read(frame)) {
cv::imshow("Video", frame);
if (cv::waitKey(25) >= 0) { // 约40FPS (1000/25),按任意键退出
break;
}
}
delay 参数,你可以控制窗口的响应速度和显示时长。std::from_chars**,它比传统的std::stoi/stod更安全、更高效,且不会抛出异常。以下是具体示例:std::from_chars 转换整数#include <iostream>
#include <charconv> // C++17新增
int main() {
std::string str = "12345";
int value = 0;
// 转换并检查结果
auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), value);
if (ec == std::errc()) {
std::cout << "转换成功,值为: " << value << std::endl;
} else if (ec == std::errc::invalid_argument) {
std::cout << "无效参数:不是有效的数字" << std::endl;
} else if (ec == std::errc::result_out_of_range) {
std::cout << "范围溢出:数字超出int范围" << std::endl;
}
return 0;
}
double)#include <iostream>
#include <charconv>
int main() {
std::string str = "3.14159";
double value = 0.0;
auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), value);
if (ec == std::errc()) {
std::cout << "转换成功,值为: " << value << std::endl;
} else if (ec == std::errc::invalid_argument) {
std::cout << "无效参数" << std::endl;
} else if (ec == std::errc::result_out_of_range) {
std::cout << "范围溢出" << std::endl;
}
return 0;
}
std::string str = "123abc";
int value = 0;
auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), value);
if (ec == std::errc() && ptr == str.data() + str.size()) {
// 完全转换(整个字符串都是有效数字)
} else if (ec == std::errc() && ptr != str.data() + str.size()) {
// 部分转换(字符串包含有效数字+其他字符)
std::cout << "部分转换:仅处理了前" << (ptr - str.data()) << "个字符" << std::endl;
}
std::string str = "9999999999"; // 超出int范围
long long value = 0;
auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(), value);
if (ec == std::errc::result_out_of_range) {
std::cout << "溢出:值超出long long范围" << std::endl;
}
std::from_chars 更安全?std::errc)而非抛出异常,避免额外开销。ptr),便于检查是否完全转换。std::stoi/stod + 异常处理:try {
int value = std::stoi(str);
} catch (const std::invalid_argument& e) {
std::cout << "无效参数" << std::endl;
} catch (const std::out_of_range& e) {
std::cout << "范围溢出" << std::endl;
}
std::stoi/stod存在性能开销(异常机制)和潜在的未定义行为(如字符串包含非数字字符)。std::from_chars,尤其是在性能敏感或需要严格错误处理的场景中。std::ignore来忽略结构化绑定中的特定元素,但语法需要调整。在你的例子中,直接将std::ignore放在结构化绑定声明中是不正确的。正确的做法是使用占位符名称,并在后续代码中不使用它,或者在 C++20 及以后的版本中使用未命名的结构化绑定。#include <iostream>
#include <charconv>
void foo(const char* data) {
int value;
auto [ptr, ec] = std::from_chars(data, data + strlen(data), value);
// 忽略ptr,只使用ec
if (ec != std::errc()) {
std::cerr << "Conversion error\n";
}
}
#include <iostream>
#include <charconv>
void foo(const char* data) {
int value;
auto [_, ec] = std::from_chars(data, data + strlen(data), value);
// 使用_作为占位符(C++20允许)
if (ec != std::errc()) {
std::cerr << "Conversion error\n";
}
}
std::ignore不能直接用于结构化绑定的声明,但你可以在后续赋值中使用它:#include <iostream>
#include <charconv>
#include <tuple>
void foo(const char* data) {
int value;
auto result = std::from_chars(data, data + strlen(data), value);
// 显式使用std::ignore(更冗长)
std::ignore = std::get<0>(result);
auto ec = std::get<1>(result);
if (ec != std::errc()) {
std::cerr << "Conversion error\n";
}
}
std::from_chars的结果,推荐使用 C++20 的未命名结构化绑定语法(方法二),因为它简洁且明确表达了忽略第一个元素的意图。如果你的编译器不支持 C++20,则使用方法一,简单地声明变量但不使用它。std::ignore是一个特殊的对象,用于在std::tie中忽略元组元素(例如std::tie(std::ignore, b) = some_tuple;)。auto [a, b] = ...)是独立的语法,不能直接在声明中使用std::ignore。-s or -video_size option followed by the desired width and height. Here are the common methods:ffmpeg -i input.mp4 -s WIDTHxHEIGHT output.mp4
ffmpeg -i input.mp4 -s 1280x720 output.mp4
720p, 1080p, etc., by specifying the dimensions directly:1280x7201920x10803840x2160-vf scale=WIDTH:HEIGHT with -1 to automatically calculate the other dimension:ffmpeg -i input.mp4 -vf scale=1280:-1 output.mp4 # Height is auto-calculated
force_original_aspect_ratio=decrease to pad the video with black bars if needed:ffmpeg -i input.mp4 -vf "scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" output.mp4
-pix_fmt yuv420p:ffmpeg -i input.mp4 -s 1280x720 -pix_fmt yuv420p output.mp4
-b:v for better quality:ffmpeg -i input.mp4 -s 1280x720 -b:v 2M output.mp4
ffmpeg -i input.mp4 -s 1920x1080 -pix_fmt yuv420p -b:v 4M output.mp4
-vf scale=1280:718 (both even) or -vf scale=1280:719:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2.-deinterlace if the input is interlaced.cv::inRange函数进行颜色过滤,cv::GaussianBlur函数进行高斯滤波,cv::Canny函数进行边缘检测,cv::findContours函数进行轮廓提取,通过计算轮廓的各种特征来实现对预知形状、大小和颜色的图形的检测。这种方法类似于模板匹配的思想,但针对特定形状和颜色进行了定制化处理,比传统的模板匹配(如cv::matchTemplate函数实现的方法)更具针对性和灵活性,能更好地适应已知形状、大小和颜色的目标检测任务。cv::findContours函数是 OpenCV 中用于轮廓提取的重要函数,以下是其用法及能力边界介绍:void cv::findContours(
InputOutputArray image,
OutputArrayOfArrays contours,
OutputArray hierarchy,
int mode,
int method,
Point offset = Point()
);
image:输入的二值图像,非零像素被视为前景,零像素为背景。通常需先对图像进行阈值处理、边缘检测等预处理。contours:输出的轮廓向量,每个轮廓是一个点的向量。hierarchy:可选输出的轮廓层次结构信息。mode:轮廓检索模式,如RETR_EXTERNAL只检索最外层轮廓,RETR_LIST检索所有轮廓但不建立层次关系,RETR_TREE检索所有轮廓并建立完整的树形层次结构。method:轮廓逼近方法,CHAIN_APPROX_NONE存储所有轮廓点,CHAIN_APPROX_SIMPLE压缩水平、垂直和对角方向的链码,仅保留端点。#include <iostream>
#include <opencv2/opencv.hpp>
int main() {
// 读取图像并转为灰度图
cv::Mat image = cv::imread("image.jpg", cv::IMREAD_GRAYSCALE);
// 阈值处理得到二值图像
cv::Mat binary;
cv::threshold(image, binary, 128, 255, cv::THRESH_BINARY);
// 存储轮廓和层次结构
std::vector<std::vector<cv::Point>> contours;
cv::Mat hierarchy;
// 提取轮廓
cv::findContours(binary, contours, hierarchy, cv::RETR_TREE, cv::CHAIN_APPROX_SIMPLE);
// 绘制轮廓
cv::Mat drawing = cv::Mat::zeros(binary.size(), CV_8UC3);
for (size_t i = 0; i < contours.size(); ++i) {
cv::Scalar color(rand() & 255, rand() & 255, rand() & 255);
cv::drawContours(drawing, contours, i, color, 2, cv::LINE_8, hierarchy, 0);
}
// 显示结果
cv::imshow("Contours", drawing);
cv::waitKey(0);
return 0;
}
cv::findContours函数和一些额外的处理来筛选。以下是具体步骤:cv::inRange函数根据已知的颜色范围创建一个掩膜图像,只保留目标颜色区域。cv::findContours函数在掩膜图像上提取轮廓,代码示例如下:// 假设mask是已经创建好的掩膜图像
std::vector<std::vector<cv::Point>> contours;
cv::Mat hierarchy;
cv::findContours(mask, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
r,面积公式为A = πr²,周长公式为C = 2πr。可以通过比较轮廓的面积和周长与理论值的接近程度来判断是否为目标圆形。示例代码如下:for (const auto& contour : contours) {
// 计算轮廓面积
double area = cv::contourArea(contour);
// 计算轮廓周长
double perimeter = cv::arcLength(contour, true);
// 计算理论面积和周长
double expectedArea = M_PI * std::pow(radius, 2);
double expectedPerimeter = 2 * M_PI * radius;
// 设置面积和周长的误差阈值
double areaErrorThreshold = 0.1 * expectedArea;
double perimeterErrorThreshold = 0.1 * expectedPerimeter;
// 判断是否接近圆形
if (std::abs(area - expectedArea) < areaErrorThreshold &&
std::abs(perimeter - expectedPerimeter) < perimeterErrorThreshold) {
// 符合条件,进行相应处理,如绘制轮廓或记录位置等
cv::drawContours(resultImage, {contour}, 0, cv::Scalar(0, 255, 0), 2);
}
}
cv::contourArea和cv::arcLength函数计算轮廓的面积和周长,然后与理论值进行比较,设置合适的误差阈值来判断是否为目标圆形。如果符合条件,则可以在结果图像上绘制该轮廓或进行其他所需的操作。