#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
智能车竞赛全自动控制程序 - 操场赛道优化版
针对400m操场左转弯道向右偏航问题进行了专项优化
转向补偿值:4.0度
代码长度:19987字符
"""
import os
import time
import datetime
import cv2
import numpy as np
import pigpio
import onnxruntime
from PIL import Image, ImageDraw, ImageFont
# ===== 全局配置 =====
CAM_INDEX = 2
W, H = 640, 480
FPS_TARGET = 20
DATA_SAVE_DIR = "/home/pi/smartcar_data"
os.makedirs(DATA_SAVE_DIR, exist_ok=True)
SAVE_INTERVAL = 30
# ===== 硬件控制参数 =====
MOTOR_PIN = 13
NEUTRAL_PULSE = 1500
FORWARD_BASE = 1350
SERVO_PIN = 12
SERVO_MIN_ANGLE = 70
SERVO_MAX_ANGLE = 110
SERVO_MID_ANGLE = 90
SERVO_MIN_PULSE = 500
SERVO_MAX_PULSE = 2500
# ===== PID控制参数 =====
KP = 0.09
KD = 0.07
DEAD_ZONE = 5.0
TURN_DECAY = 0.85
MAX_TURN_DURATION = 8
# ===== 转向补偿参数 =====
TURN_COMPENSATION = 4.0
COMPENSATION_THRESHOLD = 15.0
COMPENSATION_DIRECTION = "left"
# ===== 循迹参数 =====
WHITE_THRESHOLD = 170
MIN_LINE_LENGTH = 20
MAX_LINE_GAP = 8
ANGLE_RANGE = (0, 55)
ROI_RATIO = 2 / 3
SINGLE_LINE_OFFSET = 80
# ===== 避障参数 =====
OBSTACLE_COLOR_LOWER = np.array([97, 154, 60])
OBSTACLE_COLOR_UPPER = np.array([117, 255, 255])
OBSTACLE_AREA_MIN = 600
ASPECT_RATIO_RANGE = (0.5, 1.4)
EXTENT_THRESHOLD = 0.95
# ===== 斑马线检测 =====
ZEBRA_RATIO_THRESH = 0.3
CROSSWALK_STOP_DURATION = 3
# ===== 箭头识别 =====
ARROW_MODEL_PATH = "/home/pi/arrow_best.onnx"
ARROW_CLASS_NAMES = ["arrow_left", "arrow_right"]
SIGN_COLOR_LOWER = np.array([100, 80, 50])
SIGN_COLOR_UPPER = np.array([130, 255, 255])
SIGN_AREA_MIN = 200
CONF_THRESHOLD = 0.4
REQUIRED_CONSECUTIVE = 3
# ===== A/B识别 =====
AB_MODEL_PATH = "/home/pi/ab_best.onnx"
AB_CLASS_NAMES = ["A", "B"]
# ===== 停车区检测 =====
YELLOW_COLOR_LOWER = np.array([20, 100, 100])
YELLOW_COLOR_UPPER = np.array([30, 255, 255])
YELLOW_LINE_MIN_LENGTH = 200
PARK_AREA_LEFT = (0, 320)
PARK_AREA_RIGHT = (320, 640)
# ===== 状态机 =====
STATE_WAIT_START = "WAIT_START"
STATE_START = "START"
STATE_TRACK = "TRACK"
STATE_CROSSWALK = "CROSSWALK"
STATE_LANE_CHANGE = "LANE_CHANGE"
STATE_AB_RECOG = "AB_RECOG"
STATE_PARK = "PARK"
STATE_FINISH = "FINISH"
# ===== 硬件控制类 =====
class HardwareController:
def __init__(self):
self.pi = pigpio.pi()
if not self.pi.connected:
raise RuntimeError("请启动pigpio服务")
self.last_error = 0.0
self.current_angle = SERVO_MID_ANGLE
self.turn_duration = 0
self.last_turn_direction = 0
self.last_known_direction = None
self.pi.set_servo_pulsewidth(MOTOR_PIN, NEUTRAL_PULSE)
time.sleep(1.0)
self.set_servo_angle(SERVO_MID_ANGLE)
print("[HW] 初始化完成")
def angle_to_pulse(self, angle):
angle = np.clip(angle, SERVO_MIN_ANGLE, SERVO_MAX_ANGLE)
pulse_range = SERVO_MAX_PULSE - SERVO_MIN_PULSE
return int(SERVO_MIN_PULSE + (angle / 180.0) * pulse_range)
def set_servo_angle(self, angle):
smoothed = 0.6 * angle + 0.4 * self.current_angle
pulse = self.angle_to_pulse(smoothed)
self.pi.set_servo_pulsewidth(SERVO_PIN, pulse)
self.current_angle = smoothed
return self.current_angle
def set_motor_speed(self, pulse):
pulse = int(np.clip(pulse, 1000, 2000))
self.pi.set_servo_pulsewidth(MOTOR_PIN, pulse)
return pulse
def pid_control(self, error):
# 更新最后已知方向
if error < -5:
self.last_known_direction = "left"
elif error > 5:
self.last_known_direction = "right"
# 转向补偿机制
if abs(error) > COMPENSATION_THRESHOLD:
if COMPENSATION_DIRECTION == "left":
error -= TURN_COMPENSATION
else:
error += TURN_COMPENSATION
error = -error # 反转误差
# 判断当前转向方向
current_direction = 0
if error > DEAD_ZONE:
current_direction = 2
elif error < -DEAD_ZONE:
current_direction = 1
# 更新连续转向时间
if current_direction != self.last_turn_direction:
self.turn_duration = 0
self.last_turn_direction = current_direction
else:
self.turn_duration += 1
# 长时间转向衰减
if self.turn_duration >= MAX_TURN_DURATION:
error *= TURN_DECAY
# 死区处理
if abs(error) < DEAD_ZONE:
error = 0.0
self.turn_duration = 0
# PID计算
p = KP * error
d = KD * (error - self.last_error)
self.last_error = error
total_offset = p + d
target_angle = SERVO_MID_ANGLE + total_offset
self.set_servo_angle(target_angle)
self.set_motor_speed(FORWARD_BASE)
return target_angle
def stop(self):
self.set_motor_speed(NEUTRAL_PULSE)
return NEUTRAL_PULSE
def cleanup(self):
try:
self.stop()
self.set_servo_angle(SERVO_MID_ANGLE)
self.pi.stop()
except:
pass
print("[HW] 清理完成")
# ===== 图像处理函数 =====
def draw_chinese(frame, text, pos=(10, 30), color=(255, 255, 0)):
img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil = Image.fromarray(img_rgb)
draw = ImageDraw.Draw(pil)
try:
font = ImageFont.truetype("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", 24)
except:
font = ImageFont.load_default()
draw.text(pos, text, font=font, fill=tuple(int(c) for c in color))
return cv2.cvtColor(np.array(pil), cv2.COLOR_RGB2BGR)
def detect_color_contours(frame, lower, upper, min_area=0):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, lower, upper)
mask = cv2.medianBlur(mask, 5)
kernel = np.ones((5, 5), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
return [c for c in contours if cv2.contourArea(c) >= min_area], mask
def detect_blue_cone(frame):
contours, mask = detect_color_contours(frame, OBSTACLE_COLOR_LOWER, OBSTACLE_COLOR_UPPER, OBSTACLE_AREA_MIN)
valid = []
for c in contours:
area = cv2.contourArea(c)
if area < OBSTACLE_AREA_MIN:
continue
x, y, w, h = cv2.boundingRect(c)
ar = w / float(h) if h > 0 else 0
if not (ASPECT_RATIO_RANGE[0] <= ar <= ASPECT_RATIO_RANGE[1]):
continue
extent = area / float(w * h)
if extent >= EXTENT_THRESHOLD:
continue
valid.append((area, (x, y, w, h)))
if not valid:
return False, None, mask
valid.sort(key=lambda x: x[0], reverse=True)
return True, valid[0][1], mask
def detect_crosswalk(frame):
roi = frame[int(H * ROI_RATIO):, :]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY)
ratio = cv2.countNonZero(binary) / float(roi.shape[0] * roi.shape[1])
return ratio >= ZEBRA_RATIO_THRESH
def generate_black_text_mask(frame):
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask_blue = cv2.inRange(hsv, SIGN_COLOR_LOWER, SIGN_COLOR_UPPER)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
mask_blue = cv2.morphologyEx(mask_blue, cv2.MORPH_OPEN, kernel)
mask_blue = cv2.morphologyEx(mask_blue, cv2.MORPH_CLOSE, kernel)
contours, _ = cv2.findContours(mask_blue, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
mask_sym = None
if contours:
max_contour = max(contours, key=cv2.contourArea)
if cv2.contourArea(max_contour) >= SIGN_AREA_MIN:
x, y, w, h = cv2.boundingRect(max_contour)
pad = 3
x0, y0 = max(0, x - pad), max(0, y - pad)
x1, y1 = min(frame.shape[1], x + w + pad), min(frame.shape[0], y + h + pad)
roi = frame[y0:y1, x0:x1]
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
gray = cv2.equalizeHist(gray)
_, mask_sym = cv2.threshold(gray, 160, 255, cv2.THRESH_BINARY)
return cv2.bitwise_not(mask_sym) if mask_sym is not None else cv2.bitwise_not(mask_blue)
def model_infer(mask, session, input_name, model_input_size, class_names):
mask_resized = cv2.resize(mask, model_input_size)
if len(mask_resized.shape) == 2:
mask_resized = cv2.cvtColor(mask_resized, cv2.COLOR_GRAY2BGR)
mask_input = mask_resized[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0
mask_input = np.expand_dims(mask_input, axis=0)
outputs = session.run(None, {input_name: mask_input})[0]
max_conf = -1.0
best_cls = "Waiting..."
for det in outputs:
conf = float(det[4])
cls_idx = int(det[5])
if conf > max_conf and conf >= CONF_THRESHOLD and cls_idx < len(class_names):
max_conf = conf
best_cls = class_names[cls_idx]
return best_cls, max_conf
def get_lane_error(frame):
height, width = frame.shape[:2]
roi_y_start = int(height * ROI_RATIO)
roi = frame[roi_y_start:height, :]
roi_h = roi.shape[0]
if roi.size == 0:
return None, (None, None, None, [], None)
gray_roi = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(4, 4))
enhanced = clahe.apply(gray_roi)
blur = cv2.GaussianBlur(enhanced, (3, 3), 0)
_, binary = cv2.threshold(blur, WHITE_THRESHOLD, 255, cv2.THRESH_BINARY)
kernel = np.ones((3, 3), np.uint8)
binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=2)
lines = cv2.HoughLinesP(
binary,
1,
np.pi / 180,
20,
minLineLength=MIN_LINE_LENGTH,
maxLineGap=MAX_LINE_GAP
)
left_lines, right_lines = [], []
if lines is not None:
for line in lines:
x1, y1, x2, y2 = line[0]
dx = x2 - x1
dy = y2 - y1
if dx == 0:
continue
angle = abs(np.arctan(dy / dx) * 180 / np.pi)
if not (ANGLE_RANGE[0] < angle < ANGLE_RANGE[1]):
continue
if y1 < y2:
x1, y1, x2, y2 = x2, y2, x1, y1
if x1 < width / 2:
left_lines.append([x1, y1, x2, y2])
else:
right_lines.append([x1, y1, x2, y2])
def fit_line(lines_list):
if len(lines_list) < 2:
return None
points = []
for x1, y1, x2, y2 in lines_list:
points.append((x1, y1, 2))
points.append((x2, y2, 1))
xs = np.array([p[0] for p in points])
ys = np.array([p[1] for p in points])
weights = np.array([p[2] for p in points])
try:
coeff = np.polyfit(ys, xs, 1, w=weights)
y_min, y_max = 0, roi_h - 1
x_min = int(coeff[0] * y_min + coeff[1])
x_max = int(coeff[0] * y_max + coeff[1])
return [x_min, y_min, x_max, y_max]
except:
return None
left_fit = fit_line(left_lines)
right_fit = fit_line(right_lines)
mid_points = []
error = None
if left_fit and right_fit:
for y_roi in range(0, roi_h, 10):
left_coeff = np.polyfit([left_fit[1], left_fit[3]], [left_fit[0], left_fit[2]], 1)
right_coeff = np.polyfit([right_fit[1], right_fit[3]], [right_fit[0], right_fit[2]], 1)
left_x = left_coeff[0] * y_roi + left_coeff[1]
right_x = right_coeff[0] * y_roi + right_coeff[1]
mid_x = int((left_x + right_x) / 2)
mid_y = y_roi + roi_y_start
mid_points.append((mid_x, mid_y))
image_center_x = width // 2
current_mid_x = mid_points[-1][0] if mid_points else image_center_x
error = current_mid_x - image_center_x
elif left_fit and not right_fit:
for y_roi in range(0, roi_h, 10):
left_coeff = np.polyfit([left_fit[1], left_fit[3]], [left_fit[0], left_fit[2]], 1)
left_x = left_coeff[0] * y_roi + left_coeff[1]
target_x = left_x + SINGLE_LINE_OFFSET
mid_points.append((int(target_x), y_roi + roi_y_start))
image_center_x = width // 2
current_target_x = mid_points[-1][0] if mid_points else image_center_x
error = current_target_x - image_center_x
elif right_fit and not left_fit:
for y_roi in range(0, roi_h, 10):
right_coeff = np.polyfit([right_fit[1], right_fit[3]], [right_fit[0], right_fit[2]], 1)
right_x = right_coeff[0] * y_roi + right_coeff[1]
target_x = right_x - SINGLE_LINE_OFFSET
mid_points.append((int(target_x), y_roi + roi_y_start))
image_center_x = width // 2
current_target_x = mid_points[-1][0] if mid_points else image_center_x
error = current_target_x - image_center_x
return error, (left_fit, right_fit, roi_y_start, mid_points, binary)
# ===== 主控制逻辑 =====
def main():
cap = cv2.VideoCapture(CAM_INDEX)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, W)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, H)
cap.set(cv2.CAP_PROP_FPS, FPS_TARGET)
time.sleep(1.0)
if not cap.isOpened():
print("[ERR] 摄像头打开失败")
return
try:
hw = HardwareController()
except Exception as e:
print(f"[ERR] 硬件初始化失败: {e}")
cap.release()
return
try:
arrow_sess = onnxruntime.InferenceSession(ARROW_MODEL_PATH, providers=["CPUExecutionProvider"])
arrow_input = arrow_sess.get_inputs()[0].name
arrow_size = (arrow_sess.get_inputs()[0].shape[3], arrow_sess.get_inputs()[0].shape[2])
except Exception as e:
print(f"[ERR] 箭头模型加载失败: {e}")
hw.cleanup()
cap.release()
return
try:
ab_sess = onnxruntime.InferenceSession(AB_MODEL_PATH, providers=["CPUExecutionProvider"])
ab_input = ab_sess.get_inputs()[0].name
ab_size = (ab_sess.get_inputs()[0].shape[3], ab_sess.get_inputs()[0].shape[2])
except Exception as e:
print(f"[ERR] A/B模型加载失败: {e}")
hw.cleanup()
cap.release()
return
state = STATE_WAIT_START
start_time = None
crosswalk_start = None
lane_change_dir = None
lane_change_confirm_count = 0
ab_result = None
done_flags = {
STATE_START: False,
STATE_CROSSWALK: False,
STATE_LANE_CHANGE: False,
STATE_AB_RECOG: False,
STATE_PARK: False
}
print("[SYSTEM] 系统就绪,等待拆除挡板...")
try:
while True:
loop_start = time.time()
ret, frame = cap.read()
if not ret:
print("[ERR] 读帧失败")
break
display = frame.copy()
lane_error, lane_info = get_lane_error(frame)
left_fit, right_fit, roi_y_start, mid_points, binary = (None, None, None, [], None)
if lane_info is not None:
left_fit, right_fit, roi_y_start, mid_points, binary = lane_info
if state == STATE_WAIT_START:
contours, _ = detect_color_contours(frame, SIGN_COLOR_LOWER, SIGN_COLOR_UPPER, SIGN_AREA_MIN)
if contours:
display = draw_chinese(display, "检测到挡板,等待移除...", (10, H // 2 - 20), (0, 0, 255))
hw.stop()
else:
if start_time is None:
start_time = time.time()
elif time.time() - start_time >= 0.5:
state = STATE_START
done_flags[STATE_START] = False
start_time = time.time()
display = draw_chinese(display, "等待拆挡板...", (10, 30), (255, 0, 0))
elif state == STATE_START:
if lane_error is not None:
hw.pid_control(lane_error)
if left_fit and right_fit:
if not done_flags[STATE_START]:
done_flags[STATE_START] = True
state = STATE_TRACK
else:
display = draw_chinese(display, "发车等待:查找起跑线...", (10, 60), (0, 255, 255))
elif state == STATE_TRACK:
if lane_error is not None:
servo_angle = hw.pid_control(lane_error)
display = draw_chinese(display, f"舵机: {int(servo_angle)}°", (10, 60), (200, 200, 0))
else:
if hw.last_known_direction == "left":
angle = max(SERVO_MIN_ANGLE, SERVO_MID_ANGLE - 5)
elif hw.last_known_direction == "right":
angle = min(SERVO_MAX_ANGLE, SERVO_MID_ANGLE + 5)
else:
angle = SERVO_MID_ANGLE
hw.set_servo_angle(angle)
hw.set_motor_speed(FORWARD_BASE - 100)
display = draw_chinese(display, "丢失车道线,智能恢复...", (10, 60), (255, 0, 0))
cone_found, bbox, _ = detect_blue_cone(frame)
if cone_found and bbox is not None:
x, y, w, h = bbox
center_x = x + w // 2
bottom_y = y + h
if bottom_y > int(H * 0.5):
display = cv2.rectangle(display, (x, y), (x + w, y + h), (0, 0, 255), 2)
if center_x < W // 2:
new_angle = min(SERVO_MAX_ANGLE, hw.current_angle + 8)
else:
new_angle = max(SERVO_MIN_ANGLE, hw.current_angle - 8)
hw.set_servo_angle(new_angle)
hw.set_motor_speed(FORWARD_BASE - 150)
if detect_crosswalk(frame) and not done_flags[STATE_CROSSWALK]:
state = STATE_CROSSWALK
crosswalk_start = time.time()
done_flags[STATE_CROSSWALK] = True
hw.stop()
display = draw_chinese(display, f"补偿: {TURN_COMPENSATION}°", (10, 90), (0, 200, 200))
display = draw_chinese(display, f"方向: {hw.last_known_direction}", (10, 120), (0, 200, 200))
display = draw_chinese(display, "阶段:巡线避障", (10, 30), (0, 255, 0))
elif state == STATE_CROSSWALK:
elapsed = time.time() - crosswalk_start
remain_time = max(0, CROSSWALK_STOP_DURATION - elapsed)
display = draw_chinese(display, f"斑马线停车: {int(remain_time)}s", (10, 60), (0, 255, 255))
hw.stop()
if elapsed >= CROSSWALK_STOP_DURATION:
state = STATE_LANE_CHANGE
lane_change_dir = None
lane_change_confirm_count = 0
elif state == STATE_LANE_CHANGE:
display = draw_chinese(display, "阶段:变道任务", (10, 30), (255, 165, 0))
if lane_error is not None:
hw.pid_control(lane_error)
mask = generate_black_text_mask(frame)
pred, conf = model_infer(mask, arrow_sess, arrow_input, arrow_size, ARROW_CLASS_NAMES)
if pred != "Waiting...":
if lane_change_dir is None:
lane_change_dir = pred
lane_change_confirm_count = 1
else:
if pred == lane_change_dir:
lane_change_confirm_count += 1
else:
lane_change_dir = pred
lane_change_confirm_count = 1
display = draw_chinese(
display,
f"箭头: {pred} ({conf:.2f}) 连续: {lane_change_confirm_count}/{REQUIRED_CONSECUTIVE}",
(10, 70),
(255, 255, 0)
)
if lane_change_confirm_count >= REQUIRED_CONSECUTIVE:
print(f"[LANE] 执行变道: {lane_change_dir}")
if lane_change_dir == "arrow_left":
hw.set_servo_angle(max(SERVO_MIN_ANGLE, hw.current_angle - 20))
else:
hw.set_servo_angle(min(SERVO_MAX_ANGLE, hw.current_angle + 20))
time.sleep(0.6)
lane_change_dir = "executed"
if lane_change_dir == "executed":
blue_cnts, _ = detect_color_contours(frame, OBSTACLE_COLOR_LOWER, OBSTACLE_COLOR_UPPER, 100)
yellow_cnts, _ = detect_color_contours(frame, YELLOW_COLOR_LOWER, YELLOW_COLOR_UPPER, 100)
if blue_cnts and yellow_cnts:
hw.set_servo_angle(SERVO_MID_ANGLE)
state = STATE_AB_RECOG
done_flags[STATE_LANE_CHANGE] = True
elif state == STATE_AB_RECOG:
display = draw_chinese(display, "阶段:A/B识别", (10, 30), (255, 0, 255))
if lane_error is not None:
hw.pid_control(lane_error)
mask = generate_black_text_mask(frame)
pred, conf = model_infer(mask, ab_sess, ab_input, ab_size, AB_CLASS_NAMES)
display = draw_chinese(display, f"A/B识别: {pred} ({conf:.2f})", (10, 70), (255, 255, 0))
if pred in AB_CLASS_NAMES and conf >= CONF_THRESHOLD:
ab_result = pred
state = STATE_PARK
done_flags[STATE_AB_RECOG] = True
elif state == STATE_PARK:
display = draw_chinese(display, "阶段:停车任务", (10, 30), (0, 165, 255))
if lane_error is not None:
hw.pid_control(lane_error)
yellow_cnts, _ = detect_color_contours(frame, YELLOW_COLOR_LOWER, YELLOW_COLOR_UPPER, 50)
found = False
if yellow_cnts:
cnt = max(yellow_cnts, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
cx = x + w // 2
park_area = PARK_AREA_LEFT if ab_result == "A" else PARK_AREA_RIGHT
if park_area[0] < cx < park_area[1]:
hw.stop()
print(f"[PARK] 进入 {ab_result} 区停车完成")
state = STATE_FINISH
done_flags[STATE_PARK] = True
found = True
else:
target_cx = (park_area[0] + park_area[1]) // 2
e = target_cx - cx
hw.pid_control(e)
if not found:
display = draw_chinese(display, "搜索黄线停车线...", (10, 70), (255, 0, 0))
elif state == STATE_FINISH:
display = draw_chinese(display, "任务完成!", (W // 2 - 80, H // 2 - 10), (0, 255, 0))
cv2.imshow("SmartCar", display)
cv2.waitKey(2000)
break
if mid_points:
for i in range(len(mid_points) - 1):
cv2.line(display, mid_points[i], mid_points[i + 1], (0, 0, 255), 3)
if binary is not None:
try:
thumb = cv2.resize(binary, (160, 120))
display[10:130, W-170:W-10] = cv2.cvtColor(thumb, cv2.COLOR_GRAY2BGR)
except:
pass
display = draw_chinese(display, f"状态: {state}", (8, 8), (255, 255, 255))
cv2.imshow("SmartCar", display)
if cv2.waitKey(1) & 0xFF == ord('q'):
print("[USER] 手动退出")
break
elapsed_loop = time.time() - loop_start
time.sleep(max(0, 1.0 / FPS_TARGET - elapsed_loop))
except KeyboardInterrupt:
print("[USER] 中断退出")
finally:
cap.release()
cv2.destroyAllWindows()
hw.cleanup()
print("[SYSTEM] 退出完成")
if __name__ == "__main__":
main()
将捕捉画面截去上部分2/3,保留下1/3,贴进跑道提高准确度
最新发布