diff --git a/__pycache__/config.cpython-312.pyc b/__pycache__/config.cpython-312.pyc index 8174d18..9511b07 100644 Binary files a/__pycache__/config.cpython-312.pyc and b/__pycache__/config.cpython-312.pyc differ diff --git a/__pycache__/laser_manager.cpython-312.pyc b/__pycache__/laser_manager.cpython-312.pyc new file mode 100644 index 0000000..51df997 Binary files /dev/null and b/__pycache__/laser_manager.cpython-312.pyc differ diff --git a/__pycache__/main.cpython-312.pyc b/__pycache__/main.cpython-312.pyc index 215a5c6..7faebb2 100644 Binary files a/__pycache__/main.cpython-312.pyc and b/__pycache__/main.cpython-312.pyc differ diff --git a/__pycache__/power.cpython-312.pyc b/__pycache__/power.cpython-312.pyc new file mode 100644 index 0000000..ef59564 Binary files /dev/null and b/__pycache__/power.cpython-312.pyc differ diff --git a/__pycache__/shoot_manager.cpython-312.pyc b/__pycache__/shoot_manager.cpython-312.pyc new file mode 100644 index 0000000..530d440 Binary files /dev/null and b/__pycache__/shoot_manager.cpython-312.pyc differ diff --git a/__pycache__/vision.cpython-312.pyc b/__pycache__/vision.cpython-312.pyc new file mode 100644 index 0000000..1d101c7 Binary files /dev/null and b/__pycache__/vision.cpython-312.pyc differ diff --git a/app.yaml b/app.yaml index f111ba7..af5f184 100644 --- a/app.yaml +++ b/app.yaml @@ -1,6 +1,6 @@ id: t11 name: t11 -version: 2.15.35 +version: 2.17.14 author: t11 icon: '' desc: t11 @@ -18,12 +18,12 @@ files: - laser_manager.py - logger_manager.py - main.py - - model_270139.cvimodel - - model_270139.mud + - model_317828.cvimodel + - model_317828.mud + - model_317211.cvimodel + - model_317211.mud - model_270820.cvimodel - model_270820.mud - - model_285484.cvimodel - - model_285484.mud - network.py - ota_curl.sh - ota_manager.py diff --git a/config.py b/config.py index b30e352..a868769 100644 --- a/config.py +++ b/config.py @@ -239,10 +239,10 @@ TRIANGLE_BLACKHAT_KERNEL_FRAC = 0.018 # 核大小 ≈ min(h,w)*frac,取奇数 # ── YOLO(NPU) 靶环 ROI → 裁剪后再跑三角形(减小 CPU 处理面积)────────────────── # 日志里 net_in=W×H 来自 .mud 模型(det.input_width/height),不是这里配置的。 TRIANGLE_YOLO_ROI_ENABLE = True -TRIANGLE_YOLO_MODEL_PATH = APP_DIR + "/model_270139.mud" +TRIANGLE_YOLO_MODEL_PATH = APP_DIR + "/model_317211.mud" # 参与 ROI 的类别:多类时只填「整靶/靶环」的 id;不要填角标类,否则 union 仍可对,但 largest 会偏小。 TRIANGLE_YOLO_RING_CLASS_IDS = (0,) -TRIANGLE_YOLO_CONF_TH = 0.7 +TRIANGLE_YOLO_CONF_TH = 0.9 TRIANGLE_YOLO_IOU_TH = 0.45 # YOLO 首次/临界帧可能在高阈值下 0 框;启用后仅在 0 候选时用较低阈值重试一次。 # 后续仍会经过 min_box_side、ROI aspect、三角形几何校验,避免直接放大假阳性。 @@ -269,9 +269,9 @@ TRIANGLE_YOLO_PRELOAD_ON_BOOT = False # YOLO target size classification: class 0=20cm, class 1=40cm. TARGET_CLASS_YOLO_ENABLE = True -TARGET_CLASS_YOLO_MODEL_PATH = APP_DIR + "/model_285484.mud" +TARGET_CLASS_YOLO_MODEL_PATH = APP_DIR + "/model_317828.mud" TARGET_CLASS_YOLO_LABELS = (20, 40) -TARGET_CLASS_YOLO_CONF_TH = 0.50 +TARGET_CLASS_YOLO_CONF_TH = 0.66 TARGET_CLASS_YOLO_IOU_TH = 0.45 TARGET_CLASS_YOLO_RETRY_ON_EMPTY = False TARGET_CLASS_YOLO_RETRY_CONF_TH = 0.25 @@ -331,14 +331,17 @@ LOG_QUEUE_MAXSIZE = 10000 # 日志队列上限 MAX_CMD_THREADS = 10 # 并发命令线程上限(防止服务器下发命令时无限创建线程) # ==================== 图像保存配置 ==================== -SAVE_IMAGE_ENABLED = False # 是否保存图像(True=保存,False=不保存) -SAVE_IMAGE_ON_FAILURE = True # 检测失败时是否强制保存图像(供调试测试用) +SAVE_IMAGE_ENABLED = True # 是否保存图像(True=保存,False=不保存) +SAVE_IMAGE_ON_FAILURE = False # 检测失败时是否强制保存图像(供调试测试用) PHOTO_DIR = "/root/phot" # 照片存储目录 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 相同 TRIANGLE_BLACK_YOLO_STAGE2_ROI_MAX_IMAGES = None -SHOW_CAMERA_PHOTO_WHILE_SHOOTING = False # 是否在拍摄时显示摄像头图像(True=显示,False=不显示),建议在连着USB测试过程中打开 +SHOW_CAMERA_PHOTO_WHILE_SHOOTING = False # 关闭拍摄实时显示 # ==================== OTA配置 ==================== MAX_BACKUPS = 5 diff --git a/main.py b/main.py index 447c4c7..997ab76 100644 --- a/main.py +++ b/main.py @@ -171,8 +171,10 @@ def cmd_str(): and bool(getattr(config, "TARGET_CLASS_YOLO_PRELOAD_ON_BOOT", True)) ) _preload_yolo = _preload_yolo or _need_black_preload or _need_target_preload - if _preload_yolo: + if _preload_yolo and not os.path.exists(f"{config.APP_DIR}/ota_pending.json"): 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: if logger: logger.warning(f"[YOLO-ROI] 启动预加载异常(不影响后续射箭): {e}") @@ -254,7 +256,11 @@ def cmd_str(): network_manager.read_device_id() # 5. 创建照片存储目录(如果启用图像保存或检测失败时强制保存) - if config.SAVE_IMAGE_ENABLED or getattr(config, "SAVE_IMAGE_ON_FAILURE", False): + if ( + 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 if photo_dir not in os.listdir("/root"): try: @@ -286,12 +292,13 @@ def cmd_str(): logger.info("系统准备完成...") last_adc_trigger = 0 + trigger_adc_val = 0 # 触发时的气压值,气压需降回此值以下才能再次触发 + enable_check = True # 读取一次ADC初始值,防止开机时传感器已有压力导致误触发 try: last_adc_val = hardware_manager.adc_obj.read() except Exception: last_adc_val = 0 - peak_adc_val = 0 # 当前周期内的压力峰值 # 气压采样:减少日志频率(每 N 个点输出一条),避免 logger.debug 拖慢采样 PRESSURE_BATCH_SIZE = 100 @@ -381,22 +388,16 @@ def cmd_str(): pressure_max = adc_val if len(pressure_buf) >= PRESSURE_BATCH_SIZE: _flush_pressure_buf("batch") - # 峰值检测:压力从峰值下降时触发,确保捕获到最大冲击时刻 - if adc_val > peak_adc_val: - peak_adc_val = adc_val # 更新峰值 - if (peak_adc_val >= config.ADC_TRIGGER_THRESHOLD - and adc_val < peak_adc_val - and last_adc_val >= peak_adc_val): - # 封顶后下降沿触发:peak是最大值,当前值开始下降,且上次值还在peak位置 + # 突变增量检测:压力增量大于500时触发。 + # 触发后需等气压降回触发点以下,才重新允许下一次触发。 + if adc_val < trigger_adc_val: + enable_check = True + if (adc_val - last_adc_val) > 500 and enable_check: 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 - peak_adc_val = 0 # 触发后重置峰值 - # 触发前先把缓存刷出来,避免波形被长耗时处理截断 + trigger_adc_val = adc_val # 记录触发点 + last_adc_val = adc_val # 更新基准,防止连续增量重复触发 + enable_check = False _flush_pressure_buf("before_trigger") try: @@ -415,7 +416,7 @@ def cmd_str(): camera_manager.show(camera_manager.read_frame()) except Exception as e: pass - time.sleep_ms(5) + time.sleep_ms(1) last_adc_val = adc_val except Exception as e: diff --git a/model_317211.cvimodel b/model_317211.cvimodel new file mode 100644 index 0000000..159f4d0 Binary files /dev/null and b/model_317211.cvimodel differ diff --git a/model_317211.mud b/model_317211.mud new file mode 100644 index 0000000..ed01f1e --- /dev/null +++ b/model_317211.mud @@ -0,0 +1,13 @@ + +[basic] +type = cvimodel +model = model_317211.cvimodel + +[extra] +model_type = yolov5 +input_type = rgb +mean = 0, 0, 0 +scale = 0.00392156862745098, 0.00392156862745098, 0.00392156862745098 +anchors = 10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326 +labels = circle, triangle + diff --git a/model_317828.cvimodel b/model_317828.cvimodel new file mode 100644 index 0000000..0216585 Binary files /dev/null and b/model_317828.cvimodel differ diff --git a/model_317828.mud b/model_317828.mud new file mode 100644 index 0000000..d67e0e2 --- /dev/null +++ b/model_317828.mud @@ -0,0 +1,13 @@ + +[basic] +type = cvimodel +model = model_317828.cvimodel + +[extra] +model_type = yolov5 +input_type = rgb +mean = 0, 0, 0 +scale = 0.00392156862745098, 0.00392156862745098, 0.00392156862745098 +anchors = 10, 13, 16, 30, 33, 23, 30, 61, 62, 45, 59, 119, 116, 90, 156, 198, 373, 326 +labels = 20, 40 + diff --git a/shoot_manager.py b/shoot_manager.py index 078cc9e..9ac3346 100644 --- a/shoot_manager.py +++ b/shoot_manager.py @@ -8,7 +8,7 @@ from laser_manager import laser_manager from logger_manager import logger_manager from network import network_manager 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 +from vision import estimate_distance, detect_circle_v3, enqueue_save_shot, enqueue_save_raw_shot from maix import image, time # 缓存相机标定与三角形位置,避免每次射箭重复读磁盘 @@ -322,6 +322,11 @@ def process_shot(adc_val): try: 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) @@ -380,10 +385,6 @@ def process_shot(adc_val): if dx is None and dy is None and logger: logger.warning("[MAIN] 未检测到偏移量(三角形与圆形均失败),但会保存图像") - # 生成射箭ID - from shot_id_generator import shot_id_generator - shot_id = shot_id_generator.generate_id() - if logger: logger.info(f"[MAIN] 射箭ID: {shot_id}") @@ -441,6 +442,13 @@ def process_shot(adc_val): inner_data["ellipse_center_x"] = 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} if logger: logger.info( diff --git a/version.md b/version.md index a43fd22..1fab875 100644 --- a/version.md +++ b/version.md @@ -30,4 +30,8 @@ # 2.15.17 修复wifi连接问题 # 2.15.18 wifi连接成功重新登录 # 2.16.4 优化射箭延迟 -# 2.17.0 yolo标靶类别识别 \ No newline at end of file +# 2.17.0 yolo标靶类别识别 +# 2.17.1 26-08-19 17:39 压力传感修改 增量方式 +# 2.17.2 26-08-24 17:56 靶纸识别模型更替 +# 2.17.3 26-08-25 9:57 原图拍摄开关 +# 2.17.4 26-08-25 14:57 模型修改 diff --git a/version.py b/version.py index 4bf8f49..5a91078 100644 --- a/version.py +++ b/version.py @@ -4,6 +4,6 @@ 应用版本号 每次 OTA 更新时,只需要更新这个文件中的版本号 """ -VERSION = '2.18.0' +VERSION = '2.17.14' diff --git a/vision.py b/vision.py index a38a67c..1309c10 100644 --- a/vision.py +++ b/vision.py @@ -902,13 +902,16 @@ def _save_shot_image_impl(img_cv, center, radius, method, ellipse_params, def _save_worker_loop(): - """存图 worker:从队列取任务并调用 _save_shot_image_impl。""" + """存图 worker:处理标注图和可选的纯原图任务。""" while True: try: item = _save_queue.get() if item is None: break - _save_shot_image_impl(*item) + if isinstance(item, dict) and item.get("kind") == "raw": + _save_raw_image_impl(item["img_cv"], item["shot_id"], item["photo_dir"]) + else: + _save_shot_image_impl(*item) except Exception as e: logger = logger_manager.logger if logger: @@ -936,6 +939,52 @@ def start_save_shot_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, laser_point, distance_m, shot_id=None, photo_dir=None, yolo_roi_xyxy=None, force_save=False):