import cv2
import numpy as np
import platform
from collections collections
import defaultdict
import time
# 纸张物理参数(毫米)
PAPER_WIDTH_MM = 170.0
PAPER_HEIGHT_MM = 257.0
BORDER_WIDTH_MM = 20.0 # 边框宽度
class PaperGeometryDetector:
def __init__(self):
# 3D参考点(纸张四角,以中心为原点)
self.object_3d_points = np.array(
[
[-PAPER_WIDTH_MM / 2, -PAPER_HEIGHT_MM / 2, 0],
[PAPER_WIDTH_MM / 2, -PAPER_HEIGHT_MM / 2, 0],
[PAPER_WIDTH_MM / 2, PAPER_HEIGHT_MM / 2, 0],
[-PAPER_WIDTH_MM / 2, PAPER_HEIGHT_MM / 2, 0],
],
dtype=np.float32,
)
# 摄像头内参(可根据实际校准修改)
self.camera_matrix = np.array(
[[640, 0, 320],
[0, 640, 240],
[0, 0, 1]],
dtype=np.float32
)
self.dist_coeffs = np.zeros((5, 1), dtype=np.float32) # 畸变系数
self.try_open_camera()
self.frame_width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) if hasattr(self, 'cap') else 640
self.frame_height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) if hasattr(self, 'cap') else 480
# 调整内参以匹配实际分辨率
self.camera_matrix[0, 2] = self.frame_width / 2
self.camera_matrix[1, 2] = self.frame_height / 2
self.pixel_per_mm = 1.0 # 像素-毫米转换比例
self.distance = 0.0 # 摄像头到纸张的距离(毫米)
self.edges = None
self.finalWarped = None
self.frame = None
self.corners = None
self.warped = None
def try_open_camera(self, camera_id=0):
"""打开摄像头并设置分辨率"""
if platform.system().lower() == "windows":
cap = cv2.VideoCapture(camera_id, cv2.CAP_DSHOW)
else:
cap = cv2.VideoCapture(camera_id)
# 设置分辨率(640x480兼容大多数摄像头)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
if cap.isOpened():
print(f"成功打开摄像头: {camera_id},分辨率: 640x480")
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_INV) # 反相阈值,突出白色纸张
edges = cv2.Canny(binary_img, 50, 150)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
valid_contours = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area < 5000: # 过滤小轮廓
continue
epsilon = 0.02 * cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, epsilon, True)
if len(approx) == 4 and cv2.isContourConvex(approx): # 四边形形且凸多边形
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):
"""通过PNP算法计算摄像头到纸张的距离(毫米)"""
if image_points is None or len(image_points) != 4:
return 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:
# 距离为平移向量的模长
distance = np.linalg.norm(tvec)
return distance
except Exception as e:
print(f"PNP计算失败: {e}")
return None
def warp_perspective_from_corners(self, frame, corners,
output_size=(int(PAPER_WIDTH_MM * 2), int(PAPER_HEIGHT_MM * 2))):
"""透视变换矫正纸张区域"""
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(PAPER_WIDTH_MM * 3), int(PAPER_HEIGHT_MM * 3)) # 按纸张尺寸放大3倍
resized_up = cv2.resize(self.warped, up_points, interpolation=cv2.INTER_LINEAR)
self.pixel_per_mm = up_points[0] / PAPER_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):
"""执行纸张检测和预处理"""
if not hasattr(self, 'cap'):
return False
ret, frame = self.cap.read()
if not ret:
return False
self.frame = cv2.flip(frame, 1) # 镜像翻转,便于操作
self.corners = self.detect_corners(self.frame)
if self.corners is None:
return False
# 计算摄像头到纸张的距离(毫米)
self.distance = self.calculate_distance_pnp(self.corners)
if self.distance is None:
return False
# 透视变换并缩放
self.warped, M, Minv = self.warp_perspective_from_corners(self.frame, self.corners)
if self.warped is None:
return False
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_INV)
self.edges = cv2.Canny(binary_img, 50, 150)
# 遮盖边框区域(避免干扰)
edge_border = int(self.mm_to_pixels(BORDER_WIDTH_MM)) + 5
self.edges[:edge_border, :] = 0 # 顶部
self.edges[-edge_border:, :] = 0 # 底部
self.edges[:, :edge_border] = 0 # 左侧
self.edges[:, -edge_border:] = 0 # 右侧
# 绘制角点和距离信息
for i, corner in enumerate(self.corners):
cv2.circle(self.frame, tuple(corner.astype(int)), 5, (0, 255, 0), -1)
cv2.putText(self.frame, f"Corner {i}", tuple(corner.astype(int) + 10),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 255), 1)
# 显示距离(转换为厘米)
cv2.putText(self.frame, f"纸张距离: {self.distance / 10:.1f}cm", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
return True
class ShapeDetector:
"""形状检测与尺寸测量(含矩形扩展与内部边框识别)"""
def __init__(self, geo):
self.geo = geo
self.detection_history = defaultdict(list) # 存储检测历史
self.expand_pixels = 8 # 矩形向外扩展的像素数(可根据需要调整)
self.inner_borders = [] # 存储检测到的内部边框
def detect_inner_borders(self, roi):
"""检测ROI区域内的内部边框"""
# 对ROI区域进行边缘检测
gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
blurred_roi = cv2.GaussianBlur(gray_roi, (3, 3), 0)
edges_roi = cv2.Canny(blurred_roi, 50, 150)
# 查找内部轮廓
inner_contours, _ = cv2.findContours(edges_roi, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
inner_borders = []
for cnt in inner_contours:
area = cv2.contourArea(cnt)
if 50 < area < 10000: # 过滤过小或过大的轮廓
peri = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
if len(approx) == 4: # 只保留四边形边框
inner_borders.append(approx)
return inner_borders
def expand_rectangle(self, x, y, w, h, img_shape):
"""将矩形向外扩展指定像素,确保不超出图像边界"""
x_expanded = max(0, x - self.expand_pixels)
y_expanded = max(0, y - self.expand_pixels)
w_expanded = min(img_shape[1] - x_expanded, w + 2 * self.expand_pixels)
h_expanded = min(img_shape[0] - y_expanded, h + 2 * self.expand_pixels)
return x_expanded, y_expanded, w_expanded, h_expanded
def detect_shapes(self):
"""检测纸张上的形状并测量尺寸(增强矩形处理)"""
if self.geo.edges is None or self.geo.finalWarped is None:
return []
contours, _ = cv2.findContours(self.geo.edges.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
detected_shapes = []
img_shape = self.geo.finalWarped.shape
self.inner_borders = [] # 重置内部边框列表
for cnt in contours:
area_px = cv2.contourArea(cnt)
if area_px < 100: # 过滤小轮廓
continue
# 形状识别
peri = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.03 * peri, True)
vertices = len(approx)
shape = "unknown"
size_mm = 0.0
# 三角形(3条边)
if vertices == 3:
shape = "triangle"
# 计算边长(取平均值)
sides = []
for i in range(3):
p1 = approx[i][0]
p2 = approx[(i + 1) % 3][0]
side_px = np.linalg.norm(p1 - p2)
sides.append(self.geo.pixels_to_mm(side_px))
size_mm = np.mean(sides) # 平均边长
# 四边形(4条边)
elif vertices == 4:
x, y, w, h = cv2.boundingRect(approx)
aspect_ratio = w / h
# 扩展矩形边界并检测内部边框
x_exp, y_exp, w_exp, h_exp = self.expand_rectangle(x, y, w, h, img_shape)
roi = self.geo.finalWarped[y_exp:y_exp + h_exp, x_exp:x_exp + w_exp]
inner_borders = self.detect_inner_borders(roi)
# 存储内部边框(调整坐标到原始图像)
for border in inner_borders:
border_shifted = border + np.array([[x_exp, y_exp]], dtype=np.int32)
self.inner_borders.append(border_shifted)
# 区分正方形和矩形
if 0.95 <= aspect_ratio <= 1.05:
shape = "square"
else:
shape = "rectangle"
# 计算边长(取平均值)
sides = []
for i in range(4):
p1 = approx[i][0]
p2 = approx[(i + 1) % 4][0]
side_px = np.linalg.norm(p1 - p2)
sides.append(self.geo.pixels_to_mm(side_px))
size_mm = np.mean(sides) # 平均边长
# 圆形(通过圆度判断)
else:
circularity = 4 * np.pi * area_px / (peri ** 2) if peri > 0 else 0
if 0.7 < circularity < 1.3:
shape = "circle"
# 计算直径(最小外接圆)
(x, y), radius_px = cv2.minEnclosingCircle(cnt)
size_mm = self.geo.pixels_to_mm(radius_px * 2) # 直径
# 只保留有效形状
if shape in ["triangle", "square", "rectangle", "circle"]:
detected_shapes.append((shape, size_mm, cnt))
# 记录历史数据(用于后续平均计算)
self.detection_history[shape].append(size_mm)
return detected_shapes
def draw_detections(self, shapes):
"""在图像上绘制检测结果"""
if self.geo.finalWarped is None:
return
# 绘制形状
for shape, size_mm, cnt in shapes:
# 绘制轮廓
cv2.drawContours(self.geo.finalWarped, [cnt], -1, (0, 255, 0), 2)
# 计算中心坐标
M = cv2.moments(cnt)
if M["m00"] == 0:
continue
cX = int(M["m10"] / M["m00"])
cY = int(M["m01"] / M["m00"])
# 显示形状和尺寸
label = f"{shape}: {size_mm:.1f}mm"
if shape == "circle":
label = f"{shape} (直径): {size_mm:.1f}mm"
cv2.putText(self.geo.finalWarped, label, (cX - 50, cY),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
# 绘制扩展区域和内部边框
for border in self.inner_borders:
cv2.drawContours(self.geo.finalWarped, [border], -1, (255, 0, 0), 2) # 蓝色框标记内部边框
def print_stats(self, duration=3):
"""打印指定时间内的检测统计结果"""
print(f"\n===== {duration}秒检测统计 =====")
print(f"纸张尺寸: {PAPER_WIDTH_MM}mm x {PAPER_HEIGHT_MM}mm")
print(f"摄像头到纸张距离: {self.geo.distance / 10:.1f}cm")
if not self.detection_history:
print("未检测到有效形状")
return
for shape, sizes in self.detection_history.items():
avg_size = np.mean(sizes)
print(f"\n{shape}:")
print(f" 平均尺寸: {avg_size:.1f}mm")
print(f" 检测次数: {len(sizes)}")
print("================================\n")
def main():
geo = PaperGeometryDetector()
if not hasattr(geo, 'cap'): # 检查摄像头是否成功打开
return
shape_detector = ShapeDetector(geo)
detection_duration = 3 # 统计时长(秒)
start_time = time.time()
print(f"程序启动,将在{detection_duration}秒后输出统计结果(按'q'退出)")
while True:
if cv2.waitKey(1) & 0xFF == ord('q'):
break
# 执行纸张检测
success = geo.run() if hasattr(geo, 'run') else False
if not success:
# 显示未检测到纸张的提示
if hasattr(geo, 'frame') and geo.frame is not None:
cv2.putText(geo.frame, "未检测到纸张", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
cv2.imshow("原始图像", geo.frame)
continue
# 检测形状并绘制结果
shapes = shape_detector.detect_shapes()
shape_detector.draw_detections(shapes)
# 显示图像
cv2.imshow("原始图像", geo.frame)
cv2.imshow("矫正后纸张", geo.finalWarped)
cv2.imshow("边缘检测", geo.edges)
# 超时后输出统计结果并重置
if time.time() - start_time >= detection_duration:
shape_detector.print_stats(detection_duration)
shape_detector.detection_history.clear() # 清空历史数据
start_time = time.time()
# 释放资源
if hasattr(geo, 'cap'):
geo.cap.release()
cv2.destroyAllWindows()
print("程序已退出")
if __name__ == "__main__":
main()
最新发布