跳转至

2025C

2025电赛C题

image-20260131203239443

装置以上图为示例(此处仅做视觉思路讨论)

一、设计思路&方案

1、相似三角形

本方案主要基于相似三角形原理与计算机视觉几何算法。 首先,设定 A4 纸为标准物理参照锚点,经摄像头标定获取焦距参数后,通过提取参照物轮廓及计算像素宽度,利用相似三角形公式(D = F * W / P)解算摄像头与目标平面的垂直距离 ;其次,引入透视变换(Perspective Transform)算法校正非垂直拍摄产生的几何畸变,将图像重映射为标准正视图,确立“像素-物理尺寸”的线性对应关系 ;最后,在矫正平面内运用多边形逼近与圆形度分析算法,实现对正方形、三角形及圆形的精准分类与物理尺寸换算 。

2、相机标定

本方案基于针孔成像模型与多坐标系变换理论,旨在建立二维像素平面与三维物理世界之间的严格数学映射 。其核心设计思路主要包含三个步骤:首先是参数解算与标定,系统需预先拍摄多张不同角度的棋盘格标定板图像,通过算法解算摄像头的内参矩阵(包含焦距 f_x, f_y及主点坐标)与畸变系数(径向与切向畸变),同时确定摄像头相对于测量平面的外参矩阵(旋转向量R与平移向量T)。其次是逆向投影与坐标重构,在测量阶段利用畸变系数对原始图像进行去畸变处理,消除广角镜头产生的桶形或枕形形变,随后基于投影矩阵的逆变换,将目标特征点的像素坐标 \((u, v)\) 反向映射回世界坐标系 (X_w, Y_w, Z_w) 。最后是物理量化计算,在重构的世界坐标系中计算特征点间的欧氏距离,直接输出目标的实际物理尺寸与空间距离,从而实现精密测量 。然而,其缺陷在于对系统刚性要求极高:外参矩阵高度依赖摄像头与地面的固定相对位姿,一旦摄像头发生微小位移或振动,或环境温度导致镜头光路变化,均需重新繁琐标定。这使得该方案在动态或非实验室环境下显得不够灵活,难以满足快速部署的需求 。

因此,在现实条件下,我们最终采用的是相似三角形原理 的思路

二、代码设计

# sum.py

import cv2
import numpy as np
import enum
import math
import juzhenkey
import serial.tools.list_ports
import RPi.GPIO as GPIO
from ina226 import INA226

class ContourType(enum.Enum):
    Nothing = 1,
    Basic_shapes = 2,  # 合并正方形和三角形检测
    Basic_circle = 3,
    Extension_Two_and_One = 5,
    Extension_Rotate = 6,
    Distance_Measure = 7  # 单独测距离模式
    Nested_Rectangle = 8


### A4边框识别和测距
def calculate_pixel_width(contour):
    """使用最小外接接矩形计算短边"""
    rect = cv2.minAreaRect(contour)
    width, height = rect[1]
    distance = min(width, height)
    return distance  # 返回短边长度


def detect_a4_contour(image):
    """检测A4纸轮廓,返回轮廓、阈值图像和A4纸像素面积"""
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # 自适应阈值处理 - 增强黑色边框
    thresh = cv2.adaptiveThreshold(
        gray, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY_INV, 77, 10
    )
    thresh_copy = thresh.copy()
    kernel = np.ones((3, 3), np.uint8)
    mask = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)

    contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if DEBUG_MODE:
        print(f"检测到 {len(contours)} 个轮廓")
    # 筛选A4纸轮廓(最大轮廓+四边形判断)
    if contours:
        contours = sorted(contours, key=cv2.contourArea, reverse=True)
        if DEBUG_MODE:
            print(f"检测到contours  {len(contours)} 个轮廓")
        for cnt in contours:
            area = cv2.contourArea(cnt)
            if DEBUG_MODE:
                print(f"检测到 A4 轮廓 {area:.2f} 面积")
            if area < MIN_CONTOUR_AREA:
                continue

            # 多边形逼近
            peri = cv2.arcLength(cnt, True)
            approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
            if DEBUG_MODE:
                print(f"len(approx){len(approx)}")
            # 四边形检测
            if len(approx) == 4:
                if DEBUG_MODE:
                    print(f"approx{approx}")
                return approx, thresh_copy, area  # 返回A4纸像素面积
    return None, thresh_copy, 0


def calculate_distance(focal_length, pixel_width):
    """计算目标物距离D"""
    return (focal_length * A4_REAL_WIDTH) / pixel_width


### 透视变换
def perspective_transform(image, contour):
    """将A4纸区域透视变换为标准矩形"""
    # 将轮廓点排序为(左上、右上、右下、左下)
    pts = contour.reshape(4, 2)
    rect = np.zeros((4, 2), dtype="float32")

    # 计算轮廓点中心
    center = np.mean(pts, axis=0)

    # 根据点与中心的相对位置排序
    for point in pts:
        if point[0] < center[0] and point[1] < center[1]:
            rect[0] = point  # 左上
        elif point[0] > center[0] and point[1] < center[1]:
            rect[1] = point  # 右上
        elif point[0] > center[0] and point[1] > center[1]:
            rect[2] = point  # 右下
        else:
            rect[3] = point  # 左下

    # 计算目标标矩形尺寸(保持A4比例)
    width = max(
        np.linalg.norm(rect[0] - rect[1]),
        np.linalg.norm(rect[2] - rect[3])
    )
    height = max(
        np.linalg.norm(rect[0] - rect[3]),
        np.linalg.norm(rect[1] - rect[2])
    )

    # 创建目标点
    dst = np.array([
        [0, 0],
        [width - 1, 0],
        [width - 1, height - 1],
        [0, height - 1]
    ], dtype="float32")

    # 计算变换矩阵
    M = cv2.getPerspectiveTransform(rect, dst)

    # 应用透视变换
    warped = cv2.warpPerspective(image, M, (int(width), int(height)))

    return warped


