This commit is contained in:
yrx
2026-05-29 16:24:04 +08:00
parent 575e690868
commit 64722f4d73
17 changed files with 1647 additions and 77 deletions

View File

@@ -6,6 +6,7 @@
"""
import _thread
import json
import math
import os
import binascii
from maix import time
@@ -34,9 +35,13 @@ class LaserManager:
self._calibration_active = False
self._calibration_result = None
self._calibration_lock = threading.Lock()
self._remote_detect_active = False
self._remote_detect_lock = threading.Lock()
self._remote_detect_result = None
self._laser_point = None
self._laser_turned_on = False
self._last_frame_with_ellipse = None # 保存绘制了椭圆的图像(用于调试/显示)
self._remote_detect_last_pos = None
self._initialized = True
# ==================== 状态访问(只读属性)====================
@@ -113,7 +118,7 @@ class LaserManager:
# 正常模式:从配置文件加载
try:
if "laser_config.json" in os.listdir("/root"):
if os.path.exists(config.CONFIG_FILE):
with open(config.CONFIG_FILE, "r") as f:
data = json.load(f)
if isinstance(data, list) and len(data) == 2:
@@ -124,11 +129,240 @@ class LaserManager:
raise ValueError
else:
self._laser_point = config.DEFAULT_LASER_POINT
except:
except Exception as e:
if self.logger:
self.logger.warning(f"[LASER] 加载激光点失败,使用默认值: {e}")
self._laser_point = config.DEFAULT_LASER_POINT
return self._laser_point
@property
def remote_detect_active(self):
with self._remote_detect_lock:
return self._remote_detect_active
def get_remote_detect_result(self):
"""获取并清除远程激光识别结果 (x, y) 或 None。"""
with self._remote_detect_lock:
result = self._remote_detect_result
self._remote_detect_result = None
return result
def remote_detect_tick(self, frame):
"""
主循环显示路径调用的轻量 tick。
兼容旧调用点:当前远程识别由后台线程处理,这里不做重计算,
仅保留接口避免 AttributeError。
"""
return None
def overlay_remote_detect_preview(self, frame):
"""
在预览画面叠加远程识别点与坐标文本。
"""
try:
import cv2
from maix import image
with self._remote_detect_lock:
pos = self._remote_detect_last_pos
if not pos:
return frame
img_cv = image.image2cv(frame, False, False)
if img_cv is None or img_cv.size == 0:
return frame
x, y = int(pos[0]), int(pos[1])
h, w = img_cv.shape[:2]
if x < 0 or y < 0 or x >= w or y >= h:
return frame
color = (255, 0, 0) # RGB
cv2.circle(img_cv, (x, y), 8, color, 2)
cv2.line(img_cv, (x - 12, y), (x + 12, y), color, 1)
cv2.line(img_cv, (x, y - 12), (x, y + 12), color, 1)
cv2.putText(img_cv, f"laser=({x},{y})", (max(5, x + 10), max(20, y - 10)),
cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 1, cv2.LINE_AA)
return image.cv2image(img_cv, False, False)
except Exception as e:
if self.logger:
self.logger.debug(f"[LASER-REMOTE] overlay 绘制失败: {e}")
return frame
def _set_remote_detect_result(self, result):
with self._remote_detect_lock:
self._remote_detect_result = result
def set_hardcoded_laser_point(self, x, y):
"""更新 config.HARDCODE_LASER_POINT_VALUETCP cmd=201"""
try:
ix = int(round(float(x)))
iy = int(round(float(y)))
except (TypeError, ValueError) as e:
raise ValueError(f"invalid laser point ({x!r}, {y!r})") from e
config.HARDCODE_LASER_POINT = True
config.HARDCODE_LASER_POINT_VALUE = (ix, iy)
self._laser_point = (ix, iy)
try:
with open(config.CONFIG_FILE, "w") as f:
json.dump([ix, iy], f)
except Exception as e:
if self.logger:
self.logger.warning(f"[LASER] 保存硬编码激光点到本地失败: {e}")
raise
if self.logger:
self.logger.info(
f"[LASER] 已设置硬编码激光点 HARDCODE_LASER_POINT_VALUE=({ix}, {iy}) 并已保存到 {config.CONFIG_FILE}"
)
return ix, iy
def start_remote_laser_detect(self):
"""
启动远程激光识别会话TCP cmd=200开激光后持续检测。
每次稳定 3s 上报一次坐标,外循环直到 cmd=201 调用 stop_remote_laser_detect()。
Returns:
True 已启动False 会话已在运行
"""
with self._remote_detect_lock:
if self._remote_detect_active:
return False
self._remote_detect_active = True
self._remote_detect_result = None
self._remote_detect_last_pos = None
_thread.start_new_thread(self._remote_laser_detect_worker, ())
if self.logger:
self.logger.info("[LASER] 远程激光识别已启动 (cmd=200)")
return True
def stop_remote_laser_detect(self):
with self._remote_detect_lock:
self._remote_detect_active = False
def _remote_laser_detect_worker(self):
from camera_manager import camera_manager
stable_sec = float(getattr(config, "LASER_REMOTE_DETECT_STABLE_SEC", 3.0))
max_move = float(getattr(config, "LASER_REMOTE_DETECT_MAX_MOVE_PX", 12.0))
sample_ms = int(getattr(config, "LASER_REMOTE_DETECT_SAMPLE_MS", 80))
min_samples = int(getattr(config, "LASER_REMOTE_DETECT_MIN_SAMPLES", 8))
warmup_ms = int(getattr(config, "LASER_REMOTE_DETECT_WARMUP_MS", 500))
stable_ms = int(max(500, stable_sec * 1000))
samples = []
miss_count = 0
stable_hit_count = 0
reported = False
try:
if not self._laser_turned_on:
try:
self.turn_on_laser()
except Exception as e:
if self.logger:
self.logger.warning(f"[LASER] cmd200 worker 开激光失败: {e}")
if warmup_ms > 0:
if self.logger:
self.logger.info(f"[LASER] cmd200 激光预热 {warmup_ms}ms …")
time.sleep_ms(warmup_ms)
if self.logger:
self.logger.info("[LASER] 远程识别外循环已启动,直至 cmd=201 停止")
while True:
with self._remote_detect_lock:
if not self._remote_detect_active:
if self.logger:
self.logger.info("[LASER] 远程识别会话结束 (cmd=201 或取消)")
return
try:
frame = camera_manager.read_frame()
pos = self.find_red_laser_remote(frame)
except Exception as e:
if self.logger:
self.logger.warning(f"[LASER] 远程识别帧异常: {e}")
pos = None
time.sleep_ms(sample_ms)
continue
now_ms = time.ticks_ms()
if pos is None:
miss_count += 1
samples.clear()
stable_hit_count = 0
if miss_count == 1 or miss_count % 40 == 0:
if self.logger:
self.logger.info(
f"[LASER-REMOTE] 本帧未检出激光点(累计 {miss_count} 帧),"
f"全图多策略搜索中…"
)
time.sleep_ms(sample_ms)
continue
miss_count = 0
x, y = float(pos[0]), float(pos[1])
samples.append((now_ms, x, y))
cutoff = now_ms - stable_ms
samples = [(t, px, py) for t, px, py in samples if t >= cutoff]
if len(samples) < 2:
time.sleep_ms(sample_ms)
continue
xs = [s[1] for s in samples]
ys = [s[2] for s in samples]
span = max(
max(xs) - min(xs),
max(ys) - min(ys),
)
for i in range(len(samples)):
for j in range(i + 1, len(samples)):
d = math.hypot(
samples[i][1] - samples[j][1],
samples[i][2] - samples[j][2],
)
span = max(span, d)
if span > max_move:
if self.logger:
self.logger.debug(
f"[LASER] 检测到大幅位移 span={span:.1f}px>{max_move},重新计时"
)
samples.clear()
stable_hit_count = 0
time.sleep_ms(sample_ms)
continue
window_ms = samples[-1][0] - samples[0][0]
if window_ms >= stable_ms and len(samples) >= min_samples:
fx = int(round(sum(xs) / len(xs)))
fy = int(round(sum(ys) / len(ys)))
stable_hit_count += 1
if self.logger:
self.logger.info(
f"[LASER] 远程识别稳定命中 {stable_hit_count}/3 span={span:.1f}px → ({fx}, {fy})"
)
samples.clear()
if stable_hit_count >= 3 and not reported:
reported = True
self._set_remote_detect_result(
{"result":"laser_detect_ok", "x": fx, "y": fy}
)
if self.logger:
self.logger.info(
f"[LASER] 已连续3次坐标稳定完成上报继续等待 cmd=201 关闭会话"
)
time.sleep_ms(sample_ms)
continue
time.sleep_ms(sample_ms)
finally:
if self.logger:
self.logger.info("[LASER] 远程识别线程退出,等待下一次 cmd=200")
with self._remote_detect_lock:
self._remote_detect_active = False
def save_laser_point(self, point):
"""保存激光中心点到配置文件
如果启用硬编码模式,则不保存(直接返回 True
@@ -831,6 +1065,172 @@ class LaserManager:
# 使用原来的最亮点方法
return self._find_red_laser_brightest(frame, threshold, search_radius, ellipse_params)
def find_red_laser_remote(self, frame):
"""
cmd=200 远程识别专用:全图搜索、多策略、放宽阈值,不限距画面中心距离。
常规 find_red_laser 仅搜中心 ±LASER_SEARCH_RADIUS 且距中心 >50px 会丢弃。
"""
import cv2
import numpy as np
from maix import image
img_cv = image.image2cv(frame, False, False)
if img_cv is None or img_cv.size == 0:
return None
h, w = img_cv.shape[:2]
r = img_cv[:, :, 0].astype(np.int32)
g = img_cv[:, :, 1].astype(np.int32)
b = img_cv[:, :, 2].astype(np.int32)
brightness = r + g + b
red_ratio = float(getattr(config, "LASER_RED_RATIO", 1.5))
ratio_lo = max(1.15, red_ratio - 0.35)
strategies = []
base_th = int(getattr(config, "LASER_DETECTION_THRESHOLD", 140))
for th in (base_th, 120, 100, 80, 60):
mask = (
(r > th)
& (r > g * ratio_lo)
& (r > b * ratio_lo)
)
strategies.append(("rgb", th, mask))
oe_th = int(getattr(config, "LASER_OVEREXPOSED_THRESHOLD", 200))
oe_diff = int(getattr(config, "LASER_OVEREXPOSED_DIFF", 10))
mask_oe = (
(r > oe_th - 30)
& (g > oe_th - 40)
& (b > oe_th - 40)
& (r >= g)
& (r >= b)
& ((r - g) > max(5, oe_diff - 5))
& ((r - b) > max(5, oe_diff - 5))
)
strategies.append(("overexposed", oe_th, mask_oe))
mask_bright = (brightness > 380) & (r >= g) & (r >= b) & ((r - g) > 3)
strategies.append(("bright", 0, mask_bright))
hsv = cv2.cvtColor(img_cv, cv2.COLOR_RGB2HSV)
hc, sc, vc = cv2.split(hsv)
mask_hsv = ((hc <= 18) | (hc >= 162)) & (sc >= 60) & (vc >= 60)
strategies.append(("hsv", 0, mask_hsv))
best_pos = None
best_score = -1.0
best_tag = None
max_area = float(getattr(config, "LASER_REMOTE_MAX_AREA", 300.0))
min_circularity = float(getattr(config, "LASER_REMOTE_MIN_CIRCULARITY", 0.25))
for name, th, mask in strategies:
m = (mask.astype(np.uint8)) * 255
if cv2.countNonZero(m) == 0:
continue
contours, _ = cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if not contours:
continue
for cnt in contours:
area = cv2.contourArea(cnt)
if area < 1.5 or area > max_area:
continue
peri = cv2.arcLength(cnt, True)
if peri <= 0:
continue
circularity = float(4.0 * math.pi * area / (peri * peri))
if circularity < min_circularity:
continue
M = cv2.moments(cnt)
if M["m00"] <= 0:
continue
cx = float(M["m10"] / M["m00"])
cy = float(M["m01"] / M["m00"])
ix, iy = int(round(cx)), int(round(cy))
if ix < 0 or iy < 0 or ix >= w or iy >= h:
continue
local_r = float(r[iy, ix])
score = area * local_r * (1.0 + local_r / 255.0) * (0.5 + circularity)
if score > best_score:
best_score = score
best_pos = (ix, iy)
best_tag = (name, th, area)
if best_pos is not None:
with self._remote_detect_lock:
self._remote_detect_last_pos = best_pos
self._save_remote_detect_debug_image(frame, best_pos, best_tag)
if self.logger:
self.logger.info(
f"[LASER-REMOTE] 检测到激光点 {best_pos} "
f"strategy={best_tag[0]} th={best_tag[1]} area={best_tag[2]:.1f}"
)
elif self.logger:
self.logger.debug("[LASER-REMOTE] 未通过面积/圆度过滤")
return best_pos
def _save_remote_detect_debug_image(self, frame, pos, tag=None):
"""保存远程识别调试图:叠加激光坐标并落盘。"""
try:
if not bool(getattr(config, "SAVE_IMAGE_ENABLED", True)):
return
import cv2
from maix import image
img_cv = image.image2cv(frame, False, False)
if img_cv is None or img_cv.size == 0:
return
x, y = int(pos[0]), int(pos[1])
h, w = img_cv.shape[:2]
if x < 0 or y < 0 or x >= w or y >= h:
return
cv2.circle(img_cv, (x, y), 8, (255, 0, 0), 2)
cv2.line(img_cv, (x - 12, y), (x + 12, y), (255, 0, 0), 1)
cv2.line(img_cv, (x, y - 12), (x, y + 12), (255, 0, 0), 1)
desc = ""
if tag:
desc = f" {tag[0]} th={tag[1]} area={tag[2]:.1f}"
cv2.putText(
img_cv,
f"laser=({x},{y}){desc}",
(10, 24),
cv2.FONT_HERSHEY_SIMPLEX,
0.55,
(255, 0, 0),
1,
cv2.LINE_AA,
)
base_dir = getattr(config, "PHOTO_DIR", "/root/phot")
debug_dir = f"{base_dir}/laser_remote"
try:
if debug_dir not in os.listdir("/root") and "/" not in debug_dir.replace("/root/", ""):
os.mkdir(debug_dir)
else:
try:
os.makedirs(debug_dir, exist_ok=True)
except Exception:
pass
except Exception:
try:
os.makedirs(debug_dir, exist_ok=True)
except Exception:
return
ts = int(time.ticks_ms())
filename = f"{debug_dir}/remote_{x}_{y}_{ts}.jpg"
out = image.cv2image(img_cv, False, False)
out.save(filename)
if self.logger:
self.logger.info(f"[LASER-REMOTE] 调试图已保存: {filename}")
except Exception as e:
if self.logger:
self.logger.warning(f"[LASER-REMOTE] 保存调试图失败: {e}")
def calibrate_laser_position(self, timeout_ms=8000, check_sharpness=True):
"""
执行激光校准:循环拍照 → 检测靶心 → 检查激光点清晰度 → 找红点 → 保存坐标