Author SHA1 Message Date
linyimin e4d8454947 fix: update version 2026-09-02 11:26:44 +08:00
linyimin c09189332d fix: 摄像头翻转 2026-09-02 11:25:33 +08:00
yrx a00baa1770 two ! 2026-09-02 11:22:01 +08:00
19 changed files with 38 additions and 104 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3 -3
View File
@@ -1,6 +1,6 @@
id: t11 id: t11
name: t11 name: t11
version: 2.18.2 version: 3.0.0
author: t11 author: t11
icon: '' icon: ''
desc: t11 desc: t11
@@ -18,8 +18,8 @@ files:
- laser_manager.py - laser_manager.py
- logger_manager.py - logger_manager.py
- main.py - main.py
- model_317828.cvimodel - model_285484.cvimodel
- model_317828.mud - model_285484.mud
- network.py - network.py
- ota_curl.sh - ota_curl.sh
- ota_manager.py - ota_manager.py
+7 -10
View File
@@ -241,10 +241,10 @@ TRIANGLE_BLACKHAT_KERNEL_FRAC = 0.018 # 核大小 ≈ min(h,w)*frac,取奇数
# ── YOLO(NPU) 靶环 ROI → 裁剪后再跑三角形(减小 CPU 处理面积)────────────────── # ── YOLO(NPU) 靶环 ROI → 裁剪后再跑三角形(减小 CPU 处理面积)──────────────────
# 日志里 net_in=W×H 来自 .mud 模型(det.input_width/height),不是这里配置的。 # 日志里 net_in=W×H 来自 .mud 模型(det.input_width/height),不是这里配置的。
TRIANGLE_YOLO_ROI_ENABLE = True TRIANGLE_YOLO_ROI_ENABLE = True
TRIANGLE_YOLO_MODEL_PATH = APP_DIR + "/model_317828.mud" TRIANGLE_YOLO_MODEL_PATH = APP_DIR + "/model_270139.mud"
# 参与 ROI 的类别:多类时只填「整靶/靶环」的 id;不要填角标类,否则 union 仍可对,但 largest 会偏小。 # 参与 ROI 的类别:多类时只填「整靶/靶环」的 id;不要填角标类,否则 union 仍可对,但 largest 会偏小。
TRIANGLE_YOLO_RING_CLASS_IDS = (0,) TRIANGLE_YOLO_RING_CLASS_IDS = (0,)
TRIANGLE_YOLO_CONF_TH = 0.9 TRIANGLE_YOLO_CONF_TH = 0.7
TRIANGLE_YOLO_IOU_TH = 0.45 TRIANGLE_YOLO_IOU_TH = 0.45
# YOLO 首次/临界帧可能在高阈值下 0 框;启用后仅在 0 候选时用较低阈值重试一次。 # YOLO 首次/临界帧可能在高阈值下 0 框;启用后仅在 0 候选时用较低阈值重试一次。
# 后续仍会经过 min_box_side、ROI aspect、三角形几何校验,避免直接放大假阳性。 # 后续仍会经过 min_box_side、ROI aspect、三角形几何校验,避免直接放大假阳性。
@@ -271,9 +271,9 @@ TRIANGLE_YOLO_PRELOAD_ON_BOOT = False
# YOLO target size classification: class 0=20cm, class 1=40cm. # YOLO target size classification: class 0=20cm, class 1=40cm.
TARGET_CLASS_YOLO_ENABLE = True TARGET_CLASS_YOLO_ENABLE = True
TARGET_CLASS_YOLO_MODEL_PATH = APP_DIR + "/model_317828.mud" TARGET_CLASS_YOLO_MODEL_PATH = APP_DIR + "/model_285484.mud"
TARGET_CLASS_YOLO_LABELS = (20, 40) TARGET_CLASS_YOLO_LABELS = (20, 40)
TARGET_CLASS_YOLO_CONF_TH = 0.66 TARGET_CLASS_YOLO_CONF_TH = 0.50
TARGET_CLASS_YOLO_IOU_TH = 0.45 TARGET_CLASS_YOLO_IOU_TH = 0.45
TARGET_CLASS_YOLO_RETRY_ON_EMPTY = False TARGET_CLASS_YOLO_RETRY_ON_EMPTY = False
TARGET_CLASS_YOLO_RETRY_CONF_TH = 0.25 TARGET_CLASS_YOLO_RETRY_CONF_TH = 0.25
@@ -333,17 +333,14 @@ LOG_QUEUE_MAXSIZE = 10000 # 日志队列上限
MAX_CMD_THREADS = 10 # 并发命令线程上限(防止服务器下发命令时无限创建线程) MAX_CMD_THREADS = 10 # 并发命令线程上限(防止服务器下发命令时无限创建线程)
# ==================== 图像保存配置 ==================== # ==================== 图像保存配置 ====================
SAVE_IMAGE_ENABLED = True # 是否保存图像(True=保存,False=不保存) SAVE_IMAGE_ENABLED = False # 是否保存图像(True=保存,False=不保存)
SAVE_IMAGE_ON_FAILURE = False # 检测失败时是否强制保存图像(供调试测试用) SAVE_IMAGE_ON_FAILURE = True # 检测失败时是否强制保存图像(供调试测试用)
PHOTO_DIR = "/root/phot" # 照片存储目录 PHOTO_DIR = "/root/phot" # 照片存储目录
MAX_IMAGES = 1000 MAX_IMAGES = 1000
SAVE_RAW_IMAGE_ENABLED = False # 原图保存功能保留,但当前关闭
RAW_IMAGE_DIR = PHOTO_DIR + "/raw"
RAW_IMAGE_MAX_IMAGES = MAX_IMAGES
# Stage2 调试目录(默认 PHOTO_DIR/stage2_roi)内 JPEG 最多保留张数;None 表示与 MAX_IMAGES 相同 # Stage2 调试目录(默认 PHOTO_DIR/stage2_roi)内 JPEG 最多保留张数;None 表示与 MAX_IMAGES 相同
TRIANGLE_BLACK_YOLO_STAGE2_ROI_MAX_IMAGES = None TRIANGLE_BLACK_YOLO_STAGE2_ROI_MAX_IMAGES = None
SHOW_CAMERA_PHOTO_WHILE_SHOOTING = False # 关闭拍摄时显示 SHOW_CAMERA_PHOTO_WHILE_SHOOTING = False # 是否在拍摄时显示摄像头图像(True=显示,False=不显示),建议在连着USB测试过程中打开
# ==================== OTA配置 ==================== # ==================== OTA配置 ====================
MAX_BACKUPS = 5 MAX_BACKUPS = 5
+18 -20
View File
@@ -136,7 +136,6 @@ def cmd_str():
sync_system_time_from_4g() sync_system_time_from_4g()
# 2.1 WiFi 热点配网兜底:仅当 STA 与 4G 均不可用时起 AP + HTTP;提交后删 /boot/wifi.ap、建 wifi.sta 并 reboot # 2.1 WiFi 热点配网兜底:仅当 STA 与 4G 均不可用时起 AP + HTTP;提交后删 /boot/wifi.ap、建 wifi.sta 并 reboot
_ota_pending_path = f"{config.APP_DIR}/ota_pending.json"
try: try:
from wifi_config_httpd import maybe_start_wifi_ap_fallback from wifi_config_httpd import maybe_start_wifi_ap_fallback
@@ -172,10 +171,8 @@ def cmd_str():
and bool(getattr(config, "TARGET_CLASS_YOLO_PRELOAD_ON_BOOT", True)) and bool(getattr(config, "TARGET_CLASS_YOLO_PRELOAD_ON_BOOT", True))
) )
_preload_yolo = _preload_yolo or _need_black_preload or _need_target_preload _preload_yolo = _preload_yolo or _need_black_preload or _need_target_preload
if _preload_yolo and not os.path.exists(f"{config.APP_DIR}/ota_pending.json"): if _preload_yolo:
preload_yolo_detector(logger) preload_yolo_detector(logger)
elif _preload_yolo and logger:
logger.warning("[YOLO] ota_pending.json found; skip model preload until rollback check")
except Exception as e: except Exception as e:
if logger: if logger:
logger.warning(f"[YOLO-ROI] 启动预加载异常(不影响后续射箭): {e}") logger.warning(f"[YOLO-ROI] 启动预加载异常(不影响后续射箭): {e}")
@@ -257,11 +254,7 @@ def cmd_str():
network_manager.read_device_id() network_manager.read_device_id()
# 5. 创建照片存储目录(如果启用图像保存或检测失败时强制保存) # 5. 创建照片存储目录(如果启用图像保存或检测失败时强制保存)
if ( if config.SAVE_IMAGE_ENABLED or getattr(config, "SAVE_IMAGE_ON_FAILURE", False):
config.SAVE_IMAGE_ENABLED
or getattr(config, "SAVE_IMAGE_ON_FAILURE", False)
or getattr(config, "SAVE_RAW_IMAGE_ENABLED", False)
):
photo_dir = config.PHOTO_DIR photo_dir = config.PHOTO_DIR
if photo_dir not in os.listdir("/root"): if photo_dir not in os.listdir("/root"):
try: try:
@@ -293,13 +286,12 @@ def cmd_str():
logger.info("系统准备完成...") logger.info("系统准备完成...")
last_adc_trigger = 0 last_adc_trigger = 0
trigger_adc_val = 0 # 触发时的气压值,气压需降回此值以下才能再次触发
# 读取一次ADC初始值,防止开机时传感器已有压力导致误触发 # 读取一次ADC初始值,防止开机时传感器已有压力导致误触发
enable_check = True
try: try:
last_adc_val = hardware_manager.adc_obj.read() last_adc_val = hardware_manager.adc_obj.read()
except Exception: except Exception:
last_adc_val = 0 last_adc_val = 0
peak_adc_val = 0 # 当前周期内的压力峰值
# 气压采样:减少日志频率(每 N 个点输出一条),避免 logger.debug 拖慢采样 # 气压采样:减少日志频率(每 N 个点输出一条),避免 logger.debug 拖慢采样
PRESSURE_BATCH_SIZE = 100 PRESSURE_BATCH_SIZE = 100
@@ -389,16 +381,22 @@ def cmd_str():
pressure_max = adc_val pressure_max = adc_val
if len(pressure_buf) >= PRESSURE_BATCH_SIZE: if len(pressure_buf) >= PRESSURE_BATCH_SIZE:
_flush_pressure_buf("batch") _flush_pressure_buf("batch")
# 突变增量检测:压力增量大于300时触发 # 峰值检测:压力从峰值下降时触发,确保捕获到最大冲击时刻
# 触发后需等气压降到触发值以下才重新检测增量 if adc_val > peak_adc_val:
if adc_val < trigger_adc_val : peak_adc_val = adc_val # 更新峰值
enable_check = True if (peak_adc_val >= config.ADC_TRIGGER_THRESHOLD
if (adc_val - last_adc_val) > 500 and enable_check: and adc_val < peak_adc_val
and last_adc_val >= peak_adc_val):
# 封顶后下降沿触发:peak是最大值,当前值开始下降,且上次值还在peak位置
hardware_manager.start_idle_timer() # 重新计时 hardware_manager.start_idle_timer() # 重新计时
diff_ms = current_time - last_adc_trigger
if diff_ms < 3000:
peak_adc_val = 0 # 去抖期间重置峰值
time.sleep_ms(5)
continue
last_adc_trigger = current_time last_adc_trigger = current_time
trigger_adc_val = adc_val # 记录触发时的气压 peak_adc_val = 0 # 触发后重置峰
last_adc_val = adc_val # 更新基准值,防止连续增量误触发 # 触发前先把缓存刷出来,避免波形被长耗时处理截断
enable_check = False
_flush_pressure_buf("before_trigger") _flush_pressure_buf("before_trigger")
try: try:
@@ -417,7 +415,7 @@ def cmd_str():
camera_manager.show(camera_manager.read_frame()) camera_manager.show(camera_manager.read_frame())
except Exception as e: except Exception as e:
pass pass
time.sleep_ms(1) time.sleep_ms(5)
last_adc_val = adc_val last_adc_val = adc_val
except Exception as e: except Exception as e:
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
[basic] [basic]
type = cvimodel type = cvimodel
model = model_317828.cvimodel model = model_285484.cvimodel
[extra] [extra]
model_type = yolov5 model_type = yolov5
+5 -13
View File
@@ -8,7 +8,7 @@ from laser_manager import laser_manager
from logger_manager import logger_manager from logger_manager import logger_manager
from network import network_manager from network import network_manager
from triangle_target import load_camera_from_xml, load_triangle_positions, try_triangle_scoring from triangle_target import load_camera_from_xml, load_triangle_positions, try_triangle_scoring
from vision import estimate_distance, detect_circle_v3, enqueue_save_shot, enqueue_save_raw_shot from vision import estimate_distance, detect_circle_v3, enqueue_save_shot
from maix import image, time from maix import image, time
# 缓存相机标定与三角形位置,避免每次射箭重复读磁盘 # 缓存相机标定与三角形位置,避免每次射箭重复读磁盘
@@ -322,11 +322,6 @@ def process_shot(adc_val):
try: try:
frame = camera_manager.read_frame() frame = camera_manager.read_frame()
# 在任何检测和绘图之前复制原始帧;默认由配置关闭,不增加量产开销。
from shot_id_generator import shot_id_generator
shot_id = shot_id_generator.generate_id()
enqueue_save_raw_shot(frame, shot_id)
# 网络事件移到拍照之后,避免阻塞拍照 # 网络事件移到拍照之后,避免阻塞拍照
network_manager.safe_enqueue({"shoot_event": "start"}, msg_type=2, high=True) network_manager.safe_enqueue({"shoot_event": "start"}, msg_type=2, high=True)
@@ -385,6 +380,10 @@ def process_shot(adc_val):
if dx is None and dy is None and logger: if dx is None and dy is None and logger:
logger.warning("[MAIN] 未检测到偏移量(三角形与圆形均失败),但会保存图像") logger.warning("[MAIN] 未检测到偏移量(三角形与圆形均失败),但会保存图像")
# 生成射箭ID
from shot_id_generator import shot_id_generator
shot_id = shot_id_generator.generate_id()
if logger: if logger:
logger.info(f"[MAIN] 射箭ID: {shot_id}") logger.info(f"[MAIN] 射箭ID: {shot_id}")
@@ -442,13 +441,6 @@ def process_shot(adc_val):
inner_data["ellipse_center_x"] = None inner_data["ellipse_center_x"] = None
inner_data["ellipse_center_y"] = None inner_data["ellipse_center_y"] = None
upload_time_ms = int(time_std.time() * 1000)
upload_time_sec, upload_time_millis = divmod(upload_time_ms, 1000)
inner_data["upload_time"] = (
time_std.strftime("%Y-%m-%d %H:%M:%S", time_std.localtime(upload_time_sec))
+ f".{upload_time_millis:03d}"
)
report_data = {"cmd": 1, "data": inner_data} report_data = {"cmd": 1, "data": inner_data}
if logger: if logger:
logger.info( logger.info(
View File
Binary file not shown.
-4
View File
@@ -31,7 +31,3 @@
# 2.15.18 wifi连接成功重新登录 # 2.15.18 wifi连接成功重新登录
# 2.16.4 优化射箭延迟 # 2.16.4 优化射箭延迟
# 2.17.0 yolo标靶类别识别 # 2.17.0 yolo标靶类别识别
# 2.17.1 26-08-19 1739 压力传感修改 增量方式
# 2.17.2 26-08-24 1756 靶纸识别模型更替
# 2.17.3 26-08-25 957 原图拍摄开关
# 2.17.4 26-08-25 1457 模型修改
+1 -1
View File
@@ -4,6 +4,6 @@
应用版本号 应用版本号
每次 OTA 更新时,只需要更新这个文件中的版本号 每次 OTA 更新时,只需要更新这个文件中的版本号
""" """
VERSION = '2.18.2' VERSION = '3.0.0'
+2 -51
View File
@@ -902,16 +902,13 @@ def _save_shot_image_impl(img_cv, center, radius, method, ellipse_params,
def _save_worker_loop(): def _save_worker_loop():
"""存图 worker处理标注图和可选的纯原图任务""" """存图 worker从队列取任务并调用 _save_shot_image_impl"""
while True: while True:
try: try:
item = _save_queue.get() item = _save_queue.get()
if item is None: if item is None:
break break
if isinstance(item, dict) and item.get("kind") == "raw": _save_shot_image_impl(*item)
_save_raw_image_impl(item["img_cv"], item["shot_id"], item["photo_dir"])
else:
_save_shot_image_impl(*item)
except Exception as e: except Exception as e:
logger = logger_manager.logger logger = logger_manager.logger
if logger: if logger:
@@ -939,52 +936,6 @@ def start_save_shot_worker():
logger.info("[VISION] 存图 worker 线程已启动") logger.info("[VISION] 存图 worker 线程已启动")
def _save_raw_image_impl(img_cv, shot_id, photo_dir):
"""保存未标注、未裁剪的完整原始帧。"""
logger = logger_manager.logger
try:
os.makedirs(photo_dir, exist_ok=True)
filename = os.path.join(photo_dir, f"shot_{shot_id}_raw.jpg")
image.cv2image(img_cv, False, False).save(filename)
prune_old_images_in_dir(
photo_dir,
getattr(config, "RAW_IMAGE_MAX_IMAGES", config.MAX_IMAGES),
logger,
"[VISION-RAW]",
)
if logger:
logger.info(f"[VISION-RAW] 已保存纯原图: {filename}")
return filename
except Exception as e:
if logger:
logger.error(f"[VISION-RAW] 保存纯原图失败: {e}")
return None
def enqueue_save_raw_shot(frame, shot_id, photo_dir=None):
"""复制并异步保存原始帧;由 SAVE_RAW_IMAGE_ENABLED 控制是否启用。"""
if not getattr(config, "SAVE_RAW_IMAGE_ENABLED", False):
return
if photo_dir is None:
photo_dir = getattr(config, "RAW_IMAGE_DIR", os.path.join(config.PHOTO_DIR, "raw"))
try:
img_copy = np.copy(image.image2cv(frame, False, False))
_save_queue.put_nowait({
"kind": "raw",
"img_cv": img_copy,
"shot_id": shot_id,
"photo_dir": photo_dir,
})
except queue.Full:
logger = logger_manager.logger
if logger:
logger.warning("[VISION-RAW] 存图队列已满,跳过本次纯原图保存")
except Exception as e:
logger = logger_manager.logger
if logger:
logger.error(f"[VISION-RAW] 复制纯原图失败: {e}")
def enqueue_save_shot(result_img, center, radius, method, ellipse_params, def enqueue_save_shot(result_img, center, radius, method, ellipse_params,
laser_point, distance_m, shot_id=None, photo_dir=None, laser_point, distance_m, shot_id=None, photo_dir=None,
yolo_roi_xyxy=None, force_save=False): yolo_roi_xyxy=None, force_save=False):