def calculate_rotation(contour):
    """计算形状的旋转角度(内部使用,不对外输出)"""
    rect = cv2.minAreaRect(contour)
    angle = rect[2]

    # 优化角度计算,确保范围在0-90度之间
    if angle < -45:
        angle += 90
    angle = abs(angle)
    if angle > 90:
        angle = 180 - angle

    return angle


### 形状检测函数
def detect_squares_in_a4(warped, a4_pixel_area):
    """检测正方形,使用面积比例例计算边长"""
    inverted_gray = cv2.bitwise_not(warped)
    _, binary_img = cv2.threshold(inverted_gray, 160, 255, cv2.THRESH_BINARY)
    contours, _ = cv2.findContours(binary_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
    squares = []
    for cnt in contours:
        peri = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, 0.02 * peri, True)
        if len(approx) == 4 and is_square(approx):
            # 计算正方形像素面积(白色部分)
            square_pixel_area = cv2.contourArea(approx)
            if square_pixel_area < 2:
                continue

            # 核心:通过面积比例计算真实面积
            if a4_pixel_area > 0:
                area_ratio = square_pixel_area / a4_pixel_area
                square_real_area = A4_REAL_AREA * area_ratio
                square_real_side = math.sqrt(square_real_area)
            else:
                # 回退方案:原有比例方法
                square_real_side = calculate_square_size_using_a4_ratio(
                    calculate_side_length(approx), warped)

            M = cv2.moments(cnt)
            cX = int(M["m10"] / M["m00"])
            cY = int(M["m01"] / M["m00"])
            squares.append({
                "type": "square",
                "size": square_real_side,
                "center": (cX, cY),
                "contour": approx  # 添加轮廓信息
            })

    return squares


def is_square(contour, angle_threshold=15, aspect_threshold=0.15):
    """验证是否为正方形(基于角度和边长比例)"""
    sides = []
    for i in range(4):
        pt1 = contour[i][0]
        pt2 = contour[(i + 1) % 4][0]
        sides.append(np.linalg.norm(pt2 - pt1))
    max_side, min_side = max(sides), min(sides)
    aspect_ratio = abs(max_side - min_side) / ((max_side + min_side) / 2)
    if aspect_ratio > aspect_threshold:
        return False
    for i in range(4):
        pt1 = contour[i][0]
        pt2 = contour[(i + 1) % 4][0]
        pt3 = contour[(i + 2) % 4][0]
        angle = calculate_angle(pt1, pt2, pt3)
        if not (85 <= angle <= 95):
            return False
    return True


def calculate_angle(p1, p2, p3):
    """计算三点形成的角度"""
    v1 = p1 - p2
    v2 = p3 - p2
    cos_theta = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
    # 处理数值精度问题
    cos_theta = np.clip(cos_theta, -1.0, 1.0)
    return np.degrees(np.arccos(cos_theta))


def calculate_side_length(contour):
    """计算正方形边长(像素素单位)"""
    sides = []
    for i in range(4):
        pt1 = contour[i][0]
        pt2 = contour[(i + 1) % 4][0]
        sides.append(np.linalg.norm(pt2 - pt1))
    avg_side = np.mean(sides)
    rect = cv2.minAreaRect(contour)
    min_side = min(rect[1])
    return (avg_side + min_side) / 2


def calculate_square_size_using_a4_ratio(pixel_side, warped_a4):
    """基于A4纸比例计算正方形实际尺寸的方法(作为备选)"""
    a4_pixel_height, a4_pixel_width = warped_a4.shape[:2]
    a4_pixel_ratio = a4_pixel_width / a4_pixel_height
    a4_real_ratio = A4_REAL_WIDTH / A4_REAL_HEIGHT  # 标准A4比例

    if a4_pixel_ratio > a4_real_ratio:  # 横放A4纸
        pixel_to_cm = A4_REAL_HEIGHT / a4_pixel_height  # 使用高度计算比例
    else:  # 竖放A4纸
        pixel_to_cm = A4_REAL_WIDTH / a4_pixel_width  # 使用宽度计算比例

    return pixel_side * pixel_to_cm


def detect_triangles_in_a4(warped):
    """检测三角形"""
    inverted = cv2.bitwise_not(warped)
    _, binary_img = cv2.threshold(inverted, 160, 255, cv2.THRESH_BINARY)
    contours, _ = cv2.findContours(binary_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    triangles = []
    for cnt in contours:
        peri = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, 0.04 * peri, True)
        if len(approx) == 3 and is_equilateral_triangle(approx):
            side_length = calculate_triangle_side(approx)
            pixel_side = side_length
            cm_side = (pixel_side / warped.shape[1]) * A4_REAL_WIDTH

            M = cv2.moments(cnt)
            cX = int(M["m10"] / M["m00"])
            cY = int(M["m01"] / M["m00"])

            triangles.append({
                "type": "triangle",
                "size": cm_side,
                "center": (cX, cY),
                "contour": approx  # 添加轮廓信息

            })
    return triangles


