新分支 加入了标靶判断
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
network.py
|
||||
7
.idea/archery.iml
generated
Normal file
7
.idea/archery.iml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module version="4">
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
</module>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.13 virtualenv at H:\iot\racingiot_v1\.venv" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (PyCharmMiscProject)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
BIN
__pycache__/version.cpython-312.pyc
Normal file
BIN
__pycache__/version.cpython-312.pyc
Normal file
Binary file not shown.
12
adc.py
12
adc.py
@@ -4,12 +4,12 @@ from maix import time
|
||||
a = adc.ADC(0, adc.RES_BIT_12)
|
||||
|
||||
while True:
|
||||
# raw_data = a.read()
|
||||
# print(f"ADC raw data:{raw_data}")
|
||||
# if raw_data > 2450:
|
||||
# print(f"ADC raw data:{raw_data}")
|
||||
# elif raw_data < 2000:
|
||||
# print(f"ADC raw data:{raw_data}")
|
||||
raw_data = a.read()
|
||||
print(f"ADC raw data:{raw_data}")
|
||||
if raw_data > 2450:
|
||||
print(f"ADC raw data:{raw_data}")
|
||||
elif raw_data < 2000:
|
||||
print(f"ADC raw data:{raw_data}")
|
||||
time.sleep_ms(1)
|
||||
|
||||
vol = int(a.read_vol() * 10) / 10
|
||||
|
||||
3
app.yaml
3
app.yaml
@@ -1,6 +1,6 @@
|
||||
id: t11
|
||||
name: t11
|
||||
version: 2.14.1
|
||||
version: 1.2.15.1
|
||||
author: t11
|
||||
icon: ''
|
||||
desc: t11
|
||||
@@ -23,6 +23,7 @@ files:
|
||||
- ota_manager.py
|
||||
- power.py
|
||||
- server.pem
|
||||
- set_autostart.py
|
||||
- shoot_manager.py
|
||||
- shot_id_generator.py
|
||||
- target_roi_yolo.py
|
||||
|
||||
@@ -169,6 +169,7 @@ TRIANGLE_SHAPE_COS_TOLERANCE = 0.25 # 直角余弦绝对值上限(原 0.20
|
||||
# 建议设为实测最坏耗时的 1.2 倍;超时后圆心检测仍会并行跑完,跑完后若三角形已结束则优先用三角形。
|
||||
TRIANGLE_TIMEOUT_MS = 1000
|
||||
# True=打印各阶段耗时(ms),用于定位瓶颈;稳定后可 False 减少日志
|
||||
ARCHERY_TIMING_ENABLE = True # 总开关:False 关闭所有算法耗时统计(shoot_manager + triangle_target + vision)
|
||||
TRIANGLE_TIMING_LOG = True
|
||||
# True=Stage2 每个子框内传统三角失败时打一条统计(Otsu/Adaptive 下轮廓数与各拒绝原因计数)
|
||||
TRIANGLE_LOG_STAGE2_PATCH_REJECT = True
|
||||
@@ -256,9 +257,13 @@ TRIANGLE_CROP_ROI_MIN_SIDE_PX = 64
|
||||
# 射箭保存图 / 预览上绘制 YOLO 靶环 ROI 矩形 (x0,y0,x1,y1),核对是否裁准;不需要时改 False
|
||||
TRIANGLE_YOLO_DRAW_ROI_ON_SHOT = True
|
||||
# 物方采样调试:以靶心为中心,取半径 15cm 的圆周样本点,用于黑/白颜色对比
|
||||
TRIANGLE_SAMPLE_ENABLE = True
|
||||
TRIANGLE_SAMPLE_TIMING_ENABLE = True # 仅统计物方采样耗时(其他 timing 可关)
|
||||
TRIANGLE_SAMPLE_RADIUS_CM = 15.0
|
||||
TRIANGLE_SAMPLE_ANGLES_DEG = (0, 90, 180, 270)
|
||||
TRIANGLE_SAMPLE_PATCH_HALF_PX = 2
|
||||
# 物方采样判断黑白阈值(R/G/B 均小于此值视为黑);40cm 黑靶在靶面位置全黑,20cm 白靶则 R/G/B 偏高
|
||||
TRIANGLE_SAMPLE_BLACK_THRESH = 30.0
|
||||
# 开机阶段预加载 YOLO detector;detect 使用 dual_buff=False,避免返回上一帧结果。
|
||||
TRIANGLE_YOLO_PRELOAD_ON_BOOT = True
|
||||
|
||||
@@ -310,6 +315,7 @@ LASER_LENGTH = 2
|
||||
|
||||
# ==================== 图像保存配置 ====================
|
||||
SAVE_IMAGE_ENABLED = True # 是否保存图像(True=保存,False=不保存)
|
||||
VISION_TIMING_ENABLE = True # 视觉圆检测耗时统计(detect_circle_v3 内部各步骤耗时)
|
||||
PHOTO_DIR = "/root/phot" # 照片存储目录
|
||||
MAX_IMAGES = 1000
|
||||
# Stage2 调试目录(默认 PHOTO_DIR/stage2_roi)内 JPEG 最多保留张数;None 表示与 MAX_IMAGES 相同
|
||||
|
||||
Binary file not shown.
@@ -1,13 +0,0 @@
|
||||
|
||||
[basic]
|
||||
type = cvimodel
|
||||
model = model_270820.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 = triangle
|
||||
|
||||
130
shoot_manager.py
130
shoot_manager.py
@@ -58,6 +58,7 @@ def analyze_shot(frame, laser_point=None):
|
||||
# ── Step 1: 确定激光点 ────────────────────────────────────────────────────
|
||||
laser_point_method = None
|
||||
distance_m_first = None
|
||||
best_radius1_temp = None
|
||||
|
||||
if config.HARDCODE_LASER_POINT:
|
||||
laser_point = laser_manager.laser_point
|
||||
@@ -102,9 +103,22 @@ def analyze_shot(frame, laser_point=None):
|
||||
r_img, center, radius, method, best_radius1, ellipse_params = cdata
|
||||
dx, dy = None, None
|
||||
d_m = distance_m_first
|
||||
tri_h = None
|
||||
if center and radius:
|
||||
dx, dy = laser_manager.compute_laser_position(center, (x, y), radius, method)
|
||||
d_m = estimate_distance(best_radius1) if best_radius1 else distance_m_first
|
||||
try:
|
||||
import numpy as _np
|
||||
px_per_cm = float(radius) / 10.0
|
||||
if px_per_cm > 1e-6:
|
||||
cxp, cyp = float(center[0]), float(center[1])
|
||||
tri_h = _np.array([
|
||||
[1.0 / px_per_cm, 0.0, -cxp / px_per_cm],
|
||||
[0.0, 1.0 / px_per_cm, -cyp / px_per_cm],
|
||||
[0.0, 0.0, 1.0],
|
||||
], dtype=float)
|
||||
except Exception:
|
||||
tri_h = None
|
||||
out = {
|
||||
"success": True,
|
||||
"result_img": r_img,
|
||||
@@ -114,6 +128,7 @@ def analyze_shot(frame, laser_point=None):
|
||||
"laser_point": laser_point, "laser_point_method": laser_point_method,
|
||||
"offset_method": "yellow_ellipse" if ellipse_params else "yellow_circle",
|
||||
"distance_method": "yellow_radius",
|
||||
"tri_homography": tri_h,
|
||||
}
|
||||
if yolo_roi_xyxy is not None:
|
||||
out["yolo_roi_xyxy"] = yolo_roi_xyxy
|
||||
@@ -129,8 +144,10 @@ def analyze_shot(frame, laser_point=None):
|
||||
roi_xyxy = None
|
||||
yolo_ring_ms = 0.0
|
||||
yolo_black_ms = 0.0
|
||||
_timing_on = bool(getattr(config, "ARCHERY_TIMING_ENABLE", True))
|
||||
_sample_on = bool(getattr(config, "TRIANGLE_SAMPLE_ENABLE", False))
|
||||
if getattr(config, "TRIANGLE_YOLO_ROI_ENABLE", False):
|
||||
_t_yolo_ring = time_std.perf_counter()
|
||||
_t_yolo_ring = time_std.perf_counter() if _timing_on else None
|
||||
try:
|
||||
from target_roi_yolo import try_get_triangle_roi_from_yolo
|
||||
roi_xyxy = try_get_triangle_roi_from_yolo(
|
||||
@@ -140,7 +157,8 @@ def analyze_shot(frame, laser_point=None):
|
||||
if logger:
|
||||
logger.warning(f"[YOLO-ROI] {e}")
|
||||
finally:
|
||||
yolo_ring_ms = (time_std.perf_counter() - _t_yolo_ring) * 1000.0
|
||||
if _timing_on and _t_yolo_ring is not None:
|
||||
yolo_ring_ms = (time_std.perf_counter() - _t_yolo_ring) * 1000.0
|
||||
|
||||
_loc_mode = str(
|
||||
getattr(config, "TRIANGLE_BLACK_TRIANGLE_LOCATE_MODE", "yolo")
|
||||
@@ -155,7 +173,7 @@ def analyze_shot(frame, laser_point=None):
|
||||
and roi_xyxy is not None
|
||||
)
|
||||
if _run_stage2_black_yolo:
|
||||
_t_yolo_black = time_std.perf_counter()
|
||||
_t_yolo_black = time_std.perf_counter() if _timing_on else None
|
||||
try:
|
||||
from target_roi_yolo import try_black_triangle_boxes_work
|
||||
|
||||
@@ -166,7 +184,8 @@ def analyze_shot(frame, laser_point=None):
|
||||
if logger:
|
||||
logger.warning(f"[YOLO-BLACK] {e}")
|
||||
finally:
|
||||
yolo_black_ms = (time_std.perf_counter() - _t_yolo_black) * 1000.0
|
||||
if _timing_on and _t_yolo_black is not None:
|
||||
yolo_black_ms = (time_std.perf_counter() - _t_yolo_black) * 1000.0
|
||||
elif (
|
||||
logger
|
||||
and _loc_mode == "traditional"
|
||||
@@ -184,7 +203,7 @@ def analyze_shot(frame, laser_point=None):
|
||||
try:
|
||||
logger.info(f"[TRI] begin {datetime.now()}")
|
||||
logger.info(f"[TRI] K: {K}, dist: {dist_coef}, pos: {pos}, {datetime.now()}")
|
||||
_t_wall_try = time_std.perf_counter()
|
||||
_t_wall_try = time_std.perf_counter() if _timing_on else None
|
||||
tri = try_triangle_scoring(
|
||||
img_cv, (x, y), pos, K, dist_coef,
|
||||
size_range=getattr(config, "TRIANGLE_SIZE_RANGE", (8, 500)),
|
||||
@@ -193,8 +212,8 @@ def analyze_shot(frame, laser_point=None):
|
||||
yolo_ring_ms=yolo_ring_ms,
|
||||
yolo_black_ms=yolo_black_ms,
|
||||
)
|
||||
_wall_try_ms = (time_std.perf_counter() - _t_wall_try) * 1000.0
|
||||
if logger and bool(getattr(config, "TRIANGLE_LOG_E2E_TIMING", True)):
|
||||
_wall_try_ms = (time_std.perf_counter() - _t_wall_try) * 1000.0 if _timing_on else 0.0
|
||||
if logger and bool(getattr(config, "TRIANGLE_LOG_E2E_TIMING", True)) and _timing_on:
|
||||
_e2e = float(yolo_ring_ms) + float(yolo_black_ms) + float(_wall_try_ms)
|
||||
logger.info(
|
||||
f"[TRI] timing_e2e_triangle_ms={_e2e:.1f} "
|
||||
@@ -280,6 +299,16 @@ def analyze_shot(frame, laser_point=None):
|
||||
"tri_markers_completed": tri.get("markers_completed", []),
|
||||
"tri_homography": tri.get("homography"),
|
||||
}
|
||||
try:
|
||||
import numpy as _np
|
||||
_H = tri.get("homography")
|
||||
if _H is not None and _np.all(_np.isfinite(_H)):
|
||||
_H_inv = _np.linalg.inv(_H)
|
||||
_pt = _np.array([[[0.0, 0.0]]], dtype=_np.float32)
|
||||
_center_pt = cv2.perspectiveTransform(_pt, _H_inv)[0][0]
|
||||
out["tri_center_px"] = [float(_center_pt[0]), float(_center_pt[1])]
|
||||
except Exception:
|
||||
pass
|
||||
if yolo_roi_xyxy is not None:
|
||||
out["yolo_roi_xyxy"] = yolo_roi_xyxy
|
||||
return out
|
||||
@@ -318,6 +347,7 @@ def process_shot(adc_val):
|
||||
:return: None
|
||||
"""
|
||||
logger = logger_manager.logger
|
||||
_timing_on = bool(getattr(config, "ARCHERY_TIMING_ENABLE", True))
|
||||
|
||||
try:
|
||||
network_manager.safe_enqueue({"shoot_event": "start"}, msg_type=2, high=True)
|
||||
@@ -356,6 +386,86 @@ def process_shot(adc_val):
|
||||
)
|
||||
x, y = laser_point
|
||||
|
||||
# 物方采样调试(config.TRIANGLE_SAMPLE_ENABLE):靶心为原点,取两个对称点判断黑白来区分 40/20 标靶
|
||||
# 逻辑:若两个采样点 RGB 均 < 阈值 → 全黑 → 40cm 标靶;否则 → 20cm 标靶
|
||||
sample_target_type = None
|
||||
_t_sample = time_std.perf_counter() if _timing_on else None
|
||||
_t_sample_ms = 0.0
|
||||
sample_points = []
|
||||
sample_patch_half = 2
|
||||
if bool(getattr(config, "TRIANGLE_SAMPLE_ENABLE", False)):
|
||||
sample_obj_radius_cm = float(getattr(config, "TRIANGLE_SAMPLE_RADIUS_CM", 15.0))
|
||||
sample_obj_angles_deg = (0, 180) # 只取两个对称点:+X 和 -X
|
||||
sample_patch_half = int(getattr(config, "TRIANGLE_SAMPLE_PATCH_HALF_PX", 2))
|
||||
sample_black_thresh = float(getattr(config, "TRIANGLE_SAMPLE_BLACK_THRESH", 30.0))
|
||||
try:
|
||||
import math as _math
|
||||
import numpy as _np
|
||||
import cv2 as _cv2
|
||||
|
||||
if tri_homography is not None:
|
||||
_H_inv = _np.linalg.inv(tri_homography)
|
||||
for _ang in sample_obj_angles_deg:
|
||||
_rad = _math.radians(float(_ang))
|
||||
_pt_obj = _np.array([
|
||||
[[sample_obj_radius_cm * _math.cos(_rad), sample_obj_radius_cm * _math.sin(_rad)]]
|
||||
], dtype=_np.float32)
|
||||
_pt_img = _cv2.perspectiveTransform(_pt_obj, _H_inv)[0][0]
|
||||
_px, _py = float(_pt_img[0]), float(_pt_img[1])
|
||||
sample_points.append({
|
||||
"angle_deg": float(_ang),
|
||||
"obj_cm": (float(sample_obj_radius_cm * _math.cos(_rad)), float(sample_obj_radius_cm * _math.sin(_rad))),
|
||||
"img_px": (int(round(_px)), int(round(_py))),
|
||||
})
|
||||
elif center and radius:
|
||||
_px_per_cm = float(radius) / 10.0
|
||||
for _ang in sample_obj_angles_deg:
|
||||
_rad = _math.radians(float(_ang))
|
||||
_px = float(center[0]) + sample_obj_radius_cm * _math.cos(_rad) * _px_per_cm
|
||||
_py = float(center[1]) + sample_obj_radius_cm * _math.sin(_rad) * _px_per_cm
|
||||
sample_points.append({
|
||||
"angle_deg": float(_ang),
|
||||
"obj_cm": (float(sample_obj_radius_cm * _math.cos(_rad)), float(sample_obj_radius_cm * _math.sin(_rad))),
|
||||
"img_px": (int(round(_px)), int(round(_py))),
|
||||
})
|
||||
|
||||
# 取样后立即读像素并判断黑白:三角成功用 H_inv;三角失败但圆心成功用 center/radius 近似物方半径
|
||||
_all_black = False
|
||||
_sample_infos = []
|
||||
if sample_points:
|
||||
_img_cv_for_sample = image.image2cv(result_img, False, False)
|
||||
_all_black = True
|
||||
for _sp in sample_points:
|
||||
_sx, _sy = _sp["img_px"]
|
||||
_hh = max(1, sample_patch_half)
|
||||
_patch = []
|
||||
for _yy in range(_sy - _hh, _sy + _hh + 1):
|
||||
if _yy < 0 or _yy >= _img_cv_for_sample.shape[0]:
|
||||
continue
|
||||
for _xx in range(_sx - _hh, _sx + _hh + 1):
|
||||
if _xx < 0 or _xx >= _img_cv_for_sample.shape[1]:
|
||||
continue
|
||||
_patch.append(_img_cv_for_sample[_yy, _xx].astype(float))
|
||||
if _patch:
|
||||
_mean_rgb = _np.mean(_patch, axis=0)
|
||||
_is_black = bool(_mean_rgb[0] < sample_black_thresh
|
||||
and _mean_rgb[1] < sample_black_thresh
|
||||
and _mean_rgb[2] < sample_black_thresh)
|
||||
if not _is_black:
|
||||
_all_black = False
|
||||
_sample_infos.append(
|
||||
f"{int(_sp['angle_deg'])}°@{_sx},{_sy} rgb=({int(_mean_rgb[0])},{int(_mean_rgb[1])},{int(_mean_rgb[2])})"
|
||||
)
|
||||
sample_target_type = "40cm_black" if _all_black else "20cm"
|
||||
if _sample_infos:
|
||||
logger.info("[采样] " + " | ".join(_sample_infos) + f" → {sample_target_type}")
|
||||
except Exception as _e_sample:
|
||||
sample_points = []
|
||||
if logger:
|
||||
logger.warning(f"[采样] 标靶类型判断失败: {_e_sample}")
|
||||
if _timing_on and _t_sample is not None:
|
||||
_t_sample_ms = (time_std.perf_counter() - _t_sample) * 1000.0
|
||||
|
||||
# 三角形路径成功时 center/radius 为空是正常的;此时用 triangle 方法名用于保存文件名与上报字段 m
|
||||
if (not method) and tri_markers:
|
||||
method = "triangle_homography"
|
||||
@@ -397,6 +507,7 @@ def process_shot(adc_val):
|
||||
"target_y": float(y),
|
||||
"offset_method": offset_method,
|
||||
"distance_method": distance_method,
|
||||
"target_type": 40 if sample_target_type == "40cm_black" else (20 if sample_target_type == "20cm" else None),
|
||||
}
|
||||
|
||||
if ellipse_params:
|
||||
@@ -471,6 +582,11 @@ def process_shot(adc_val):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 物方采样标靶类型判断耗时(合并在上面采样块内,单独统计)
|
||||
if _timing_on and bool(getattr(config, "TRIANGLE_SAMPLE_ENABLE", False)) and sample_target_type is not None:
|
||||
logger.info(f"[采样] 标靶类型: {sample_target_type} 耗时: {_t_sample_ms:.2f}ms")
|
||||
|
||||
|
||||
# 叠加信息:落点-圆心距离 / 相机-靶距离等
|
||||
try:
|
||||
import math as _math
|
||||
|
||||
30
test/test_yolov8
Normal file
30
test/test_yolov8
Normal file
@@ -0,0 +1,30 @@
|
||||
from maix import image, nn, display
|
||||
|
||||
# 1. 加载模型
|
||||
detector = nn.YOLOv8(model="/root/models/yolov8n.mud", dual_buff=True)
|
||||
|
||||
# 2. 加载指定图片(根据模型输入尺寸自动缩放宽高)
|
||||
img = image.load("/root/test.jpg")
|
||||
if img is None:
|
||||
raise FileNotFoundError("图片加载失败,请检查路径")
|
||||
|
||||
# 3. 调整图片尺寸到模型输入要求(可选,detect内部会处理,但提前缩放可提高速度)
|
||||
# img = img.resize(detector.input_width(), detector.input_height())
|
||||
|
||||
# 4. 检测
|
||||
objs = detector.detect(img, conf_th=0.5, iou_th=0.45)
|
||||
|
||||
# 5. 在图片上绘制结果
|
||||
for obj in objs:
|
||||
img.draw_rect(obj.x, obj.y, obj.w, obj.h, color=image.COLOR_RED)
|
||||
msg = f'{detector.labels[obj.class_id]}: {obj.score:.2f}'
|
||||
img.draw_string(obj.x, obj.y, msg, color=image.COLOR_RED)
|
||||
|
||||
# 6. 显示结果(如果设备有屏幕)
|
||||
disp = display.Display()
|
||||
disp.show(img)
|
||||
|
||||
# 7. 保存结果(可选)
|
||||
img.save("/root/result.jpg")
|
||||
|
||||
print("识别完成,结果已显示并保存为 result.jpg")
|
||||
@@ -224,7 +224,7 @@ def detect_triangle_markers(
|
||||
blackhat_kernel_frac = 0.018
|
||||
try:
|
||||
import config as _tcfg
|
||||
_timing_log = bool(getattr(_tcfg, "TRIANGLE_TIMING_LOG", True))
|
||||
_timing_log = bool(getattr(_tcfg, "ARCHERY_TIMING_ENABLE", True)) and bool(getattr(_tcfg, "TRIANGLE_TIMING_LOG", True))
|
||||
except Exception:
|
||||
_timing_log = True
|
||||
|
||||
@@ -641,7 +641,40 @@ def detect_triangle_markers(
|
||||
med_l = float(np.median(legs))
|
||||
leg_dev = max(abs(l - med_l) / (med_l + 1e-6) for l in legs)
|
||||
|
||||
score = (diag_ratio - 1.0) * 3.0 + (h_ratio - 1.0) + (v_ratio - 1.0) + leg_dev * 2.0
|
||||
# 方向一致性:四个标靶角的直角顶点朝向应符合大致的四象限布局。
|
||||
# 对于相邻标靶很近的情况,这个方向信息能显著减少“把同一方向的两个三角混成一组”的误判。
|
||||
orient_pen = 0.0
|
||||
orient_vote = []
|
||||
for c in cands_4:
|
||||
cen = np.array(c["center_px"], dtype=np.float32)
|
||||
rpt = np.array(c["right_pt"], dtype=np.float32)
|
||||
vx = float(cen[0] - rpt[0])
|
||||
vy = float(cen[1] - rpt[1])
|
||||
# 方向太接近轴线时,说明顶点朝向不稳定,增加惩罚
|
||||
if abs(vx) < 1e-6 or abs(vy) < 1e-6:
|
||||
orient_pen += 1.0
|
||||
orient_vote.append(None)
|
||||
continue
|
||||
if abs(vx) < abs(vy) * 0.15 or abs(vy) < abs(vx) * 0.15:
|
||||
orient_pen += 0.5
|
||||
# 以中心相对右角顶点的方向做粗分类:TL=向右下,TR=向左下,BL=向右上,BR=向左上
|
||||
if vx > 0 and vy > 0:
|
||||
orient_vote.append(0)
|
||||
elif vx < 0 and vy > 0:
|
||||
orient_vote.append(1)
|
||||
elif vx > 0 and vy < 0:
|
||||
orient_vote.append(2)
|
||||
else:
|
||||
orient_vote.append(3)
|
||||
|
||||
# 如果 4 个候选的方向落点本身就重复很多,说明可能混入了别的靶标角,直接加罚。
|
||||
valid_votes = [v for v in orient_vote if v is not None]
|
||||
if valid_votes:
|
||||
from collections import Counter
|
||||
vc = Counter(valid_votes)
|
||||
orient_pen += max(0, max(vc.values()) - 1) * 0.8
|
||||
|
||||
score = (diag_ratio - 1.0) * 3.0 + (h_ratio - 1.0) + (v_ratio - 1.0) + leg_dev * 2.0 + orient_pen
|
||||
return score, (tl, bl, br, tr)
|
||||
|
||||
assigned = None
|
||||
@@ -947,7 +980,33 @@ def _assign_marker_ids_from_filtered(filtered, verbose=True):
|
||||
v_ratio = max(s_left, s_right) / (min(s_left, s_right) + 1e-6)
|
||||
med_l = float(np.median(legs))
|
||||
leg_dev = max(abs(l - med_l) / (med_l + 1e-6) for l in legs)
|
||||
score = (diag_ratio - 1.0) * 3.0 + (h_ratio - 1.0) + (v_ratio - 1.0) + leg_dev * 2.0
|
||||
orient_pen = 0.0
|
||||
orient_vote = []
|
||||
for c in cands_4:
|
||||
cen = np.array(c["center_px"], dtype=np.float32)
|
||||
rpt = np.array(c["right_pt"], dtype=np.float32)
|
||||
vx = float(cen[0] - rpt[0])
|
||||
vy = float(cen[1] - rpt[1])
|
||||
if abs(vx) < 1e-6 or abs(vy) < 1e-6:
|
||||
orient_pen += 1.0
|
||||
orient_vote.append(None)
|
||||
continue
|
||||
if abs(vx) < abs(vy) * 0.15 or abs(vy) < abs(vx) * 0.15:
|
||||
orient_pen += 0.5
|
||||
if vx > 0 and vy > 0:
|
||||
orient_vote.append(0)
|
||||
elif vx < 0 and vy > 0:
|
||||
orient_vote.append(1)
|
||||
elif vx > 0 and vy < 0:
|
||||
orient_vote.append(2)
|
||||
else:
|
||||
orient_vote.append(3)
|
||||
valid_votes = [v for v in orient_vote if v is not None]
|
||||
if valid_votes:
|
||||
from collections import Counter
|
||||
vc = Counter(valid_votes)
|
||||
orient_pen += max(0, max(vc.values()) - 1) * 0.8
|
||||
score = (diag_ratio - 1.0) * 3.0 + (h_ratio - 1.0) + (v_ratio - 1.0) + leg_dev * 2.0 + orient_pen
|
||||
return score, (tl, bl, br, tr)
|
||||
|
||||
assigned = None
|
||||
@@ -1113,7 +1172,7 @@ def try_triangle_scoring(
|
||||
|
||||
try:
|
||||
import config as _cfg_tl
|
||||
_try_timing_log = bool(getattr(_cfg_tl, "TRIANGLE_TIMING_LOG", True))
|
||||
_try_timing_log = bool(getattr(_cfg_tl, "ARCHERY_TIMING_ENABLE", True)) and bool(getattr(_cfg_tl, "TRIANGLE_TIMING_LOG", True))
|
||||
_crop_min_side = int(getattr(_cfg_tl, "TRIANGLE_CROP_ROI_MIN_SIDE_PX", 64))
|
||||
except Exception:
|
||||
_try_timing_log = True
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
应用版本号
|
||||
每次 OTA 更新时,只需要更新这个文件中的版本号
|
||||
"""
|
||||
VERSION = '2.14.1'
|
||||
VERSION = '1.2.15'
|
||||
|
||||
|
||||
# 1.2.0 开始使用C++编译成.so,替换部分代码
|
||||
@@ -22,7 +22,7 @@ VERSION = '2.14.1'
|
||||
# 1.2.110 关掉了黑色三角形算法,只用于测试
|
||||
# 1.2.13 修改wifi连接
|
||||
# 1.2.14 修改了icc登录部分
|
||||
|
||||
# 1.2.15 增加了标靶判断 20 40
|
||||
|
||||
|
||||
|
||||
|
||||
90
vision.py
90
vision.py
@@ -10,6 +10,7 @@ import os
|
||||
import math
|
||||
import threading
|
||||
import queue
|
||||
import time
|
||||
from maix import image
|
||||
import config
|
||||
from logger_manager import logger_manager
|
||||
@@ -531,6 +532,9 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
|
||||
if img_cv is None:
|
||||
img_cv = image.image2cv(frame, False, False)
|
||||
logger = logger_manager.logger
|
||||
_timing_on = bool(getattr(config, "VISION_TIMING_ENABLE", True))
|
||||
_t0 = time.perf_counter() if _timing_on else None
|
||||
_t1 = _t2 = _t3 = _t4 = _t5 = None
|
||||
from datetime import datetime
|
||||
logger.debug(f"[detect_circle_v3] begin {datetime.now()}")
|
||||
# -- 1. 缩图加速(与三角形路径保持一致)
|
||||
@@ -554,6 +558,8 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
|
||||
ellipse_params = None
|
||||
|
||||
logger.debug(f"[detect_circle_v3] step 1 fin {datetime.now()}")
|
||||
if _timing_on:
|
||||
_t1 = time.perf_counter()
|
||||
|
||||
# -- 2. HSV + 黄色掩码
|
||||
hsv = cv2.cvtColor(img_det, cv2.COLOR_RGB2HSV)
|
||||
@@ -567,6 +573,9 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
|
||||
mask_yellow = cv2.morphologyEx(mask_yellow, cv2.MORPH_CLOSE, kernel)
|
||||
|
||||
logger.debug(f"[detect_circle_v3] step 2 fin {datetime.now()}")
|
||||
if _timing_on:
|
||||
_t2 = time.perf_counter()
|
||||
_t3 = time.perf_counter()
|
||||
|
||||
# -- 3. 红色掩码:在循环外只算一次
|
||||
mask_red = cv2.bitwise_or(
|
||||
@@ -593,6 +602,9 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
|
||||
red_candidates.append({"center": (int(xr), int(yr)), "radius": int(rr)})
|
||||
|
||||
logger.debug(f"[detect_circle_v3] step 3 fin {datetime.now()}")
|
||||
if _timing_on:
|
||||
_t3 = time.perf_counter()
|
||||
_t4 = time.perf_counter()
|
||||
|
||||
# -- 4. 黄色轮廓循环(复用上面的红色候选列表)
|
||||
contours_yellow, _ = cv2.findContours(mask_yellow, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
@@ -642,6 +654,9 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
|
||||
logger.debug("Debug -> 未找到匹配的红色圆圈,可能是误识别")
|
||||
|
||||
logger.debug(f"[detect_circle_v3] step 4 fin {datetime.now()}")
|
||||
if _timing_on:
|
||||
_t4 = time.perf_counter()
|
||||
_t5 = time.perf_counter()
|
||||
|
||||
# -- 5. 选最佳目标,坐标还原到原始分辨率
|
||||
if valid_targets:
|
||||
@@ -669,7 +684,20 @@ def detect_circle_v3(frame, laser_point=None, img_cv=None):
|
||||
ellipse_params = be
|
||||
best_radius1 = best_radius * 5
|
||||
result_img = image.cv2image(img_cv, False, False)
|
||||
logger.debug(f"[detect_circle_v3] step 5 fin {datetime.now()}")
|
||||
if _timing_on:
|
||||
_t5 = time.perf_counter()
|
||||
_t_all = (_t5 - _t0) * 1000
|
||||
_ms1 = (_t1 - _t0) * 1000
|
||||
_ms2 = (_t2 - _t1) * 1000
|
||||
_ms3 = (_t3 - _t2) * 1000
|
||||
_ms4 = (_t4 - _t3) * 1000
|
||||
_ms5 = (_t5 - _t4) * 1000
|
||||
logger.info(
|
||||
f"[VISION timing] total={_t_all:.1f}ms "
|
||||
f"resize={_ms1:.1f} hsv_yellow={_ms2:.1f} "
|
||||
f"red_mask={_ms3:.1f} yellow_loop={_ms4:.1f} "
|
||||
f"select_cv2img={_ms5:.1f}"
|
||||
)
|
||||
return result_img, best_center, best_radius, method, best_radius1, ellipse_params
|
||||
|
||||
def estimate_distance(pixel_radius):
|
||||
@@ -1010,3 +1038,63 @@ def detect_target(frame, laser_point=None):
|
||||
logger.debug("[VISION] 使用传统黄色靶心检测")
|
||||
return detect_circle_v3(frame, laser_point)
|
||||
|
||||
|
||||
def sample_target_rgb_at_physical_radius(frame, target_center, target_radius_px, radius_cm=None, angles_deg=None, patch_half_px=None, black_thresh=None, timing=False):
|
||||
"""
|
||||
在物方半径位置采样 RGB,判断黑/白靶。
|
||||
返回: dict {ok, is_black, mean_rgb, samples, black_ratio, elapsed_ms}
|
||||
"""
|
||||
logger = logger_manager.logger
|
||||
if target_center is None or target_radius_px is None:
|
||||
return {"ok": False, "reason": "no_target", "is_black": None, "elapsed_ms": 0.0}
|
||||
|
||||
radius_cm = float(radius_cm if radius_cm is not None else getattr(config, "TRIANGLE_SAMPLE_RADIUS_CM", 15.0))
|
||||
angles_deg = tuple(angles_deg if angles_deg is not None else getattr(config, "TRIANGLE_SAMPLE_ANGLES_DEG", (0, 90, 180, 270)))
|
||||
patch_half_px = int(patch_half_px if patch_half_px is not None else getattr(config, "TRIANGLE_SAMPLE_PATCH_HALF_PX", 2))
|
||||
black_thresh = float(black_thresh if black_thresh is not None else getattr(config, "TRIANGLE_SAMPLE_BLACK_THRESH", 30.0))
|
||||
timing_on = bool(timing) and bool(getattr(config, "TRIANGLE_SAMPLE_TIMING_ENABLE", True))
|
||||
t0 = time.perf_counter() if timing_on else None
|
||||
|
||||
try:
|
||||
img_cv = image.image2cv(frame, False, False)
|
||||
h, w = img_cv.shape[:2]
|
||||
cx, cy = float(target_center[0]), float(target_center[1])
|
||||
scale = float(target_radius_px) / max(radius_cm, 1e-6)
|
||||
samples = []
|
||||
black_count = 0
|
||||
for ang in angles_deg:
|
||||
rad = math.radians(float(ang))
|
||||
sx = int(round(cx + math.cos(rad) * radius_cm * scale))
|
||||
sy = int(round(cy + math.sin(rad) * radius_cm * scale))
|
||||
x0 = max(0, sx - patch_half_px)
|
||||
y0 = max(0, sy - patch_half_px)
|
||||
x1 = min(w, sx + patch_half_px + 1)
|
||||
y1 = min(h, sy + patch_half_px + 1)
|
||||
if x1 <= x0 or y1 <= y0:
|
||||
continue
|
||||
patch = img_cv[y0:y1, x0:x1]
|
||||
mean_rgb = patch.reshape(-1, 3).mean(axis=0)
|
||||
is_black = bool(np.all(mean_rgb < black_thresh))
|
||||
black_count += 1 if is_black else 0
|
||||
samples.append({"angle": float(ang), "xy": (sx, sy), "mean_rgb": tuple(float(v) for v in mean_rgb), "is_black": is_black})
|
||||
black_ratio = float(black_count) / float(len(samples) or 1)
|
||||
out = {
|
||||
"ok": len(samples) > 0,
|
||||
"is_black": black_ratio >= 0.5,
|
||||
"mean_rgb": tuple(float(v) for v in (np.mean([s["mean_rgb"] for s in samples], axis=0) if samples else (0, 0, 0))),
|
||||
"samples": samples,
|
||||
"black_ratio": black_ratio,
|
||||
"elapsed_ms": (time.perf_counter() - t0) * 1000.0 if timing_on else 0.0,
|
||||
}
|
||||
if logger:
|
||||
logger.info(
|
||||
f"[TRI-SAMPLE] radius_cm={radius_cm:.1f} black_thresh={black_thresh:.1f} "
|
||||
f"black_ratio={black_ratio:.2f} is_black={out['is_black']} "
|
||||
f"elapsed_ms={out['elapsed_ms']:.1f} samples={len(samples)}"
|
||||
)
|
||||
return out
|
||||
except Exception as e:
|
||||
if logger:
|
||||
logger.error(f"[TRI-SAMPLE] 采样失败: {e}")
|
||||
return {"ok": False, "reason": str(e), "is_black": None, "elapsed_ms": 0.0}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user