所有
This commit is contained in:
404
laser_manager.py
404
laser_manager.py
@@ -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_VALUE(TCP 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):
|
||||
"""
|
||||
执行激光校准:循环拍照 → 检测靶心 → 检查激光点清晰度 → 找红点 → 保存坐标
|
||||
|
||||
Reference in New Issue
Block a user