def is_equilateral_triangle(contour, angle_threshold=15, side_threshold=0.15):
    """验证等边三角形(基于角度和边长)"""
    points = contour.reshape(3, 2)
    sides = [
        np.linalg.norm(points[1] - points[0]),
        np.linalg.norm(points[2] - points[1]),
        np.linalg.norm(points[0] - points[2])
    ]
    max_side, min_side = max(sides), min(sides)
    aspect_ratio = abs(max_side - min_side) / ((max_side + min_side) / 2)
    if aspect_ratio > side_threshold:
        return False
    for i in range(3):
        a, b, c = points[i], points[(i + 1) % 3], points[(i + 2) % 3]
        angle = calculate_angle(a, b, c)
        if not (55 <= angle <= 65):
            return False
    return True


def calculate_triangle_side(contour):
    """计算三角形边长(三种方法融合)"""
    points = contour.reshape(3, 2)
    sides_direct = [
        np.linalg.norm(points[1] - points[0]),
        np.linalg.norm(points[2] - points[1]),
        np.linalg.norm(points[0] - points[2])
    ]
    avg_side = np.mean(sides_direct)
    (cx, cy), radius = cv2.minEnclosingCircle(contour)
    circum_radius = radius
    theoretical_side = circum_radius * np.sqrt(3)
    area = cv2.contourArea(contour)
    area_side = (4 * area / np.sqrt(3)) ** 0.5
    return (avg_side * 0.6 + theoretical_side * 0.3 + area_side * 0.1)


def detect_circle_in_a4(warped):
    """检测圆形"""
    inverted_gray = cv2.bitwise_not(warped)
    _, binary_img = cv2.threshold(inverted_gray, 160, 255, cv2.THRESH_BINARY)
    contours, _ = cv2.findContours(binary_img, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    MIN_AREA = 30
    MAX_AREA = 6000
    MIN_CIRCULARITY = 0.7
    MAX_CIRCULARITY = 1.3

    for contour in contours:
        area = cv2.contourArea(contour)
        perimeter = cv2.arcLength(contour, True)
        if perimeter == 0:
            continue
        circularity = (4 * np.pi * area) / (perimeter ** 2)
        if (MIN_AREA <= area <= MAX_AREA and
                MIN_CIRCULARITY <= circularity <= MAX_CIRCULARITY):
            (x, y), radius = cv2.minEnclosingCircle(contour)
            center = (int(x), int(y))
            diameter = 2 * radius
            pixel_diameter = diameter * circularity
            actual_diameter = (pixel_diameter / warped.shape[1]) * A4_REAL_WIDTH
            if actual_diameter < 9:
                continue
            return {
                "type": "circle",
                "size": actual_diameter,
                "center": (int(x), int(y))
            }
    return None


def detect_and_measure_squares(warped_a4, a4_pixel_area):
    """检测測并测量A4纸上的正方形,使用面积比例例计算边长"""
    # 确保输入转为灰度图
    if len(warped_a4.shape) == 3:
        gray = cv2.cvtColor(warped_a4, cv2.COLOR_BGR2GRAY)
    else:
        gray = warped_a4.copy()

    inverted_gray = cv2.bitwise_not(gray)
    # 阈值处理
    _, binary_img = cv2.threshold(inverted_gray, 150, 255, cv2.THRESH_BINARY)

    # 形态学操作
    kernel = np.ones((2, 2), np.uint8)
    closed1 = cv2.morphologyEx(binary_img, cv2.MORPH_CLOSE, kernel, iterations=1)
    opened = cv2.morphologyEx(closed1, cv2.MORPH_OPEN, kernel, iterations=1)

    # 寻找轮廓
    contours, _ = cv2.findContours(opened, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    squares = []

    for cnt in contours:
        # 过滤过小轮廓
        area = cv2.contourArea(cnt)
        if area < 5:
            continue

        # 轮廓近似
        peri = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, 0.03 * peri, True)

        # 检查是否为四边形
        if len(approx) == 4:
            # 计算四条边长度
            sides = []
            for i in range(4):
                x1, y1 = approx[i][0]
                x2, y2 = approx[(i + 1) % 4][0]
                length = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
                sides.append(length)

            # 检查边长比例(正方形判断)
            max_side = max(sides)
            min_side = min(sides)
            if max_side == 0:
                continue
            aspect_ratio = min_side / max_side
            if aspect_ratio < (1 - 0.25):  # 放宽的比例阈值
                continue

            # 检查角度是否接近直角
            angles = []
            for i in range(4):
                p_curr = approx[i][0]
                p_prev = approx[(i - 1) % 4][0]
                p_next = approx[(i + 1) % 4][0]

                v_prev = p_curr - p_prev
                v_next = p_curr - p_next

                dot = np.dot(v_prev, v_next)
                norm_prev = np.linalg.norm(v_prev)
                norm_next = np.linalg.norm(v_next)

                if norm_prev > 0 and norm_next > 0:
                    cos_theta = dot / (norm_prev * norm_next)
                    angle = np.abs(np.degrees(np.arccos(np.clip(cos_theta, -1.0, 1.0))))
                    angles.append(angle)

            # 角度检查
            if not angles or not all(70 < a < 110 for a in angles):
                continue

            # 计算平均边长
            side_length = np.mean(sides)

            # 面积一致性检查
            contour_area = cv2.contourArea(approx)
            expected_area = side_length ** 2
            if not (0.5 < (contour_area / expected_area) < 1.5):
                continue

            # 计算中心坐标
            M = cv2.moments(cnt)
            if M["m00"] == 0:
                continue
            cX = int(M["m10"] / M["m00"])
            cY = int(M["m01"] / M["m00"])

            # 计算实际尺寸(使用面积比例法)
            actual_size = None
            if a4_pixel_area > 0:
                area_ratio = contour_area / a4_pixel_area
                square_real_area = A4_REAL_AREA * area_ratio
                actual_size = math.sqrt(square_real_area)
            else:
                actual_size = calculate_square_size_using_a4_ratio(side_length, warped_a4)

            # 添加到结果列表
            squares.append({
                "type": "square",
                "side_length": side_length,
                "center": (cX, cY),
                "actual_size": actual_size,
                "area": contour_area,
                "contour": approx  # 添加轮廓信息
            })

    # 按面积排序
    squares.sort(key=lambda x: x["area"])
    return squares


def find_min_area_square(squares):
    """识别最小面积的正方形"""
    if not squares:
        return None
    return min(squares, key=lambda x: x["side_length"])


def convert_to_cm(pixel_length, warped_image):
    """将像素长度转换为实际厘米"""
    a4_pixel_width = warped_image.shape[1]
    pixel_to_cm_ratio = A4_REAL_WIDTH / a4_pixel_width
    return pixel_length * pixel_to_cm_ratio

# ===== FENGE功能: 内切圆辅助函数开始 =====
def find_local_maxima(dist, min_radius=5):
    """查找距离变换图中的局部最大值点"""
    dilated = cv2.dilate(dist, np.ones((3, 3), np.uint8))
    local_max = (dist == dilated)
    radius_mask = dist > min_radius
    maxima = np.where(local_max & radius_mask)
    points = list(zip(maxima[1], maxima[0]))  # (x, y)
    radii = dist[maxima]
    return points, radii

def compute_intersection(line1, line2):
    """计算两条直线的交点"""
    x1, y1, x2, y2 = line1
    x3, y3, x4, y4 = line2
    denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
    if abs(denom) < 1e-6:  # 避免除零错误
        return None
    px = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denom
    py = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denom
    # 检查交点是否在合理范围内
    if abs(px) > 10000 or abs(py) > 10000:
        return None
    return int(px), int(py)

def get_lines_and_intersections(edges, min_line_length=30, max_line_gap=10):
    """获取直线和交点"""
    lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=80,
                            minLineLength=min_line_length, maxLineGap=max_line_gap)
    intersections = []
    if lines is not None and len(lines) > 1:
        for i in range(min(len(lines), 20)):  # 限制线条数量避免过多计算
            for j in range(i + 1, min(len(lines), 20)):
                pt = compute_intersection(lines[i][0], lines[j][0])
                if pt is not None:
                    intersections.append(pt)
    return lines, intersections

