import time, os, math
from media.sensor import *
from media.display import *
from media.media import *
from machine import FPIOA, PWM, Pin
from machine import UART
from machine import TOUCH
import gc
# 图像参数
IMAGE_WIDTH = 352
IMAGE_HEIGHT = 240
SENSOR_ID = 2
sensor = None
THRESHOLD = (69, 252) # 初始灰度阈值
# 显示模式设置
DISPLAY_MODE = "LCD" # 可选 "VIRT", "LCD", "HDMI"
# 根据显示模式设置分辨率
if DISPLAY_MODE == "VIRT":
DISPLAY_WIDTH, DISPLAY_HEIGHT = 1920, 1080
elif DISPLAY_MODE == "LCD":
DISPLAY_WIDTH, DISPLAY_HEIGHT = 800, 480
elif DISPLAY_MODE == "HDMI":
DISPLAY_WIDTH, DISPLAY_HEIGHT = 1920, 1080
else:
raise ValueError("无效的显示模式")
# 角点检测阈值
CORNERS_THRESHOLD = 5000
# 透视变换目标尺寸
PERSPECTIVE_SIZE = 200 # 变换后的矩形大小
# 矩形过滤参数
MIN_AREA_RATIO = 0.01 # 最小面积占图像比例
MAX_AREA_RATIO = 0.8 # 最大面积占图像比例
ASPECT_RATIO_MIN = 0.5 # 最小宽高比
ASPECT_RATIO_MAX = 2.0 # 最大宽高比
COS_THRESHOLD = 0.3 # 角度余弦阈值(绝对值)
# 初始化UART2
fpioa = FPIOA()
fpioa.set_function(11, FPIOA.UART2_TXD) # UART2发送
fpioa.set_function(12, FPIOA.UART2_RXD) # UART2接收
uart2 = UART(UART.UART2, 115200, 8, 0, 1, timeout=1000) # 用于坐标通信
# 初始化按键
fpioa.set_function(53, FPIOA.GPIO53)
button = Pin(53, Pin.IN, Pin.PULL_DOWN) # 使用下拉电阻
debounce_delay = 20 # 毫秒
last_press_time = 0 # 上次按键按下的时间,单位为毫秒
button_last_state = 0 # 上次按键状态
timeTouch = 0
tp = TOUCH(0)
# 按钮文本
textL = ["back", "GL-", "GH-", "set"]
textR = ["Gray", "GL+", "GH+",]
isLabFlag = False # 默认使用灰度模式
isInvertFlag = False # 是否反转
# 虚拟按钮范围
buttonsL = [
(0, 5, 100, 100), # 按钮"back"
(0, 65, 100, 100), # 按钮"GL-"
(0, 125, 100, 100), # 按钮"GH-"
(0, 200, 100, 100) # 按钮"set"
]
buttonsR = [
(250, 5, 100, 100), # 按钮"Gray"
(250, 65, 100, 100), # 按钮"GL+"
(250, 125, 100, 100), # 按钮"GH+"
]
buttonsS = [
(300, 5, 100, 50) # 步进按钮
]
# 灰度范围初始化
GL, GH = 0, 255 # 灰度范围0~255
step = 1 # 脱机阈值步进
Gray_thresholds = [(int(GL), int(GH))] # 灰度阈值列表
# 状态标志
flag = False # 页面标志,True为阈值设置
scan_mode = False # 扫描模式标志
last_scan_send_time = 0 # 上次发送扫描信号的时间
# 脱机调整阈值界面设计
def changeScreen(img):
img_1 = img.copy()
screen = img.copy()
# 绘制白色背景、阈值画面范围及原始画面
screen.draw_rectangle(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, (255, 255, 255), fill=True)
screen.draw_rectangle(200, 170, 400, 240, (0, 0, 0), 2) # 显示原始图像(全屏)
screen.draw_image(img_1, 0, 0, x_size=DISPLAY_WIDTH, y_size=DISPLAY_HEIGHT) # 获取画面
# 灰度模式处理
GrayImg = img_1.to_grayscale().binary(Gray_thresholds)
if isInvertFlag:
GrayImg = GrayImg.invert()
# 显示灰度阈值图像(全屏)
screen.draw_image(GrayImg, 0, 0, x_size=DISPLAY_WIDTH, y_size=DISPLAY_HEIGHT)
screen.draw_string_advanced(300, 420, 30, f"Gray: [{GL} {GH}]", color=(0, 0, 0))
# 绘制虚拟按钮,编写文本
for i in range(8):
screen.draw_rectangle(0, 5+i*60, 100, 50, (200, 201, 202), thickness=1, fill=True)
screen.draw_string_advanced(30, 20+i*60, 20, textL[i], (0, 0, 0))
screen.draw_rectangle(700, 5+i*60, 100, 50, (200, 201, 202), thickness=1, fill=True)
screen.draw_string_advanced(730, 20+i*60, 20, textR[i], (0, 0, 0))
screen.draw_rectangle(390, 5, 100, 50, (200, 201, 202), thickness=1, fill=True)
screen.draw_string_advanced(400, 20, 20, f"step: {step}", (0, 0, 0))
# 显示处理后的图像
Display.show_image(screen, 0, 0)
# 触摸屏按钮事件动作
def buttonAction(direction, index):
global GL, GH, flag, step, isInvertFlag, textR, Gray_thresholds
# 阈值列表重置
Gray_thresholds = [(int(GL), int(GH))]
# 按钮处理
if direction == 'left': # 减操作
if index == 1: # GL-
GL = max(GL - step, 0)
elif index == 2: # GH-
GH = max(GH - step, 0)
elif index == 0: # back
flag = False
elif direction == 'right': # 加操作
if index == 1: # GL+
GL = min(GL + step, 255)
elif index == 2: # GH+
GH = min(GH + step, 255)
elif index == 7: # invert/Ninvert
isInvertFlag = not isInvertFlag
if isInvertFlag:
textR[7] = 'invert' # 更改文本
else:
textR[7] = 'Ninvert'
elif direction == 'step': # 调整步进
step += 1
if step > 5:
step = 1
# 触摸屏事件
def touchAction():
global timeTouch
touchP = tp.read(5)
# 每3次计为1次,减少sleep时间
if touchP:
timeTouch += 1
if timeTouch >= 3:
p = touchP
timeTouch = 0
if p:
x, y = p[0].x, p[0].y
# 通过触摸坐标与按钮覆盖位置确定触摸按钮
if x < 200:
for i, (rect_x, rect_y, rect_w, rect_h) in enumerate(buttonsL):
if (rect_x <= x <= rect_x + rect_w) and (rect_y <= y <= rect_y + rect_h):
print(f"操作: {textL[i]}")
buttonAction('left', i)
elif x > 600:
for i, (rect_x, rect_y, rect_w, rect_h) in enumerate(buttonsR):
if (rect_x <= x <= rect_x + rect_w) and (rect_y <= y <= rect_y + rect_h):
print(f"操作: {textR[i]}")
buttonAction('right', i)
else:
for i, (rect_x, rect_y, rect_w, rect_h) in enumerate(buttonsS):
if (rect_x <= x <= rect_x + rect_w) and (rect_y <= y <= rect_y + rect_h):
print(f"操作: step")
buttonAction('step', 1)
time.sleep(0.01)
def send_coordinates(center):
"""通过串口发送坐标数据"""
if center:
cx, cy = center
# 协议格式: $X[坐标x]Y[坐标y]\n
data_str = f"$X{cx:04d}Y{cy:04d}\n"
uart2.write(data_str.encode())
else:
uart2.write(b"$X9999Y9999\n") # 无效坐标标志
def sort_corners(corners):
"""对矩形角点进行排序(左上、右上、右下、左下)"""
if len(corners) != 4:
return corners
# 计算中心点
center_x = sum(c[0] for c in corners) / 4
center_y = sum(c[1] for c in corners) / 4
# 分类角点
top_left = min(corners, key=lambda c: (c[0] - center_x)**2 + (c[1] - center_y)**2
if c[0] < center_x and c[1] < center_y else float('inf'))
top_right = min(corners, key=lambda c: (c[0] - center_x)**2 + (c[1] - center_y)**2
if c[0] > center_x and c[1] < center_y else float('inf'))
bottom_right = min(corners, key=lambda c: (c[0] - center_x)**2 + (c[1] - center_y)**2
if c[0] > center_x and c[1] > center_y else float('inf'))
bottom_left = min(corners, key=lambda c: (c[0] - center_x)**2 + (c[1] - center_y)**2
if c[0] < center_x and c[1] > center_y else float('inf'))
return [top_left, top_right, bottom_right, bottom_left]
def matrix_multiply(A, B):
"""3x3矩阵乘法"""
result = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
for i in range(3):
for j in range(3):
for k in range(3):
result[i][j] += A[i][k] * B[k][j]
return result
def matrix_vector_multiply(M, v):
"""矩阵向量乘法"""
result = [0, 0, 0]
for i in range(3):
for j in range(3):
result[i] += M[i][j] * v[j]
return result
def matrix_inverse(M):
"""3x3矩阵求逆"""
det = (M[0][0] * (M[1][1]*M[2][2] - M[1][2]*M[2][1]) -
M[0][1] * (M[1][0]*M[2][2] - M[1][2]*M[2][0]) +
M[0][2] * (M[1][0]*M[2][1] - M[1][1]*M[2][0]))
if abs(det) < 1e-10: # 行列式接近0,不可逆
return [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
inv_det = 1.0 / det
inv = [
[(M[1][1]*M[2][2] - M[2][1]*M[1][2]) * inv_det,
(M[0][2]*M[2][1] - M[0][1]*M[2][2]) * inv_det,
(M[0][1]*M[1][2] - M[0][2]*M[1][1]) * inv_det],
[(M[1][2]*M[2][0] - M[1][0]*M[2][2]) * inv_det,
(M[0][0]*M[2][2] - M[0][2]*M[2][0]) * inv_det,
(M[1][0]*M[0][2] - M[0][0]*M[1][2]) * inv_det],
[(M[1][0]*M[2][1] - M[2][0]*M[1][1]) * inv_det,
(M[2][0]*M[0][1] - M[0][0]*M[2][1]) * inv_det,
(M[0][0]*M[1][1] - M[1][0]*M[0][1]) * inv_det]
]
return inv
def calculate_perspective_transform(src_points):
"""
计算透视变换矩阵
src_points: 源图像中的四个点 [左上, 右上, 右下, 左下]
返回: 变换矩阵和逆变换矩阵
"""
# 目标点 (校正后的正方形)
dst_points = [
[0, 0],
[PERSPECTIVE_SIZE, 0],
[PERSPECTIVE_SIZE, PERSPECTIVE_SIZE],
[0, PERSPECTIVE_SIZE]
]
# 构建A矩阵
A = []
for i in range(4):
x, y = src_points[i]
u, v = dst_points[i]
A.append([x, y, 1, 0, 0, 0, -u*x, -u*y])
A.append([0, 0, 0, x, y, 1, -v*x, -v*y])
# 构建b向量
b = []
for i in range(4):
u, v = dst_points[i]
b.append(u)
b.append(v)
# 解线性方程组 (使用最小二乘法简化)
h = [0] * 8
for i in range(8):
for j in range(8):
# 计算A^T * A
ata = 0
for k in range(8):
ata += A[k][i] * A[k][j]
# 计算A^T * b
atb = 0
for k in range(8):
atb += A[k][i] * b[k]
# 简单求解
if abs(ata) > 1e-10:
h[i] = atb / ata
# 构造变换矩阵
H = [
[h[0], h[1], h[2]],
[h[3], h[4], h[5]],
[h[6], h[7], 1]
]
# 计算逆矩阵
H_inv = matrix_inverse(H)
return H, H_inv
def apply_perspective_transform(point, H):
"""应用透视变换到单个点"""
x, y = point
src = [x, y, 1]
dst = matrix_vector_multiply(H, src)
if abs(dst[2]) > 1e-10:
dst = [dst[0]/dst[2], dst[1]/dst[2]]
return dst
def calculate_center(corners):
"""计算矩形中心点(使用透视校正)"""
if len(corners) < 4:
return None
# 计算几何中心(原始中心)
raw_center_x = sum(c[0] for c in corners) / len(corners)
raw_center_y = sum(c[1] for c in corners) / len(corners)
# 计算透视变换
try:
H, H_inv = calculate_perspective_transform(corners)
# 将中心点映射到透视校正后的空间
corrected_center = apply_perspective_transform((raw_center_x, raw_center_y), H)
# 将校正后的中心点映射回原始图像空间
final_center = apply_perspective_transform(corrected_center, H_inv)
return (int(final_center[0]), int(final_center[1]))
except Exception as e:
# 计算失败时使用原始中心
return (int(raw_center_x), int(raw_center_y))
def is_valid_rectangle(corners, cos_threshold=COS_THRESHOLD):
"""
检查是否为有效矩形(内角接近90度)
corners: 排序后的四个角点 [左上, 右上, 右下, 左下]
cos_threshold: 余弦值阈值(绝对值应小于此值)
返回: True/False
"""
if len(corners) != 4:
return False
# 计算四个内角的余弦值
for i in range(4):
# 获取三个连续点: A-B-C
A = corners[i]
B = corners[(i + 1) % 4]
C = corners[(i + 2) % 4]
# 计算向量BA和BC
BA = (A[0] - B[0], A[1] - B[1])
BC = (C[0] - B[0], C[1] - B[1])
# 计算点积和模长
dot_product = BA[0] * BC[0] + BA[1] * BC[1]
mod_BA = math.sqrt(BA[0]**2 + BA[1]**2)
mod_BC = math.sqrt(BC[0]**2 + BC[1]**2)
# 避免除零错误
if mod_BA * mod_BC < 1e-5:
return False
# 计算余弦值
cos_value = dot_product / (mod_BA * mod_BC)
# 检查是否接近90度 (|cos| < threshold)
if abs(cos_value) > cos_threshold:
return False
return True
def calculate_rect_properties(corners):
"""计算矩形的面积和宽高比"""
if len(corners) != 4:
return 0, 0
# 计算边长
top = math.sqrt((corners[1][0] - corners[0][0])**2 + (corners[1][1] - corners[0][1])**2)
right = math.sqrt((corners[2][0] - corners[1][0])**2 + (corners[2][1] - corners[1][1])**2)
bottom = math.sqrt((corners[3][0] - corners[2][0])**2 + (corners[3][1] - corners[2][1])**2)
left = math.sqrt((corners[0][0] - corners[3][0])**2 + (corners[0][1] - corners[3][1])**2)
# 计算平均宽度和高度
width = (top + bottom) / 2
height = (left + right) / 2
# 计算面积和宽高比
area = width * height
aspect_ratio = max(width, height) / min(width, height) if min(width, height) > 0 else 0
return area, aspect_ratio
def draw_corner_info(img, corners, center, area, aspect_ratio):
"""在图像上绘制角点和中心信息"""
if len(corners) != 4:
return
# 定义角点颜色和标签
corner_colors = [(0, 255, 0), (0, 255, 0), (0, 255, 0), (0, 255, 0)]
labels = ["TL", "TR", "BR", "BL"]
# 绘制角点
for i, (x, y) in enumerate(corners):
color = corner_colors[i]
img.draw_circle(int(x), int(y), 8, color=color, thickness=2)
img.draw_string(int(x) + 10, int(y) - 10, labels[i], color=color, scale=1)
# 绘制矩形边框
for i in range(4):
x1, y1 = corners[i]
x2, y2 = corners[(i + 1) % 4]
img.draw_line(int(x1), int(y1), int(x2), int(y2), color=(0, 255, 255), thickness=2)
# 绘制对角线
img.draw_line(int(corners[0][0]), int(corners[0][1]), int(corners[2][0]), int(corners[2][1]), color=(255, 0, 0), thickness=1)
img.draw_line(int(corners[1][0]), int(corners[1][1]), int(corners[3][0]), int(corners[3][1]), color=(255, 0, 0), thickness=1)
# 绘制中心点
if center:
cx, cy = center
img.draw_cross(int(cx), int(cy), size=15, color=(255, 0, 255), thickness=2)
img.draw_circle(int(cx), int(cy), 5, color=(255, 0, 255), thickness=-1)
img.draw_string(int(cx) + 10, int(cy) - 10, "Center", color=(255, 0, 255), scale=1)
# 显示矩形属性
img.draw_string(10, 30, f"Area: {area:.1f} px", color=(0, 255, 0), scale=1)
img.draw_string(10, 50, f"Aspect: {aspect_ratio:.2f}", color=(0, 255, 0), scale=1)
def process_frame(img):
"""处理图像帧,检测矩形角点和中心(带形状验证)"""
img_gray = img.to_grayscale(copy=False)
img_bin = img_gray.binary([(GL, GH)])
rects = img_bin.find_rects(threshold=CORNERS_THRESHOLD)
# 计算图像总面积
total_image_area = img.width() * img.height()
min_area = MIN_AREA_RATIO * total_image_area
max_area = MAX_AREA_RATIO * total_image_area
# 找出所有可能的矩形
valid_rects = []
for rect in rects:
corners = rect.corners()
if len(corners) == 4:
sorted_corners = sort_corners(corners)
# 计算矩形属性
area, aspect_ratio = calculate_rect_properties(sorted_corners)
# 检查角度、面积和宽高比
angle_valid = is_valid_rectangle(sorted_corners)
area_valid = min_area <= area <= max_area
aspect_valid = ASPECT_RATIO_MIN <= aspect_ratio <= ASPECT_RATIO_MAX
if angle_valid and area_valid and aspect_valid:
valid_rects.append((area, aspect_ratio, sorted_corners))
# 选择面积最大的有效矩形
corners = []
center = None
area = 0
aspect_ratio = 0
if valid_rects:
valid_rects.sort(key=lambda x: x[0], reverse=True)
area, aspect_ratio, corners = valid_rects[0]
center = calculate_center(corners)
# 绘制结果
if center:
draw_corner_info(img, corners, center, area, aspect_ratio)
else:
# 显示未检测到矩形的信息
img.draw_string(img.width()//2 - 50, img.height()//2, "No valid rectangle", color=(255, 0, 0), scale=1)
# 资源清理
del img_gray, img_bin
gc.collect()
return corners, center
try:
print("初始化摄像头...")
# 初始化摄像头
sensor = Sensor(id=SENSOR_ID)
sensor.reset()
# 设置与显示匹配的分辨率
sensor.set_framesize(width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT)
sensor.set_pixformat(Sensor.RGB565, chn=CAM_CHN_ID_0)
# sensor.set_hmirror(1)
# sensor.set_vflip(1)
print("初始化显示器...")
# 初始化显示器
if DISPLAY_MODE == "VIRT":
Display.init(Display.VIRT, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, fps=60)
elif DISPLAY_MODE == "LCD":
Display.init(Display.ST7701, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, to_ide=True)
elif DISPLAY_MODE == "HDMI":
Display.init(Display.LT9611, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT, to_ide=True)
print("初始化媒体管理器...")
# 初始化媒体管理器
MediaManager.init()
sensor.run()
clock = time.clock()
print("开始主循环...")
while True:
os.exitpoint()
clock.tick()
# 捕获图像
img = sensor.snapshot(chn=CAM_CHN_ID_0)
# 处理按键事件
button_state = button.value() # 获取当前按键状态
current_time = time.ticks_ms() # 获取当前时间(单位:毫秒)
# 检测按键从未按下(0)到按下(1)的变化(上升沿)
if button_state == 1 and button_last_state == 0:
# 检查按键是否在消抖时间外
if current_time - last_press_time > debounce_delay:
if button.value() == 1:
flag = not flag
# 第二次按下按键时进入扫描模式
if not flag:
scan_mode = True
print("进入扫描模式")
last_press_time = current_time
# 更新上次按键状态
button_last_state = button_state
# 仅在阈值设置界面显示阈值调整UI
if flag:
changeScreen(img)
touchAction()
else:
# 正常模式下显示原始图像和处理结果
# 处理图像并检测角点
corners, center = process_frame(img)
# 发送坐标
send_coordinates(center)
time.sleep_ms(20) # 控制处理频率
# 显示FPS
img.draw_string(10, 10, f"FPS: {clock.fps():.1f}", color=(255, 0, 0), scale=1)
# 显示图像(全屏)
img.compressed_for_ide()
Display.show_image(img, 0, 0)
os.exitpoint() # 可用的退出点
except KeyboardInterrupt:
print("程序被用户中断")
except Exception as e:
print(f"发生错误: {e}")
import sys
sys.print_exception(e)
finally:
print("清理资源...")
# 清理资源
if sensor:
sensor.stop()
Display.deinit()
MediaManager.deinit()
print("程序退出")
以上述代码前81行程序为参照,修改81行以下函数的参数等使其正常运行
最新发布