import cv2
import numpy as np
import time
from collections import defaultdict
import math
def detect_a4_paper_and_shapes(frame, base_area):
"""检测A4纸并识别其上的形状,返回处理后的帧、形状数据和变换后区域面积"""
frame = cv2.resize(frame, (800, 600))
output_frame = frame.copy()
shape_data = [] # 存储形状信息:(形状名称, 面积mm², 占比%)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (7, 7), 0)
edged = cv2.Canny(blurred, 30, 120)
contours, _ = cv2.findContours(edged.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
a4_contour = None
warped_area = 0 # 默认值为0,表示未检测到A4纸
# 筛选A4纸轮廓
for contour in contours:
if cv2.contourArea(contour) < 5000:
continue
peri = cv2.arcLength(contour, True)
approx = cv2.approxPolyDP(contour, 0.02 * peri, True)
if len(approx) == 4 and cv2.isContourConvex(approx):
a4_contour = approx
break
if a4_contour is not None:
cv2.drawContours(output_frame, [a4_contour], -1, (0, 0, 255), 2)
cv2.putText(output_frame, "A4 Paper",
(a4_contour[0][0][0], a4_contour[0][0][1] - 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2)
# 透视变换准备
pts = a4_contour.reshape(4, 2)
rect = np.zeros((4, 2), dtype="float32")
s = pts.sum(axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
diff = np.diff(pts, axis=1)
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
(tl, tr, br, bl) = rect
widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
maxWidth = max(int(widthA), int(widthB))
heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
maxHeight = max(int(heightA), int(heightB))
# 计算变换后的区域面积
warped_area = maxWidth * maxHeight
dst = np.array([
[0, 0],
[maxWidth - 1, 0],
[maxWidth - 1, maxHeight - 1],
[0, maxHeight - 1]], dtype="float32")
try:
M = cv2.getPerspectiveTransform(rect, dst)
warped = cv2.warpPerspective(frame, M, (maxWidth, maxHeight))
# 形状检测
gray_warped = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
thresh = cv2.adaptiveThreshold(gray_warped, 255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 11, 2)
contours_warped, _ = cv2.findContours(thresh.copy(),
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
# 像素到毫米的转换比例(基于基准面积)
px_per_mm = maxWidth / math.sqrt(base_area * (maxWidth / maxHeight)) # 适配基准面积的比例计算
# 识别形状并计算面积
for cnt in contours_warped:
area_px = cv2.contourArea(cnt)
if area_px < 100:
continue
# 计算实际面积和占比(占空比=图形面积/基准面积)
area_mm2 = area_px / (px_per_mm ** 2)
ratio = (area_mm2 / base_area) * 100 # 占空比(百分比)
peri = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.03 * peri, True)
shape = "Unknown"
if len(approx) == 3:
shape = "三角形"
elif len(approx) == 4:
x, y, w, h = cv2.boundingRect(approx)
aspect_ratio = float(w) / h
if 0.95 <= aspect_ratio <= 1.05:
shape = "正方形"
else:
shape = "长方形"
else:
perimeter = cv2.arcLength(cnt, True)
circularity = 4 * np.pi * area_px / (perimeter ** 2) if perimeter > 0 else 0
if 0.7 < circularity < 1.3:
shape = "圆形"
elif 5 <= len(approx) <= 8:
shape = "多边形"
# 只记录目标形状
if shape in ["三角形", "圆形", "正方形"]:
shape_data.append((shape, area_mm2, ratio))
# 在图像上标记
M = cv2.moments(cnt)
if M["m00"] != 0:
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
cv2.drawContours(warped, [cnt], -1, (0, 255, 0), 2)
cv2.putText(warped, f"{shape} {ratio:.1f}%",
(cX - 50, cY),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
# 放置矫正后的图像
h, w = warped.shape[:2]
if h < output_frame.shape[0] and w < output_frame.shape[1]:
output_frame[0:h, 0:w] = warped
else:
scale = min(output_frame.shape[0] / h, output_frame.shape[1] / w)
warped = cv2.resize(warped, (int(w * scale), int(h * scale)))
h, w = warped.shape[:2]
output_frame[0:h, 0:w] = warped
except Exception as e:
print(f"处理出错: {e}")
return output_frame, shape_data, warped_area
def calculate_dimensions(shape, avg_area):
"""根据图形类型和平均面积计算尺寸(边长或直径,单位:毫米)"""
if shape == "正方形":
# 正方形面积 = 边长² → 边长 = √面积
side_length = math.sqrt(avg_area)
return f"边长: {side_length:.2f} mm"
elif shape == "圆形":
# 圆形面积 = π×(直径/2)² → 直径 = 2×√(面积/π)
diameter = 2 * math.sqrt(avg_area / math.pi)
return f"直径: {diameter:.2f} mm"
elif shape == "三角形":
# 假设为等边三角形:面积 = (√3/4)×边长² → 边长 = √(4×面积/√3)
side_length = math.sqrt((4 * avg_area) / math.sqrt(3))
return f"等边三角形边长: {side_length:.2f} mm"
return "无法计算尺寸"
def set_camera_resolution(cap, width=640, height=480):
"""尝试设置摄像头分辨率"""
# 设置目标分辨率
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
# 获取实际设置的分辨率
actual_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
return actual_width, actual_height
def main():
# 设定基准面积(整张纸的面积,单位:平方毫米)
base_area = 43690 # 更新为43690平方毫米
cap = cv2.VideoCapture(0)
if not cap.isOpened():
print("无法打开摄像头")
return
# 设置摄像头分辨率为1080p (1920x1080)
actual_width, actual_height = set_camera_resolution(cap, 640, 480)
print(f"尝试设置摄像头分辨率: 1280x720")
print(f"实际分辨率: {actual_width}x{actual_height}")
print(f"\n程序启动,以基准面积 {base_area} mm² 计算占空比,将进行4秒检测...")
print("按 'q' 键可提前退出")
# 存储4秒内的检测数据
detection_history = defaultdict(list) # 格式: {形状: [(面积, 占比), ...]}
warped_areas = [] # 存储变换后的区域面积
start_time = time.time()
detection_duration = 3 # 检测持续时间(秒)
try:
while True:
# 检查是否达到检测时间
elapsed_time = time.time() - start_time
if elapsed_time >= detection_duration:
break
ret, frame = cap.read()
if not ret:
print("无法获取视频帧")
break
frame = cv2.flip(frame, 1) # 镜像翻转
# 显示剩余时间
remaining_time = max(0, int(detection_duration - elapsed_time))
cv2.putText(frame, f"检测剩余: {remaining_time}秒", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
# 显示基准面积
cv2.putText(frame, f"基准面积: {base_area} mm²", (10, 70),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 2)
# 显示当前分辨率
cv2.putText(frame, f"分辨率: {actual_width}x{actual_height}", (10, 110),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
# 处理帧并检测形状(传入基准面积)
processed_frame, shape_data, warped_area = detect_a4_paper_and_shapes(frame, base_area)
# 如果检测到了A4纸,记录区域面积
if warped_area > 0:
warped_areas.append(warped_area)
# 记录检测数据
for shape, area, ratio in shape_data:
detection_history[shape].append((area, ratio))
cv2.imshow("A4纸形状检测", processed_frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
print("用户提前退出")
return
# 计算并打印结果(含尺寸计算)
print("\n===== 4秒检测结果 =====")
print(f"基准面积(整张纸): {base_area} mm²")
print(f"摄像头分辨率: {actual_width}x{actual_height}")
# 打印平均变换区域面积
if warped_areas:
avg_warped_area = sum(warped_areas) / len(warped_areas)
print(f"平均变换区域面积: {avg_warped_area:.2f} 像素")
print(f"距离: {17910*avg_warped_area**(-0.501)-3.495:.2f} cm")
else:
print("未检测到A4纸")
if not detection_history:
print("未检测到任何图形")
else:
for shape, data in detection_history.items():
# 计算平均面积和平均占空比
avg_area = sum(area for area, _ in data) / len(data)
avg_ratio = sum(ratio for _, ratio in data) / len(data)
# 计算尺寸
dimensions = calculate_dimensions(shape, avg_area)
print(f"\n{shape}:")
print(f" 平均面积: {avg_area:.2f} mm²")
print(f" 平均占空比: {avg_ratio:.2f}%(占整张纸面积)")
print(f" {dimensions}")
print(f" 检测到的次数: {len(data)}")
finally:
cap.release()
cv2.destroyAllWindows()
print("\n程序已退出")
用以上代码代替下面代码的可实现功能部分import cv2
import numpy as np
import platform
import Config
from Config import * # noqa: F403
class A4GeometryDetector:
def __init__(self):
self.object_3d_points = np.array(
[
[-A4_WIDTH_MM / 2, -A4_HEIGHT_MM / 2, 0],
[A4_WIDTH_MM / 2, -A4_HEIGHT_MM / 2, 0],
[A4_WIDTH_MM / 2, A4_HEIGHT_MM / 2, 0],
[-A4_WIDTH_MM / 2, A4_HEIGHT_MM / 2, 0],
],
dtype=np.float32,
)
self.camera_matrix = np.array(
Config.camera_matrix,
dtype=np.float32,
)
self.dist_coeffs = np.array(
Config.dist_coeffs,
dtype=np.float32,
)
self.try_open_camera()
self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
print(self.frame_width, self.frame_height)
self.camera_matrix[0, 2] = self.frame_width / 2
self.camera_matrix[1, 2] = self.frame_height / 2
self.pixel_per_mm = 1
self.edges = None
self.finalWarped = None
# 分辨率和id设置
def try_open_camera(self, camera_id=1):
if platform.system().lower() == "windows":
cap = cv2.VideoCapture(camera_id, cv2.CAP_DSHOW)
else:
cap = cv2.VideoCapture(camera_id)
# 或者设置为你想要的分辨率,比如640x480
cap.set(3, 640)
cap.set(4, 480)
if cap.isOpened():
print(f"成功打开摄像头: {camera_id}")
self.cap = cap
return True
else:
cap.release()
print("未找到可用的摄像头")
return False
def detect_corners(self, frame):
def angle_sort(pt):
vec = pt - center
return np.arctan2(vec[1], vec[0])
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
ret, binary_img = cv2.threshold(blurred, 127, 255, cv2.THRESH_BINARY)
edges = cv2.Canny(binary_img, threshold1=50, threshold2=150)
contours, _ = cv2.findContours(
edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
# 存储符合条件的轮廓及其面积
valid_contours = []
for cnt in contours:
epsilon = 0.01 * cv2.arcLength(cnt, closed=True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
if len(approx) == 4 and cv2.isContourConvex(approx):
area = cv2.contourArea(approx)
if area < 1000: # 可选:面积过滤
continue
corners = approx.reshape(4, 2)
valid_contours.append((area, corners))
# 如果没有找到符合条件的轮廓
if not valid_contours:
return None
# 按面积从大到小排序
valid_contours.sort(key=lambda x: x[0], reverse=True)
# 选择面积最大的轮廓
largest_area, corners = valid_contours[0]
# 对角点进行排序
center = corners.mean(axis=0)
sorted_corners = sorted(corners, key=angle_sort)
start_idx = np.argmin([pt[1] for pt in sorted_corners])
ordered = np.roll(sorted_corners, -start_idx, axis=0)
return np.array(ordered, dtype=np.float32)
def order_points(self, pts):
rect = np.zeros((4, 2), dtype=np.float32)
s = pts.sum(axis=1)
diff = np.diff(pts, axis=1)
rect[0] = pts[np.argmin(s)]
rect[2] = pts[np.argmax(s)]
rect[1] = pts[np.argmin(diff)]
rect[3] = pts[np.argmax(diff)]
return rect
def calculate_distance_pnp(self, image_points):
if image_points is None or len(image_points) != 4:
return None, None, None
ordered_points = self.order_points(image_points)
try:
success, rvec, tvec = cv2.solvePnP(
self.object_3d_points,
ordered_points,
self.camera_matrix,
self.dist_coeffs,
)
if success:
horizontal_baseline_distance = float(
np.sqrt(tvec[0][0] ** 2 + tvec[2][0] ** 2)
)
return horizontal_baseline_distance, rvec, tvec
except:
return None, None, None
return None, None, None
def warp_perspective_from_corners(self, frame, corners, output_size=(600, 400)):
if corners is None or len(corners) != 4:
return None, None, None
rect = self.order_points(corners)
dst = np.array(
[
[0, 0],
[output_size[0] - 1, 0],
[output_size[0] - 1, output_size[1] - 1],
[0, output_size[1] - 1],
],
dtype=np.float32,
)
M = cv2.getPerspectiveTransform(rect, dst)
Minv = cv2.getPerspectiveTransform(dst, rect)
warped = cv2.warpPerspective(frame, M, output_size)
return warped, M, Minv
def resize_warped(self):
up_points = (int(A4_WIDTH_MM * 3), int(A4_HEIGHT_MM * 3))
resized_up = cv2.resize(self.warped, up_points, interpolation=cv2.INTER_LINEAR)
self.pixel_per_mm = up_points[0] / A4_WIDTH_MM
return resized_up
def pixels_to_mm(self, pixels):
return pixels / self.pixel_per_mm
def mm_to_pixels(self, mm):
return mm * self.pixel_per_mm
def run(self):
ret, frame = self.cap.read()
if not ret:
return
self.frame = frame
self.corners = self.detect_corners(frame)
if self.corners is None:
return
self.distance, rvec, tvec = self.calculate_distance_pnp(self.corners)
if self.distance is None:
return
self.warped, M, Minv = self.warp_perspective_from_corners(frame, self.corners)
if self.warped is None:
return
self.finalWarped = self.resize_warped()
gray = cv2.cvtColor(self.finalWarped, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (3, 3), 0)
ret, binary_img = cv2.threshold(blurred, 127, 255, cv2.THRESH_BINARY)
self.edges = cv2.Canny(binary_img, 50, 150)
# 遮盖多余的边框线
edge_bored_width = int(self.mm_to_pixels(BORDER_WIDTH_MM)) + 5
# Top border
self.edges[0:edge_bored_width, :] = 0
# Bottom border
self.edges[-edge_bored_width:, :] = 0
# Left border
self.edges[:, 0:edge_bored_width] = 0
# Right border
self.edges[:, -edge_bored_width:] = 0
# 绘制角点
for i, corner in enumerate(self.corners):
cv2.circle(frame, tuple(corner.astype(int)), 5, (255, 0, 0), -1)
cv2.putText(
frame,
str(i),
tuple(corner.astype(int) + 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(255, 255, 0),
1,
)
# 显示距离
cv2.putText(
frame,
f"A4 Distance: {self.distance:.1f}mm ({self.distance/10:.1f}cm)",
(10, 30),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(0, 0, 255),
2,
)
class OverlayModelDetector:
def __init__(self, geo: A4GeometryDetector):
self.geo = geo
def extend_edge_endpoints(self, p1, p2, extension_length=30):
"""
延申边的两个端点
Args:
p1, p2: 边的两个端点
extension_length: 延申长度(像素)
Returns:
tuple: (延申后的p1, 延申后的p2)
"""
# 计算边的方向向量
edge_vector = np.array(p2) - np.array(p1)
edge_length = np.linalg.norm(edge_vector)
if edge_length == 0:
return p1, p2
# 单位方向向量
unit_vector = edge_vector / edge_length
# 延申端点
extended_p1 = np.array(p1) - unit_vector * extension_length
extended_p2 = np.array(p2) + unit_vector * extension_length
return extended_p1.astype(int), extended_p2.astype(int)
def is_point_in_contour(self, point, contour):
"""
使用射线投射算法检测点是否在轮廓内
Args:
point: 要检查的点 (x, y)
contour: OpenCV轮廓
Returns:
bool: True如果点在轮廓内
"""
x, y = point[0], point[1]
contour_points = contour.reshape(-1, 2)
n = len(contour_points)
inside = False
p1x, p1y = contour_points[0]
for i in range(1, n + 1):
p2x, p2y = contour_points[i % n]
if y > min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xinters:
inside = not inside
p1x, p1y = p2x, p2y
return inside
def get_edge_perpendicular_directions(self, p1, p2):
"""
获取边的两个垂直方向的单位向量
Args:
p1, p2: 边的端点
Returns:
tuple: (方向1的单位向量, 方向2的单位向量)
"""
# 边的方向向量
edge_vector = np.array(p2) - np.array(p1)
edge_length = np.linalg.norm(edge_vector)
if edge_length == 0:
return None, None
# 垂直向量(顺时针旋转90度)
perp_vector1 = np.array([edge_vector[1], -edge_vector[0]])
perp_vector1 = perp_vector1 / np.linalg.norm(perp_vector1)
# 另一个垂直向量(逆时针旋转90度)
perp_vector2 = -perp_vector1
return perp_vector1, perp_vector2
def create_square_from_edge(self, p1, p2, direction_vector):
"""
根据边和方向创建正方形
Args:
p1, p2: 边的端点
direction_vector: 垂直方向的单位向量
Returns:
np.array: 正方形的四个顶点,如果无法创建则返回None
"""
edge_vector = np.array(p2) - np.array(p1)
edge_length = np.linalg.norm(edge_vector)
if edge_length == 0:
return None
# 计算正方形的另外两个顶点
p3 = np.array(p2) + direction_vector * edge_length
p4 = np.array(p1) + direction_vector * edge_length
# 返回正方形的四个顶点(按顺序)
square = np.array([p1, p2, p3, p4], dtype=np.int32)
return square.reshape(-1, 1, 2) # OpenCV轮廓格式
def is_special_edge(self, p1, p2, contour, extension_length=15):
"""
检查是否为特殊边(延申后的端点都不在轮廓内)
Args:
p1, p2: 边的端点
contour: 轮廓
extension_length: 延申长度
Returns:
bool: True如果是特殊边
"""
extended_p1, extended_p2 = self.extend_edge_endpoints(p1, p2, extension_length)
# 检查延申后的端点是否都不在轮廓内
p1_outside = not self.is_point_in_contour(extended_p1, contour)
p2_outside = not self.is_point_in_contour(extended_p2, contour)
return p1_outside and p2_outside
def find_valid_square_for_edge(self, p1, p2, contour):
"""
为特殊边找到有效的正方形假设
Args:
p1, p2: 边的端点
contour: 轮廓
Returns:
np.array or None: 有效的正方形顶点,如果找不到则返回None
"""
# 获取两个垂直方向
dir1, dir2 = self.get_edge_perpendicular_directions(p1, p2)
if dir1 is None or dir2 is None:
return None
# 尝试两个方向
for direction in [dir1, dir2]:
square = self.create_square_from_edge(p1, p2, direction)
if square is None:
continue
# 计算正方形质心
square_points = square.reshape(-1, 2)
centroid = np.mean(square_points, axis=0)
# 检查质心是否在轮廓内
if self.is_point_in_contour(centroid, contour):
return square
return None
def detect(self):
"""
重构后的检测方法
"""
# max_edge_length = self.geo.mm_to_pixels(130)
min_edge_length = self.geo.mm_to_pixels(50)
extension_delta = self.geo.mm_to_pixels(15)
center_thresh = 30
# 获取轮廓
contours, _ = cv2.findContours(
self.geo.edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
# 绘制所有轮廓用于调试
# cv2.drawContours(
# self.geo.finalWarped, contours, -1, (0, 255, 0), thickness=cv2.FILLED
# )
# cv2.imshow("con", self.geo.finalWarped)
if not contours:
return []
results = []
seen_squares = []
id_counter = 1
for cnt in contours:
# 获取轮廓的近似多边形
epsilon = 0.01 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
if len(approx) < 4:
continue
points = [p[0] for p in approx]
n = len(points)
# 检查每条边
for i in range(n):
p1 = points[i]
p2 = points[(i + 1) % n]
# 计算边长
edge_length = np.linalg.norm(np.array(p2) - np.array(p1))
# 过滤不符合长度要求的边
if edge_length < min_edge_length:
continue
# 检查是否为特殊边
if not self.is_special_edge(p1, p2, cnt, extension_delta):
continue
# 为特殊边找到有效的正方形
square = self.find_valid_square_for_edge(p1, p2, cnt)
if square is None:
continue
# 检查是否已经检测到相似的正方形
square_points = square.reshape(-1, 2)
square_center = np.mean(square_points, axis=0)
is_duplicate = False
for prev_center in seen_squares:
if np.linalg.norm(prev_center - square_center) < center_thresh:
is_duplicate = True
break
if is_duplicate:
continue
# 记录检测到的正方形
seen_squares.append(square_center)
# 绘制特殊边和假设的正方形
cv2.line(self.geo.finalWarped, tuple(p1), tuple(p2), (255, 0, 0), 3)
cv2.polylines(
self.geo.finalWarped,
[square],
isClosed=True,
color=(255, 0, 0),
thickness=2,
)
# 绘制正方形质心
cv2.circle(
self.geo.finalWarped,
tuple(square_center.astype(int)),
3,
(255, 255, 0),
-1,
)
# 添加标签
edge_mid = ((p1[0] + p2[0]) // 2, (p1[1] + p2[1]) // 2)
cv2.putText(
self.geo.finalWarped,
f"#{id_counter}: {self.geo.pixels_to_mm(edge_length):.1f}mm",
edge_mid,
cv2.FONT_HERSHEY_SIMPLEX,
1.8,
(0, 0, 255),
1,
cv2.LINE_AA,
)
results.append((id_counter, edge_length, square_center))
id_counter += 1
return results
class NormalModelDetector:
def __init__(self, geo: A4GeometryDetector):
self.geo = geo
def detect(self):
# 寻找轮廓
contours, _ = cv2.findContours(
self.geo.edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE
)
# 过滤并分析轮廓
for contour in sorted(contours, key=cv2.contourArea, reverse=True):
# 分析几何形状
shape_type, size_pixels, shape_contour = self.analyze_shape(contour)
if shape_type and size_pixels:
# 将像素尺寸转换为实际尺寸
size_mm = self.geo.pixels_to_mm(size_pixels)
# 绘制检测到的几何图形
cv2.drawContours(
self.geo.finalWarped, [shape_contour], -1, (255, 0, 255), 3
)
shape_info = self.format_shape_info(shape_type, size_mm)
cv2.putText(
self.geo.finalWarped,
shape_info,
(10, 70),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(255, 0, 255),
2,
)
return shape_type, size_mm, shape_contour
return None, None, None
def analyze_shape(self, contour):
"""分析轮廓的几何形状"""
# 计算轮廓特征
area = cv2.contourArea(contour)
perimeter = cv2.arcLength(contour, True)
if perimeter == 0:
return None, None, None
# 多边形逼近
epsilon = 0.02 * perimeter
approx = cv2.approxPolyDP(contour, epsilon, True)
vertices = len(approx)
# 圆形检测
circularity = 4 * np.pi * area / (perimeter**2)
if circularity > 0.85: # 圆形
# 计算最小外接圆
(x, y), radius = cv2.minEnclosingCircle(contour)
diameter_pixels = radius * 2
return "circle", diameter_pixels, contour
elif vertices == 3: # 三角形
# 检查是否为等边三角形
if self.is_equilateral_triangle(approx):
# 计算边长(取三边的平均值)
side_lengths = []
for i in range(3):
p1 = approx[i][0]
p2 = approx[(i + 1) % 3][0]
side_length = np.linalg.norm(p1 - p2)
side_lengths.append(side_length)
avg_side_length = np.mean(side_lengths)
return "equilateral_triangle", avg_side_length, contour
elif vertices == 4: # 四边形
# 检查是否为正方形
if self.is_square(approx):
# 计算边长
side_lengths = []
for i in range(4):
p1 = approx[i][0]
p2 = approx[(i + 1) % 4][0]
side_length = np.linalg.norm(p1 - p2)
side_lengths.append(side_length)
avg_side_length = np.mean(side_lengths)
return "square", avg_side_length, contour
return None, None, None
def is_equilateral_triangle(self, triangle_points):
"""检查是否为等边三角形"""
if len(triangle_points) != 3:
return False
# 计算三条边的长度
sides = []
for i in range(3):
p1 = triangle_points[i][0]
p2 = triangle_points[(i + 1) % 3][0]
side_length = np.linalg.norm(p1 - p2)
sides.append(side_length)
# 检查三边长度是否接近相等(允许10%的误差)
max_side = max(sides)
min_side = min(sides)
if max_side == 0:
return False
ratio = min_side / max_side
return ratio > 0.9 # 90%相似度认为是等边三角形
def is_square(self, quad_points):
"""检查是否为正方形"""
if len(quad_points) != 4:
return False
# 计算四条边的长度
sides = []
for i in range(4):
p1 = quad_points[i][0]
p2 = quad_points[(i + 1) % 4][0]
side_length = np.linalg.norm(p1 - p2)
sides.append(side_length)
# 检查四边长度是否接近相等
max_side = max(sides)
min_side = min(sides)
if max_side == 0:
return False
side_ratio = min_side / max_side
# 检查对角线长度是否相等
diag1 = np.linalg.norm(quad_points[0][0] - quad_points[2][0])
diag2 = np.linalg.norm(quad_points[1][0] - quad_points[3][0])
if max(diag1, diag2) == 0:
return False
diag_ratio = min(diag1, diag2) / max(diag1, diag2)
# 检查角度是否接近90度
angles_ok = self.check_right_angles(quad_points)
return side_ratio > 0.9 and diag_ratio > 0.9 and angles_ok
def check_right_angles(self, quad_points):
"""检查四边形的角度是否接近90度"""
points = quad_points.reshape(4, 2)
for i in range(4):
p1 = points[i]
p2 = points[(i + 1) % 4]
p3 = points[(i + 2) % 4]
# 计算向量
v1 = p1 - p2
v2 = p3 - p2
# 计算角度
dot_product = np.dot(v1, v2)
norms = np.linalg.norm(v1) * np.linalg.norm(v2)
if norms == 0:
continue
cos_angle = dot_product / norms
cos_angle = np.clip(cos_angle, -1, 1)
angle = np.arccos(cos_angle)
angle_degrees = np.degrees(angle)
# 检查是否接近90度(允许15度误差)
if not (75 <= angle_degrees <= 105):
return False
return True
def format_shape_info(self, shape_type, size_mm):
"""格式化几何图形信息"""
if shape_type == "circle":
return f"Circle: D={size_mm:.1f}mm"
elif shape_type == "equilateral_triangle":
return f"Eq.Triangle: Side={size_mm:.1f}mm"
elif shape_type == "square":
return f"Square: Side={size_mm:.1f}mm"
return f"Shape: {size_mm:.1f}mm"
def main():
geoDetector = A4GeometryDetector()
overlayDetector = OverlayModelDetector(geoDetector)
normalDetector = NormalModelDetector(geoDetector)
while True:
cv2.waitKey(1)
geoDetector.run()
results = overlayDetector.detect() # (id_counter, edge_length, square_center)
# shape_type, size_mm, shape_contour = normalDetector.detect()
cv2.imshow("Raw", geoDetector.frame)
if geoDetector.edges is not None:
cv2.imshow("Edges", geoDetector.edges)
if geoDetector.finalWarped is not None:
cv2.imshow("Warped", geoDetector.finalWarped)
if __name__ == "__main__":
main()
最新发布