def get_valid_min_inscribed_circle(img, min_area=500, min_radius=5, ratio_thresh=0.5,
                                   intersections=None, px_per_cm=100):
    """获取有效的最小内切圆"""
    _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    num_labels, labels = cv2.connectedComponents(binary)
    circles = []
    diameters = []

    if intersections is None:
        intersections = []

    for label in range(1, num_labels):
        mask = np.uint8(labels == label) * 255
        area = cv2.countNonZero(mask)
        if area < min_area:
            continue
        dist_transform = cv2.distanceTransform(mask, cv2.DIST_L2, 5)
        centers, radii = find_local_maxima(dist_transform, min_radius)
        for center, radius in zip(centers, radii):
            cx, cy = center
            # 检查是否有有效的交点,并且避免空列表或无效值
            too_close = False
            if intersections:
                try:
                    too_close = any(np.hypot(cx - ix, cy - iy) < radius * 1.2
                                    for ix, iy in intersections
                                    if ix is not None and iy is not None)
                except:
                    too_close = False
            if too_close:
                continue
            diameter = radius * 2
            diameter_cm = diameter / px_per_cm
            if diameter_cm >= 5.5:  # 只保留有效圆
                circles.append((center, int(radius)))
                diameters.append((diameter, center, int(radius)))

    valid_min_circle = None
    valid_min_diameter = None
    if diameters:
        max_diameter_tuple = max(diameters, key=lambda x: x[0])
        max_diameter = max_diameter_tuple[0]
        valid_circles = [d for d in diameters if d[0] > ratio_thresh * max_diameter]
        if valid_circles:
            valid_min_circle_tuple = min(valid_circles, key=lambda x: x[0])
            valid_min_diameter, valid_min_center, valid_min_radius = valid_min_circle_tuple
            valid_min_circle = (valid_min_center, valid_min_radius)
    else:
        max_diameter_tuple = None
        max_diameter = None

    return circles, max_diameter_tuple if diameters else None, valid_min_circle, valid_min_diameter, max_diameter if diameters else None
# ===== FENGE功能: 内切圆辅助函数结束 =====

# ===== FENGE功能: 嵌套矩形检测函数开始 =====
def find_nested_rectangles_fenge(contours, area_thresh=500):
    """FENGE功能:查找嵌套矩形"""
    rects = []
    for cnt in contours:
        epsilon = 0.02 * cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, epsilon, True)
        if len(approx) == 4 and cv2.isContourConvex(approx):
            area = cv2.contourArea(approx)
            if area > area_thresh:
                rects.append((area, approx))
    rects = sorted(rects, key=lambda x: -x[0])
    for i in range(len(rects)):
        outer = rects[i][1]
        for j in range(i + 1, len(rects)):
            inner = rects[j][1]
            if all(cv2.pointPolygonTest(outer, (float(p[0][0]), float(p[0][1])), False) >= 0 for p in inner):
                return outer, inner
    return None, None
# ===== FENGE功能: 嵌套矩形检测函数结束 =====

# ===== FENGE功能: 主处理函数开始 =====
def process_nested_rectangle_detection(frame):
    """处理嵌套矩形和内切圆检测"""
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)
    contours, _ = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    outer_box, inner_box = find_nested_rectangles_fenge(contours)

    result_text = "Nested Rect: Not found"

    if outer_box is not None and inner_box is not None:
        cv2.drawContours(frame, [outer_box], -1, (0, 255, 0), 2)
        cv2.drawContours(frame, [inner_box], -1, (255, 0, 0), 2)

        x, y, w, h = cv2.boundingRect(outer_box)
        distance_w = (FENGE_FOCAL_LENGTH * FENGE_KNOWN_WIDTH_CM) / w
        distance_h = (FENGE_FOCAL_LENGTH * FENGE_KNOWN_HEIGHT_CM) / h
        distance = (distance_w + distance_h) / 2

        cv2.putText(frame, f"Distance: {distance:.2f} cm", (x, y - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)

        x2, y2, w2, h2 = cv2.boundingRect(inner_box)
        border_px = ((x2 - x) + ((x + w) - (x2 + w2)) + (y2 - y) + ((y + h) - (y2 + h2))) / 4
        px_per_cm = border_px / FENGE_REAL_BORDER_WIDTH_CM if border_px > 0 else 100

        roi = gray[y:y + h, x:x + w]
        edge_roi = cv2.Canny(roi, 50, 150)
        lines, intersections = get_lines_and_intersections(edge_roi)

        circles, max_tuple, min_circle, min_diameter, max_diameter = get_valid_min_inscribed_circle(
            roi, min_area=500, min_radius=5, ratio_thresh=0.5, intersections=intersections, px_per_cm=px_per_cm)

        if max_tuple:
            _, max_center, max_radius = max_tuple
            cv2.circle(frame, (max_center[0] + x, max_center[1] + y), max_radius, (255, 0, 0), 2)

        if min_circle and min_diameter and max_diameter:
            min_center, min_radius = min_circle

            # 简化版本,不使用畸变校正
            min_diameter_cm = min_diameter / px_per_cm
            max_diameter_cm = max_diameter / px_per_cm
            ratio = min_diameter_cm / max_diameter_cm  -0.2

            # ===== 计算最小矩形边长 =====
            # 获取内部矩形框的边长
            inner_width = w2  # 内部矩形的宽度(像素)
            inner_height = h2  # 内部矩形的高度(像素)

            # 转换为厘米
            inner_width_cm = inner_width / px_per_cm
            inner_height_cm = inner_height / px_per_cm

            # 最小边长
            min_rect_side = min(inner_width_cm, inner_height_cm)

            # 打印结果
            print(f"检测到最小矩形边长: {min_diameter_cm:.1f} cm")


            # 绘制结果

            cv2.circle(frame, (min_center[0] + x, min_center[1] + y), min_radius, (0, 0, 255), 2)
            result_text = f"Rect: {min_rect_side:.1f}cm | Circles: {min_diameter_cm:.1f}/{max_diameter_cm:.1f}cm R={ratio:.2f}"

        if lines is not None:
            for line in lines:
                x1, y1, x2, y2 = line[0]
                cv2.line(frame, (x1 + x, y1 + y), (x2 + x, y2 + y), (200, 200, 0), 1)

        for pt in intersections:
            cv2.circle(frame, (pt[0] + x, pt[1] + y), 3, (255, 255, 0), -1)

    return result_text
# ===== FENGE功能: 主处理函数结束 =====


if __name__ == "__main__":
    ### 主循环
    '''
    print("程序启动 - 按以下键选择检测模式:")
    print("1: 基本形状检测(正方形和三角形)")
    print("2: 圆形检测")
    print("3: 检测多个正方形(最小)")
    print("4: 检测旋转信息")
    print("5: 单独测距离")
    print("ESC: 退出程序")
    '''
    sensor = INA226()

    contour_type = ContourType.Nothing

    DEBUG_MODE = False  # 调试模式显示中间过程

    A4_REAL_WIDTH = 21.0  # A4纸实际宽度(cm)
    A4_REAL_HEIGHT = 29.7  # A4纸实际高度(cm)
    A4_REAL_AREA = A4_REAL_WIDTH * A4_REAL_HEIGHT  # A4纸实际面积(cm²)
    MIN_CONTOUR_AREA = 1200  # 最小轮廓面积阈值(过滤噪声)
    FOCAL_LENGTH = 678.57  # 摄像头焦距(使用默认值)
    # ===== FENGE功能: 常量定义开始 =====
    FENGE_KNOWN_WIDTH_CM = 21.0    # A4纸宽度
    FENGE_KNOWN_HEIGHT_CM = 29.7   # A4纸高度
    FENGE_FOCAL_LENGTH = 678.57       # fenge使用的焦距
    FENGE_REAL_BORDER_WIDTH_CM = 2.0  # 实际黑边宽度
    # ===== FENGE功能: 常量定义结束 =====
    max_current_p = 0
    # 创建视频捕获对象
    cap = cv2.VideoCapture(0, cv2.CAP_V4L2)
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 240)

    gpio = juzhenkey.ReadGpio()
    # serial_port = serial.Serial(port="/dev/ttyUSB1", baudrate=115200,timeout=0.2)
    print('矩阵键盘初始化成功')
    while True:
        # 读取图像
        ret, img_cv2 = cap.read()
        if not ret:
            print("无法获取摄像头图像")
            break

        # 显示操作提示
        cv2.putText(img_cv2, "1:Shapes 2:Circle 3:MultiSq 4:Rotate 5:Distance", (10, 20),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1)

        current_a = abs(sensor.current_a())
        current_p = current_a * 5
        if current_p > max_current_p:
            max_current_p = current_p
        print(f'Current_a = {current_a:3f}A')
        print(f'Current_p = {current_p:3f}A')
        print(f'max_Current_p = {max_current_p:3f}A')

        # 检测键盘输入
        key = gpio.read_key()
        if key:
            print(f'检测到按键:{key}')
        if key == 27:  # ESC键退出
            break
        elif  key == '1' or key == '2' or key == '3' or key == '4' or key == '5' or key == '6' or key == '7' or key == '8' or key == '9' or key =='10':
            contour_type = ContourType.Distance_Measure
            print("切换到单独测距离模式")
        elif key == '11':
            contour_type = ContourType.Basic_shapes
            print("切换到基本形状检测模式(正方形和三角形)")
        elif key == '12':
            contour_type = ContourType.Basic_circle
            print("切换到圆形检测模式")
        elif key == '13':
            contour_type = ContourType.Extension_Two_and_One
            print("切换到多正方形检测模式")
        elif key == '14':
            contour_type = ContourType.Extension_Rotate
            print("切换到旋转检测模式")
        elif key == '15':
            contour_type = ContourType.Nested_Rectangle
            print("切换到重叠矩形检测模式")

        # 处理图像
        text = "Nothing"

        if contour_type != ContourType.Nothing:
            # 检测A4纸轮廓,获取A4纸像素面积
            contour, thresh, a4_pixel_area = detect_a4_contour(img_cv2)
            if contour is not None:
                # 计算像素宽度和距离
                pixel_width = calculate_pixel_width(contour)
                distance = calculate_distance(FOCAL_LENGTH, pixel_width)

                # 根据模式执行不同操作
                if contour_type == ContourType.Distance_Measure:
                    # 单独测距离模式:只显示距离信息
                    print(f"A4纸距离: {distance:.2f} cm")
                    text = f"Distance: {distance:.2f} cm"
                    # 绘制A4纸轮廓(可视化)
                    cv2.drawContours(img_cv2, [contour], -1, (0, 255, 0), 2)
                else:
                    # 透视变换
                    warped = perspective_transform(thresh, contour)
                    warped_copy = warped.copy()

                    if contour_type == ContourType.Basic_shapes:
                        # 同时检测正方形和三角形
                        squares = detect_squares_in_a4(warped_copy, a4_pixel_area)
                        triangles = detect_triangles_in_a4(warped_copy)

                        # 合并结果
                        all_shapes = squares + triangles

                        if all_shapes:
                            # 显示所有检测到的形状
                            shape_info = []
                            for shape in all_shapes:
                                shape_info.append(f"{shape['type']}:{shape['size']:.2f}cm")
                                # 在图像上标记形状中心
                                cv2.circle(img_cv2, shape['center'], 5, (0, 255, 0), -1)

                            print(f"检测到形状: {', '.join(shape_info)}, 距离: {distance:.2f} cm")
                            text = f"Shapes,D:{distance:.2f},{', '.join(shape_info)}"

                    elif contour_type == ContourType.Basic_circle:
                        circle = detect_circle_in_a4(warped_copy)
                        if circle:
                            print(f"圆形直径: {circle['size']:.2f} cm, 距离: {distance:.2f} cm")
                            text = f"Circle,D:{distance:.2f},size:{circle['size']:.2f}"
                            # 标记圆心
                            cv2.circle(img_cv2, circle['center'], 5, (0, 0, 255), -1)

                    elif contour_type == ContourType.Extension_Two_and_One:
                        squares = detect_and_measure_squares(warped, a4_pixel_area)
                        if squares and len(squares) > 0:
                            min_square = find_min_area_square(squares)
                            if min_square:
                                actual_size = min_square["actual_size"]
                                print(f"最小正方形边长: {actual_size:.2f} cm, 距离: {distance:.2f} cm")
                                text = f"MinSquare,D:{distance:.2f},size:{actual_size:.2f}"

                    elif contour_type == ContourType.Extension_Rotate:
                        squares = detect_and_measure_squares(warped, a4_pixel_area)
                        if squares and len(squares) > 0:
                            min_square = find_min_area_square(squares)
                            if min_square:
                                actual_size = min_square["actual_size"]
                                print(f"最小正方形边长: {actual_size:.2f} cm")
                                text = f"ROTATE,size:{actual_size:.2f} cm"
                    # ===== FENGE功能: 特殊处理开始 =====
                    elif contour_type == ContourType.Nested_Rectangle:
                        # 嵌套矩形检测不需要A4纸检测,直接处理
                        text = process_nested_rectangle_detection(img_cv2)
                        # 显示结果
                        cv2.putText(img_cv2, text, (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
                        cv2.imshow("Shape Detection", img_cv2)

                        continue  # 跳过后续A4纸检测流程
                    # ===== FENGE功能: 特殊处理结束 =====

        # 显示结果
        cv2.putText(img_cv2, text, (10, 40), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1)
        cv2.imshow("Shape Detection", img_cv2)

    # 释放资源
    cap.release()
    cv2.destroyAllWindows()
# 嵌套矩形和内切圆检测

import cv2
import numpy as np
import math

# 已知尺寸与焦距
KNOWN_WIDTH_CM = 21.0
KNOWN_HEIGHT_CM = 29.7
FOCAL_LENGTH = 400
REAL_BORDER_WIDTH_CM = 2.0  # 实际黑边宽度


# === 内切圆辅助函数 ===
def find_local_maxima(dist, min_radius=5):
    dilated = cv2.dilate(dist, np.ones((3, 3), np.uint8))
    local_max = (dist == dilated)
    radius_mask = dist > min_radius
    maxima = np.where(local_max & radius_mask)
    points = list(zip(maxima[1], maxima[0]))  # (x, y)
    radii = dist[maxima]
    return points, radii


def compute_intersection(line1, line2):
    x1, y1, x2, y2 = line1
    x3, y3, x4, y4 = line2
    denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
    if abs(denom) < 1e-6:  # 避免除零错误
        return None
    px = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / denom
    py = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / denom
    # 检查交点是否在合理范围内
    if abs(px) > 10000 or abs(py) > 10000:
        return None
    return int(px), int(py)


def get_lines_and_intersections(edges, min_line_length=30, max_line_gap=10):
    lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=80,
                            minLineLength=min_line_length, maxLineGap=max_line_gap)
    intersections = []
    if lines is not None and len(lines) > 1:
        for i in range(min(len(lines), 20)):  # 限制线条数量避免过多计算
            for j in range(i + 1, min(len(lines), 20)):
                pt = compute_intersection(lines[i][0], lines[j][0])
                if pt is not None:
                    intersections.append(pt)
    return lines, intersections


def get_valid_min_inscribed_circle(img, min_area=500, min_radius=5, ratio_thresh=0.5,
                                   intersections=None, px_per_cm=100):
    _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    num_labels, labels = cv2.connectedComponents(binary)
    circles = []
    diameters = []

    if intersections is None:
        intersections = []

    for label in range(1, num_labels):
        mask = np.uint8(labels == label) * 255
        area = cv2.countNonZero(mask)
        if area < min_area:
            continue
        dist_transform = cv2.distanceTransform(mask, cv2.DIST_L2, 5)
        centers, radii = find_local_maxima(dist_transform, min_radius)
        for center, radius in zip(centers, radii):
            cx, cy = center
            # 检查是否有有效的交点,并且避免空列表或无效值
            too_close = False
            if intersections:
                try:
                    too_close = any(np.hypot(cx - ix, cy - iy) < radius * 1.2
                                    for ix, iy in intersections
                                    if ix is not None and iy is not None)
                except:
                    too_close = False
            if too_close:
                continue
            diameter = radius * 2
            diameter_cm = diameter / px_per_cm
            if diameter_cm >= 5.5:  # 只保留有效圆
                circles.append((center, int(radius)))
                diameters.append((diameter, center, int(radius)))

    valid_min_circle = None
    valid_min_diameter = None
    if diameters:
        max_diameter_tuple = max(diameters, key=lambda x: x[0])
        max_diameter = max_diameter_tuple[0]
        valid_circles = [d for d in diameters if d[0] > ratio_thresh * max_diameter]
        if valid_circles:
            valid_min_circle_tuple = min(valid_circles, key=lambda x: x[0])
            valid_min_diameter, valid_min_center, valid_min_radius = valid_min_circle_tuple
            valid_min_circle = (valid_min_center, valid_min_radius)
    else:
        max_diameter_tuple = None
        max_diameter = None

    return circles, max_diameter_tuple if diameters else None, valid_min_circle, valid_min_diameter, max_diameter if diameters else None


# === 嵌套矩形检测 ===
def find_nested_rectangles(contours, area_thresh=500):
    rects = []
    for cnt in contours:
        epsilon = 0.02 * cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, epsilon, True)
        if len(approx) == 4 and cv2.isContourConvex(approx):
            area = cv2.contourArea(approx)
            if area > area_thresh:
                rects.append((area, approx))
    rects = sorted(rects, key=lambda x: -x[0])
    for i in range(len(rects)):
        outer = rects[i][1]
        for j in range(i + 1, len(rects)):
            inner = rects[j][1]
            if all(cv2.pointPolygonTest(outer, (float(p[0][0]), float(p[0][1])), False) >= 0 for p in inner):
                return outer, inner
    return None, None


# === 主程序 ===
cap = cv2.VideoCapture(0)

while True:
    ret, frame = cap.read()
    if not ret:
        break

    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 120, 255, cv2.THRESH_BINARY_INV)
    contours, _ = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
    outer_box, inner_box = find_nested_rectangles(contours)

    if outer_box is not None and inner_box is not None:
        cv2.drawContours(frame, [outer_box], -1, (0, 255, 0), 2)
        cv2.drawContours(frame, [inner_box], -1, (255, 0, 0), 2)

        x, y, w, h = cv2.boundingRect(outer_box)
        distance_w = (FOCAL_LENGTH * KNOWN_WIDTH_CM) / w
        distance_h = (FOCAL_LENGTH * KNOWN_HEIGHT_CM) / h
        distance = (distance_w + distance_h) / 2

        cv2.putText(frame, f"Distance: {distance:.2f} cm", (x, y - 10),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)

        x2, y2, w2, h2 = cv2.boundingRect(inner_box)
        border_px = ((x2 - x) + ((x + w) - (x2 + w2)) + (y2 - y) + ((y + h) - (y2 + h2))) / 4
        px_per_cm = border_px / REAL_BORDER_WIDTH_CM if border_px > 0 else 100

        roi = gray[y:y + h, x:x + w]
        edge_roi = cv2.Canny(roi, 50, 150)
        lines, intersections = get_lines_and_intersections(edge_roi)

        circles, max_tuple, min_circle, min_diameter, max_diameter = get_valid_min_inscribed_circle(
            roi, min_area=500, min_radius=5, ratio_thresh=0.5, intersections=intersections, px_per_cm=px_per_cm)

        if max_tuple:
            _, max_center, max_radius = max_tuple
            cv2.circle(frame, (max_center[0] + x, max_center[1] + y), max_radius, (255, 0, 0), 2)

        if min_circle and min_diameter and max_diameter:
            min_center, min_radius = min_circle

            # 计算矩形框的中心点作为参考点
            rect_center_x = x + w / 2
            rect_center_y = y + h / 2

            # 计算圆心到矩形中心的距离
            min_circle_center_x = min_center[0] + x
            min_circle_center_y = min_center[1] + y

            # 使用相似三角形原理进行畸变校正
            # 基于矩形框已知尺寸计算像素到厘米的转换比例
            px_per_cm_w = w / KNOWN_WIDTH_CM  # 宽度方向的像素/厘米比例
            px_per_cm_h = h / KNOWN_HEIGHT_CM  # 高度方向的像素/厘米比例

            # 根据圆心位置进行加权平均校正
            # 越靠近中心,校正因子越接近1;越远离中心,校正因子越大
            dx = abs(min_circle_center_x - rect_center_x) / (w / 2)  # 水平偏移比例
            dy = abs(min_circle_center_y - rect_center_y) / (h / 2)  # 垂直偏移比例

            # 基于位置的校正因子(简化的畸变模型)
            distortion_factor = 1 + 0.1 * (dx + dy)  # 可根据实际情况调整系数

            # 计算校正后的像素/厘米比例
            effective_px_per_cm = (px_per_cm_w + px_per_cm_h) / 2 * distortion_factor

            # 将像素直径转换为厘米(校正后)
            min_diameter_cm = min_diameter / effective_px_per_cm
            max_diameter_cm = max_diameter / effective_px_per_cm
            ratio = min_diameter_cm / max_diameter_cm

            cv2.circle(frame, (min_circle_center_x, min_circle_center_y), min_radius, (0, 0, 255), 2)
            cv2.putText(frame, f"Min: {min_diameter_cm:.1f}cm Max: {max_diameter_cm:.1f}cm R={ratio:.2f}",
                        (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
            cv2.putText(frame, f"Distortion: {distortion_factor:.2f}",
                        (10, 50), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 0), 2)

        if lines is not None:
            for line in lines:
                x1, y1, x2, y2 = line[0]
                cv2.line(frame, (x1 + x, y1 + y), (x2 + x, y2 + y), (200, 200, 0), 1)

        for pt in intersections:
            cv2.circle(frame, (pt[0] + x, pt[1] + y), 3, (255, 255, 0), -1)

    else:
        cv2.putText(frame, "No valid nested rectangles", (20, 40),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)

    cv2.imshow("Final Result", frame)
    if cv2.waitKey(1) & 0xFF == 27:
        break

cap.release()
cv2.destroyAllWindows()
# 键盘驱动

#!/usr/bin/python
#-*- coding:utf-8 -*-
import RPi.GPIO as GPIO
import time

class ReadGpio:
    def __init__(self):
        # 4x4矩阵键盘引脚设置
        self.ROW_PINS = [5, 6, 13, 19]    # 行引脚
        self.COL_PINS = [12, 16, 20, 21]   # 列引脚
        self.key_map = [
            [1, 2, 3, 4],
            [5, 6, 7, 8],
            [9, 10, 11, 12],
            [13, 14, 15, 16]
        ]
        self.GPIO_init()

    def GPIO_init(self):
        GPIO.setmode(GPIO.BCM)
        # 设置行引脚为输出,初始高电平
        for pin in self.ROW_PINS:
            GPIO.setup(pin, GPIO.OUT)
            GPIO.output(pin, GPIO.HIGH)
        # 设置列引脚为输入,上拉
        for pin in self.COL_PINS:
            GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
        print('GPIO listening!')

    def read_key(self):
        key_value = None
        time.sleep(0.05)  # 去抖动

        # 扫描矩阵键盘
        for row in range(4):
            GPIO.output(self.ROW_PINS[row], GPIO.LOW)

            for col in range(4):
                if GPIO.input(self.COL_PINS[col]) == GPIO.LOW:
                    key_value = str(self.key_map[row][col])  # 返回字符串形式的数字
                    print(f"KEY {key_value} PRESS")
                    # 等待按键释放
                    while GPIO.input(self.COL_PINS[col]) == GPIO.LOW:
                        time.sleep(0.01)

            GPIO.output(self.ROW_PINS[row], GPIO.HIGH)

        return key_value

if __name__ == '__main__':
    gpio = ReadGpio()
    while True:
        gpio.read_key()
# 能耗监测驱动

# ina226.py
import smbus

class INA226:
    REG_CONFIG        = 0x00
    REG_SHUNT_VOLTAGE = 0x01
    REG_BUS_VOLTAGE   = 0x02
    REG_POWER         = 0x03
    REG_CURRENT       = 0x04
    REG_CALIBRATION   = 0x05

    def __init__(self, i2c_addr=0x40, shunt=0.010, max_current=3.2, bus=1):
        self.addr = i2c_addr
        self.shunt = shunt
        self.current_lsb = max_current / 32768
        self.cal = int(0.00512 / (self.current_lsb * self.shunt))
        self.i2c = smbus.SMBus(bus)
        self._configure()

    def _write_reg(self, reg, value):
        self.i2c.write_i2c_block_data(self.addr, reg,
                                      [(value >> 8) & 0xFF, value & 0xFF])

    def _read_reg(self, reg):
        data = self.i2c.read_i2c_block_data(self.addr, reg, 2)
        return (data[0] << 8) | data[1]

    def _configure(self):
        # 连续模式,128 次平均,1.1 ms 转换时间
        self._write_reg(self.REG_CONFIG, 0x4127)
        self._write_reg(self.REG_CALIBRATION, self.cal)

    def current_a(self):
        raw = self._read_reg(self.REG_CURRENT)
        if raw > 0x7FFF:
            raw -= 0x10000
        return raw * self.current_lsb