yolo
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
1. 系统目标
|
||||
# 检测靶纸四角的等腰直角三角形标记(每个角一个)
|
||||
# 计算激光落点在靶面上的二维偏移(厘米)
|
||||
# 通过PnP算法估算靶面到相机的距离(米)
|
||||
|
||||
2. 核心算法流程
|
||||
2.1 三角形检测 (detect_triangle_markers)
|
||||
采用多策略级联保证鲁棒性:
|
||||
图像输入 → 多阈值策略 → 候选三角形过滤 → 四点匹配
|
||||
检测策略(按优先级):
|
||||
|
||||
1.全局Otsu二值化(最快,~10ms)
|
||||
2.自适应阈值(多种block size,光照不均时)
|
||||
3.ROI局部阈值(候选不足3个时,分象限独立处理)
|
||||
4.Black-Hat形态学增强(仍不足时,突出暗色标记)
|
||||
|
||||
三角形几何验证:
|
||||
# 必须是直角三角形(检查勾股定理,容差20%)
|
||||
# 两直角边长度差<20%
|
||||
# 内部像素足够暗(灰度≤130,暗像素比例≥30%)
|
||||
# 与周围背景对比度≥15灰度级
|
||||
|
||||
四点匹配算法:
|
||||
# 从候选三角形中枚举所有4点组合
|
||||
# 计算四边形评分:(对角比-1)*3 + (水平比-1) + (垂直比-1) + (边长偏差)*2
|
||||
# 选择评分最低的组合作为四角标记
|
||||
|
||||
2.2 单应性落点计算 (homography_calibration)
|
||||
建立图像坐标系 → 靶面坐标系(二维平面)的透视变换
|
||||
|
||||
将激光点像素坐标映射到靶面坐标(厘米)
|
||||
|
||||
使用RANSAC提高鲁棒性(阈值1像素)
|
||||
|
||||
2.3 PnP距离估计 (pnp_distance_meters)
|
||||
已知四个标记点的三维坐标(x,y,z,单位cm)
|
||||
|
||||
通过solvePnP求解相机外参(旋转+平移)
|
||||
|
||||
距离 = ‖平移向量‖ / 100(转换为米)
|
||||
|
||||
3. 关键优化策略
|
||||
3.1 多路径投票
|
||||
同一图像区域被不同二值化方法检测到时,path_votes++
|
||||
|
||||
选择投票数高的候选,提高检测可信度
|
||||
|
||||
3.2 早退机制
|
||||
候选≥3个 且 覆盖3个以上象限 → 停止更多阈值尝试
|
||||
|
||||
大幅降低嵌入式设备计算开销
|
||||
|
||||
3.3 3点补全机制
|
||||
当只检测到3个角时,通过仿射变换估算第4个角位置
|
||||
|
||||
公式:P_missing = M_inv @ [x_target, y_target, 1]
|
||||
|
||||
3.4 图像缩放
|
||||
默认缩放到0.5倍进行检测(由config控制)
|
||||
|
||||
坐标还原时乘以inv_scale,保持与标定矩阵一致
|
||||
|
||||
4. 数据流示例
|
||||
python
|
||||
输入:
|
||||
- img_rgb: H×W×3 图像
|
||||
- laser_xy: (x_px, y_px) 激光点像素坐标
|
||||
- marker_positions: {0:[0,0,0], 1:[0,30,0], 2:[30,30,0], 3:[30,0,0]} # 4角3D坐标(cm)
|
||||
|
||||
输出:
|
||||
{
|
||||
"ok": True,
|
||||
"dx_cm": 2.5, # 靶面X偏移(cm,向右为正)
|
||||
"dy_cm": -3.2, # 靶面Y偏移(cm,向上为正)
|
||||
"distance_m": 5.43, # 相机到靶面距离(米)
|
||||
"offset_method": "triangle_homography",
|
||||
"distance_method": "pnp_triangle"
|
||||
}
|
||||
5. 鲁棒性设计
|
||||
5.1 参数自适应
|
||||
从config.py动态读取所有阈值(可在线调整)
|
||||
|
||||
三角形边长范围、灰度阈值、对比度要求等均可配置
|
||||
|
||||
5.2 异常处理
|
||||
角点退化检测(距离<3像素判定为重复)
|
||||
|
||||
NaN/Inf校验(单应性矩阵、偏移量、距离)
|
||||
|
||||
距离合理性检查(0.3~20米)
|
||||
|
||||
5.3 降级策略
|
||||
PnP失败 → 只输出偏移,距离置None
|
||||
|
||||
4角检测失败 → 尝试3角补全
|
||||
|
||||
快速路径失败 → CLAHE增强兜底(可选)
|
||||
|
||||
6. 性能特点
|
||||
CPU友好:默认Otsu单次处理,多数场景10-30ms完成检测
|
||||
|
||||
内存可控:最大候选数截断(默认10个),避免组合爆炸
|
||||
|
||||
嵌入式适配:支持图像缩放、早退机制降低计算量
|
||||
|
||||
7. 局限性
|
||||
依赖四个等腰直角三角形(需靶纸特殊设计)
|
||||
|
||||
要求三角形内部足够暗、与背景有对比度
|
||||
|
||||
单应性假设靶面为平面(实际靶纸可能有轻微起伏)
|
||||
|
||||
这套算法在射击训练系统中作为主要定位手段。
|
||||
|
||||
8. 为了加速单应性的计算,引入了yolo模型,一共做了两个模型,一个为靶纸和黑色三角形一体的识别模型,用于做原照片上快速找到靶纸区域。另一个模型是黑色三角形的模型,用于做靶纸区域再找黑色三角形。但是经过对比发现,引入黑色三角形模型反而更慢。入下面的流程A和流程B:
|
||||
yolo靶纸+传统(流程B) yolo靶纸+yolo黑色三角形(流程A)
|
||||
平均值 646.08 916.4457143
|
||||
标准差 94.61300968 57.40401849
|
||||
|
||||
公共前置(两条路都一样)
|
||||
是否用靶环模型裁 Stage1
|
||||
|
||||
TRIANGLE_YOLO_ROI_ENABLE=True 时:跑 靶环 YOLO,得到全图上的 roi_xyxy,后面的三角形都在 img_work = 全图[roi] 上做(必要时再缩成 img_det 给整图传统分支用)。
|
||||
False 时:roi_xyxy=None,三角形在 整幅相机图 上当 img_work。
|
||||
之后都进入 try_triangle_scoring(img_cv, …, roi_xyxy=…, black_yolo_boxes_work=…)
|
||||
|
||||
在里面先做灰度、v_suppress、锐化、det_scale 缩略图等 prep(与是否黑三角模型无关)。
|
||||
差别从 black_yolo_boxes_work 有没有有效子框列表 开始。
|
||||
|
||||
流程 A:用黑色三角形模型(Stage2 黑三角 YOLO)
|
||||
配置要点:TRIANGLE_BLACK_YOLO_ENABLE=True,且 TRIANGLE_BLACK_TRIANGLE_LOCATE_MODE="yolo",并且 已有 Stage1 裁切(roi_xyxy 不能为 None,否则根本不会跑黑三角 YOLO)。
|
||||
|
||||
步骤概要:
|
||||
|
||||
try_black_triangle_boxes_work
|
||||
|
||||
输入:全图 RGB + Stage1 的 ring_roi_xyxy。
|
||||
在 Stage1 裁切图(与训练一致的 slab)上跑 黑三角 YOLO,得到若干个 子框(black_boxes_work,坐标在 裁切图/work 系)。
|
||||
try_triangle_scoring 内
|
||||
|
||||
若 black_yolo_boxes_work 非空:
|
||||
按配置在 Stage1 全分辨率灰度(或缩略灰度,视 det_scale / TRIANGLE_BLACK_YOLO_PATCH_GRAY_SOURCE)上,对每个子框裁 patch,跑 _extract_triangle_from_yolo_patch(子框内:Otsu → 失败再单次 Adaptive + 轮廓 + 形状/颜色)。
|
||||
median_leg 过滤,再 四点分配 ID。
|
||||
若 ≥3 个(通常 4 个)有效:认为 Stage2 成功,跳过 整幅 Stage1 上的 detect_triangle_markers。
|
||||
若 不足 3 个 且未关 fallback:在 缩略后的整幅 work 灰度上再走 detect_triangle_markers(整图 Otsu + 整图 Adaptive×block_sizes + 各类 fallback),与「不用黑三角模型时的传统主路径」同类。
|
||||
后续
|
||||
|
||||
角点从 det 坐标 ×inv_scale 回到 work,再 +roi 原点 回到全图;单应性、补第 4 点、PnP 等与另一条路相同。
|
||||
耗时上多出来的部分:黑三角 YOLO 推理 + 每个子框一遍传统小流水线(成功时通常 不再付整图 detect_triangle_markers)。
|
||||
|
||||
流程 B:不用黑色三角形模型(纯传统定位三角)
|
||||
典型配置(任一即可达到「不用黑三角模型」的效果):
|
||||
|
||||
TRIANGLE_BLACK_YOLO_ENABLE=False,或
|
||||
TRIANGLE_BLACK_TRIANGLE_LOCATE_MODE="traditional"(即使模型开关开着也不跑黑三角 YOLO),或
|
||||
没有 Stage1 ROI(roi_xyxy is None)时,当前逻辑下 也不会跑 Stage2 黑三角 YOLO。
|
||||
此时 black_yolo_boxes_work=None(或不等价于「有子框」)。
|
||||
|
||||
步骤概要:
|
||||
|
||||
try_triangle_scoring 内
|
||||
不跑 子框 _extract_triangle_from_yolo_patch。
|
||||
直接在 img_det(缩略后的 work) 上调用 detect_triangle_markers:
|
||||
全局 Otsu(若 TRIANGLE_SKIP_GLOBAL_OTSU_EXTRACT_ON_YOLO_ROI 在有 ROI 时可能 不算 Otsu 轮廓,但仍会生成 Otsu 图供后续用);
|
||||
可选 象限 ROI(TRIANGLE_ROI_ENABLED);
|
||||
整图 Adaptive(TRIANGLE_ADAPTIVE_BLOCK_SIZES,例如 (11,));
|
||||
不足再走 放宽 approxPolyDP、BlackHat 等。
|
||||
后面同样是过滤、四点组合/象限分配、单应性、PnP 等。
|
||||
特点:没有黑三角 NPU 时间,也 没有「按框重复 4 次子框传统」;但要在 一整张(缩略)ROI 图 上跑一套更重的 整图 pipeline。
|
||||
|
||||
对照一句话
|
||||
用黑三角 YOLO(流程 A) 不用黑三角 YOLO(流程 B)
|
||||
Stage2
|
||||
黑三角模型给子框 → 子框内 Otsu + 至多一次 Adaptive
|
||||
无 Stage2 模型
|
||||
三角角点从哪来
|
||||
优先 子框传统;不够再 整图 detect_triangle_markers
|
||||
只有 整图 detect_triangle_markers
|
||||
和「全图是否只做 Adaptive」
|
||||
子框 不是只做 Adaptive;整图回退时也与全图路径一致(先 Otsu 等)
|
||||
整图路径 也不是只做 Adaptive
|
||||
靶环 YOLO(Stage1 裁切)在 A/B 里都可以开或关,与「黑三角模型」是独立开关。
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
|
||||
1. CPP构建命令:在docker环境下执行以下命令
|
||||
|
||||
cd /data/cpp_ext
|
||||
rm -rf build && mkdir build && cd build
|
||||
|
||||
TOOLCHAIN_BIN=/data/MaixCDK-main/dl/extracted/toolchains/maixcam/host-tools/gcc/riscv64-linux-musl-x86_64/bin
|
||||
PYDEV=/data/python3_lib_maixcam_musl_3.11.6
|
||||
MAIXCDK=/data/MaixCDK-main
|
||||
|
||||
cmake .. -G Ninja \
|
||||
-DCMAKE_C_COMPILER="${TOOLCHAIN_BIN}/riscv64-unknown-linux-musl-gcc" \
|
||||
-DCMAKE_CXX_COMPILER="${TOOLCHAIN_BIN}/riscv64-unknown-linux-musl-g++" \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_FLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" \
|
||||
-DCMAKE_CXX_FLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" \
|
||||
-DPY_INCLUDE_DIR="${PYDEV}/include/python3.11" \
|
||||
-DPY_LIB="${PYDEV}/lib/libpython3.11.so" \
|
||||
-DPY_EXT_SUFFIX=".cpython-311-riscv64-linux-gnu.so" \
|
||||
-DMAIXCDK_PATH="${MAIXCDK}"
|
||||
|
||||
ninja
|
||||
|
||||
|
||||
2. Maixvision 直接跑项目的时候,是复制到板子上的这个目录:/tmp/maixpy_run
|
||||
|
||||
3. 4g 模块的终端测试方法:
|
||||
3.1 一个窗口 ssh 到maixcam的板子上之后,通过 printf 输入命令到 /dev/ttyS2, 然后另外一个窗口通过 cat /dev/ttyS2 输出
|
||||
# 1. 确保 PDP 激活
|
||||
printf 'AT+CGPADDR=1\r\n' > /dev/ttyS2
|
||||
# 2. 开启日志监听(另一个 SSH 窗口)
|
||||
cat /dev/ttyS2
|
||||
# 3. 发送下载命令(原窗口)
|
||||
printf 'AT+MHTTPDLFILE="http://static.shelingxingqiu.com/shoot/v1/main.py","downloaded.py",5120\r\n' > /dev/ttyS2
|
||||
|
||||
4. wifi的启动条件,在 /boot 目录下,看看是否有 wifi.sta 和 wifi.ssid, wifi.pass 这些文件。其中 wifi.sta 是开关文件。
|
||||
如果没有了它就不会启动wifi流程。具体的wifi流程 由 /etc/init.d/S30wifi 控制。它会判断 wifi.sta 是否存在,然后是否启动wifi,还是启动热点。
|
||||
|
||||
5. 给自己的程序打包到基础镜像中,参考:https://wiki.sipeed.com/maixpy/doc/zh/pro/compile_os.html
|
||||
5.1. 按照链接中的步骤,去github上获取了基础镜像,这次使用的是 v4.12.4,把Assets中的下面几样东西下载下来,我是在windows的wsl中执行的,注意,
|
||||
假如是在windows中下载的文件,在wsl中编译会很慢,所以我采用的是直接在wsl中下载,放到wsl的自己的文件系统中。
|
||||
1)maixcam-2025-12-31-maixpy-v4.12.4.img.xz
|
||||
2)maixcam_builtin_files.tar.xz
|
||||
3)MaixPy-4.12.4-py3-none-any.whl
|
||||
4)Source code(zip)
|
||||
5.2. 把自己的文件放到 buildtin_files中:
|
||||
1)我把项目文件目录 t11 放到了 maixcam_builtin_files\maixapp\apps 这个目录下。
|
||||
2)为了能让它自启动,我把 auto_start.txt 放到了 maixcam_builtin_files\maixapp 这个目录下。
|
||||
|
||||
5.3. 然后在解压后的源码中找到tools/os目录下 /home/saga/maixcam/MaixPy-4.12.4/tools/os/maixcam
|
||||
执行
|
||||
export MAIXCDK_PATH=/home/saga/maixcam/MaixCDK
|
||||
编译:
|
||||
./gen_os.sh ../../../../../maixcam/maixcam-2025-12-31-maixpy-v4.12.4.img ../../../../../maixcam/MaixPy-4.12.4-py3-none-any.whl ../../../../../maixcam/maixcam_builtin_files 0 maixcam
|
||||
注意,在编译过程中,也会去 github 下载内容,所以需要打开梯子。
|
||||
5.4. 等待编译完成,会编译成镜像文件,然后根据 https://wiki.sipeed.com/hardware/zh/maixcam/os.html 这个指引来烧录系统。
|
||||
5.5. 烧录完系统后,需要安装 runtime, 可以按照 https://wiki.sipeed.com/maixpy/doc/zh/README_no_screen.html 这个来升级运行库,或者直接在 Maixvision 中链接的时候安装 runtime。
|
||||
5.6. 安装 runtime 之后,重启,我们的系统就会自己启动起来了。
|
||||
|
||||
遇到问题:
|
||||
/mnt/d/code/shooting/compile_maixcam/MaixPy-4.12.4/MaixPy-4.12.4/tools/os/maixcam/fuse2fs: error while loading shared libraries: libfuse.so.2: cannot open shared object file: No such file or directory
|
||||
解决办法:
|
||||
安装 libfuse2
|
||||
sudo apt update
|
||||
sudo apt install libfuse2
|
||||
|
||||
遇到问题:
|
||||
python 缺少 yaml
|
||||
解决办法:
|
||||
pip install pyyaml
|
||||
|
||||
遇到问题:
|
||||
./build_all.sh: line 56: maixtool: command not found
|
||||
解决办法:
|
||||
pip install maixtool
|
||||
|
||||
遇到问题:
|
||||
./update_img.sh: line 80: mcopy: command not found
|
||||
解决办法:
|
||||
sudo apt update
|
||||
sudo apt install mtools
|
||||
|
||||
6. 相机标定:
|
||||
然后在板子上跑 test 目录下的 test_camera_rtsp.py ,让相机启动了一个服务,然后在电脑上接收这个视频流,并且跑opencv 内置的标定程序:
|
||||
set OPENCV_FFMPEG_CAPTURE_OPTIONS="rtsp_transport;tcp"
|
||||
opencv_interactive-calibration -t=chessboard -w=9 -h=6 -sz=0.025 -v="http://192.168.1.81:8000/stream" 2>nul
|
||||
|
||||
|
||||
7. 生成训练图片:在test目录下,执行以下命令。注意,其中 D:\code\shooting\target_photo\write.png 是靶纸的图片。
|
||||
D:\data\test_target_photo 是用来叠加的背景图
|
||||
|
||||
7.1 生成靶纸及黑色三角形的截图的图片,带动动,但1.12的外框
|
||||
bak
|
||||
python .\synth_compose_yolo.py --perspective 0.04 --perspective-prob 0.8 --color-jitter 0.6 --bg-dir D:\data\test_target_photo --fg D:\code\shooting\target_photo\write.png --out ./synth_out --class-name triangle --zip ./maix_dataset.zip --num 60 --triangles-json archery_triangles_default.json --format voc --stage2-crop --stage2-pad-min 0.03 --stage2-pad-max 0.18 --motion-prob 0.9 --motion-kernel-max 8 --blur-max 0 --triangle-bbox-pad-frac 0.12
|
||||
|
||||
bak_2
|
||||
python synth_keypoints_right_angle.py --bg-dir D:\data\test_target_photo --fg D:\code\shooting\target_photo\write.png --triangles-json archery_triangles_default.json --out ./synth_out --num 1000 --offscreen-shift-prob 0.3 --offscreen-shift-frac 0.4 --offscreen-min-visible 1 --stage2-crop --stage2-pad-min 0.03 --stage2-pad-max 0.18 --motion-prob 0.9 --motion-kernel-max 8 --blur-max 0 --perspective-mode planar --yaw-max-deg 10 --pitch-max-deg 8 --roll-max-deg 4 --planar-focal-frac 1.45 --perspective-prob 0.4
|
||||
|
||||
python synth_keypoints_right_angle.py --bg-dir D:\data\test_target_photo --fg D:\code\shooting\target_photo\write.png --triangles-json archery_triangles_default.json --out ./synth_out --num 1000 --offscreen-shift-prob 0.3 --offscreen-shift-frac 0.4 --offscreen-min-visible 1 --stage2-crop --stage2-pad-min 0.03 --stage2-pad-max 0.18 --motion-prob 1.0 --motion-kernel-max 8 --blur-max 0 --perspective-mode planar --yaw-max-deg 10 --pitch-max-deg 8 --roll-max-deg 4 --planar-focal-frac 1.45 --perspective-prob 0.4
|
||||
|
||||
|
||||
python pose_pixel_metrics.py --model D:\code\archery\runs\pose\runs\pose\target_pose_train\weights\best.pt --data D:\code\archery\datasets\dataset_pose.yaml --imgsz 640
|
||||
@@ -0,0 +1,41 @@
|
||||
1. 问题描述:开机失败,一直遇到Traceback (most recent call last):
|
||||
File "/tmp/maixpy_run/main.py", line 525, in <module>
|
||||
cmd_str()
|
||||
File "/tmp/maixpy_run/main.py", line 102, in cmd_str
|
||||
camera_manager.init_camera(640, 480)
|
||||
File "/tmp/maixpy_run/camera_manager.py", line 59, in init_camera
|
||||
self._camera = camera.Camera(width, height)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
RuntimeError: : Runtime error: mmf vi init failed
|
||||
解决方案:
|
||||
根据过往经验,极有可能是摄像头的接线有问题。因为在测试环境,摄像头是通过一个24针转22针的线出来的,然后再通过一个接线中继,连接到一个22针
|
||||
的fpc线到Maixcam。接线中继如果是24针的,多了两针,需要选好一边然后对连。但这里很容易出错或者松动。可以先用摄像头本身的金色接线直接接到
|
||||
Maixcam,然后跑test目录下的test_cammera.py,看看能不能正常启动,如果正常,就确定是中继接线的问题。
|
||||
|
||||
2. 问题描述:202609 批次的拓展版,在连接 202601 批次的电源板,或者不链接电源板的时候,开机后不久,出错,程序退出,日志是:
|
||||
[v1.2.10] [INFO] network.py:1078 - [NET] TCP主线程启动
|
||||
[v1.2.10] [INFO] network.py:406 - [NET] WiFi不可用或无法连接服务器,使用4G网络
|
||||
[v1.2.10] [INFO] network.py:475 - 连接到服务器,使用4G...
|
||||
[v1.2.10] [INFO] network.py:527 - [4G-TCP] AT+MIPCLOSE=2 response:
|
||||
OK
|
||||
+MIPCLOSE: 2
|
||||
-- [E] read failed
|
||||
Trigger signal, code:SIGSEGV(11)!
|
||||
maix multi-media driver released.
|
||||
ISP Vipipe(0) Free pa(0x8a52c000) va(0x0x3fbeb5e000)
|
||||
program exit failed. exit code: 1.
|
||||
|
||||
解决方案:
|
||||
从日志看,就是开始发送登录信息之后就崩溃了。出发了底层的read failed。经过排查,是一定要插上电源板的数据连线,以及电源板要插上电池。这个应该是登录时需要读电源电压数据。后面我们已经优化了日志,而且增加了对ina226的试探,但发现ina226不存在的时候,就直接返回电压和电流为0.0。而且,一定要注意,在新配套的电源板和核心板上面,才能正常读到电流和电压。
|
||||
|
||||
3. a)问题描述:202609 批次的拓展版,有一块maixcam的蓝灯常亮,询问maixcam的人,他们觉得应该是卡没有插好。但是拓展版上的激光口挡住了数据卡的出口,
|
||||
没法拔出检查,
|
||||
解决方案:需要做拓展版的公司(深链鑫创)在做好板子之后,确定系统能正常启动
|
||||
|
||||
b)问题描述:2022609 批次的拓展板,有一次maixcam的蓝灯亮的时候很长,不会闪烁,后面把sd卡插进去一点,又恢复正常了,初步怀疑是射箭时没有缓冲,
|
||||
导致了sd 卡被撞松了
|
||||
|
||||
4. 问题描述:4G模块不可用,模块的绿灯没有闪亮
|
||||
解决方案:有这样的一种情况,就是4G模块的天线,触碰到了旁边的电容,导致短路,所以模块启动失败。需要保证电容和天线的金属头不会触碰
|
||||
5.
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
1. 4G OTA 下载的时候,为什么使用十六进制下载,读取 URC 事件?
|
||||
因为使用二进制下载的时候,经常会出现错误,并且会失败?然后最稳定传输的办法,是每次传输的时候,是分块,而且每次分块都要“删/建”http实例。推测原因是因为我们现在是直接传输文件的源代码,代码中含有了一些字符串可能和 AT指令重复,导致了 AT 模块在解释的时候出错。而使用 16 进制的方式,可以避免这个问题。因为十六进制直接把数据先转成了字符串,然后在设备端再把字符串转成数据,这样就不可能出现 AT的指令,从而减少了麻烦。
|
||||
2. 4G OTA 下载的时候,为什么不用 AT 模块里 HTTPDLFILE 的指令?
|
||||
因为在测试中发现,使用 HTTPDLFILE,其实是下载到了 4G 模块内部,需要重新从模块内部转到存储卡,而且 4G 模块的存储较小,大概只有 40k,所以还需要分块来下载和转存,比较麻烦,于是最终使用了使用读取串口事件的模式。
|
||||
3. 4G OTA 下载的时候,为什么不用 AT 模块里 HTTPREAD 的指令?
|
||||
因为之前测试发现,READ模式其实是需要多步:
|
||||
3.1. AT+MHTTPCREATE
|
||||
3.2. AT+MHTTPCFG
|
||||
3.3. AT+MHTTPREQUEST
|
||||
3.4. AT+MHTTPREAD
|
||||
它其实也是把数据下载到 4g 模块的缓存里,然后再从缓存里读取出来。所以也是比较繁琐的,还不如 HTTPDLFILE 简单。
|
||||
4. WiFi OTA 流程(ota_manager.handle_wifi_and_update())
|
||||
* 解析 ota_url 得到 host:port
|
||||
* 调用 network_manager.connect_wifi(ssid, password, verify_host=host, verify_port=port, persist=True)
|
||||
* 只有“能连上 WiFi 且能访问 OTA host:port”才会把新凭证保留在 /boot
|
||||
* 连接成功后开始下载 OTA 文件(download_file())
|
||||
* 下载成功则 apply_ota_and_reboot()
|
||||
5. TCP 通信
|
||||
1) 平时 TCP 通信主流程(network_manager.tcp_main())
|
||||
外层无限循环:一直尝试保持与服务器的 TCP 会话。
|
||||
每轮开始:
|
||||
如果 OTA 正在进行:暂停(避免抢占资源/串口)。
|
||||
connect_server():建立 TCP 连接(自动选 WiFi 或 4G)。
|
||||
发送“登录包”(msg_type=1),等待服务器返回“登录成功”。
|
||||
登录成功后进入内层循环:
|
||||
接收数据:
|
||||
WiFi:非阻塞 recv();没数据返回 b"";有数据进入缓冲区拼包解析。
|
||||
4G:从 ATClient 的队列 pop_tcp_payload() 取数据。
|
||||
处理命令/ACK:
|
||||
登录响应、心跳 ACK、OTA 命令、关机命令、日志上传命令等。
|
||||
发送业务队列:
|
||||
从高优/普通队列取 1 条,发送失败会放回队首,并断线重连(不再丢消息)。
|
||||
发送心跳:
|
||||
按 HEARTBEAT_INTERVAL 发心跳包。
|
||||
心跳失败会计数(当前为连续失败到阈值才重连)。
|
||||
任何发送/接收致命失败:
|
||||
关闭 socket/断开连接 → 跳出内层循环 → 外层等待一会儿后重新 connect_server() → 重新登录。
|
||||
6. “WiFi 连接/验证”
|
||||
TCP 连接建立与网络选择(connect_server() / select_network())
|
||||
* select_network():WiFi 优先,但要求:
|
||||
is_wifi_connected() 为 True(系统层面有 WiFi IP 或 Maix WLAN connected)
|
||||
且能连到 TCP 服务器 SERVER_IP:SERVER_PORT
|
||||
否则回退到 4G
|
||||
* connect_server():
|
||||
若已有连接:WiFi 会做 _check_wifi_connection() 轻量检查;4G 直接认为 OK(由 AT 层维护)。
|
||||
否则按网络类型走:
|
||||
WiFi:创建 socket → connect → setblocking(False)(接收用非阻塞)
|
||||
4G:AT+MIPOPEN 建链
|
||||
WiFi 链接(connect_wifi())
|
||||
当前 connect_wifi() 的关键特点是:必须让 /etc/init.d/S30wifi restart 真正用新 SSID 去连,所以会临时写 /boot/wifi.ssid 和 /boot/wifi.pass,失败自动回滚。
|
||||
流程是:
|
||||
(1) 备份旧配置
|
||||
* /boot/wifi.ssid、/boot/wifi.pass
|
||||
* /etc/wpa_supplicant.conf(尽量备份)
|
||||
(2) 写入新凭证
|
||||
* 把新 ssid/pass 写到 /boot/*
|
||||
-(同时尽量写 /etc/wpa_supplicant.conf,但不强依赖)
|
||||
(3) 重启 WiFi 服务:/etc/init.d/S30wifi restart
|
||||
(4) 等待获取 IP(默认 20 秒,可调)
|
||||
(5) 验证可用性,连到 verify_host:verify_port
|
||||
(6) 成功
|
||||
* persist=True:保留 /boot/*(持久化)
|
||||
* persist=False:回滚 /boot/* 到旧值(不重启,当前连接仍可继续)
|
||||
(7) 失败
|
||||
* 回滚 /boot/* + 回滚 /etc/wpa_supplicant.conf(如果有备份)
|
||||
* 再 S30wifi restart 恢复旧网络
|
||||
* 返回错误
|
||||
|
||||
7. 日志上传(inner_cmd == 43),当前只支持 wifi 上传日志
|
||||
命令带 ssid/password/url 时:
|
||||
* 若 WiFi 未连接:先 connect_wifi(..., verify_host=upload_host, verify_port=upload_port, persist=True)
|
||||
上传内容:
|
||||
* sync # 把日志从内存同步到文件
|
||||
* 快照 app.log* 到 /tmp staging
|
||||
* 打包成 tar.gz(默认)或 zip
|
||||
* 以 multipart/form-data 的 file 字段 POST 到 url
|
||||
|
||||
8. 自动关机:
|
||||
hardware中设定了开停表,然后再增加了获取idle的时间。
|
||||
自动关机的时机: 超过配置的idle时长,
|
||||
禁止自动关机的情况:1.校准中,2.OTA中
|
||||
重启计时的时机:1.校准完成,2.命令触发射箭,3.真实触发射箭,4.初始化完成
|
||||
9. Wifi网络监控:
|
||||
有两次发现wifi网络下,有些消息发送很慢,但具体是什么缘故还不清楚,现在增加了wifi网络下的检测,并一旦发现wifi的网络质量差,就会切换到4G。
|
||||
WiFi 连接成功
|
||||
↓
|
||||
启动后台监测线程
|
||||
↓
|
||||
每 5 秒循环:
|
||||
测量 RTT (1 样本,600ms timeout)
|
||||
获取 RSSI
|
||||
更新缓存
|
||||
判断是否差:
|
||||
- RTT >= 600ms → 差
|
||||
- RTT >= 350ms 且 RSSI <= -80dBm → 差
|
||||
↓
|
||||
如果质量差:
|
||||
快速重试2次,如果其中任意一次网络恢复了,继续使用wifi。否则,
|
||||
调用 _switch_to_4g_due_to_poor_wifi()
|
||||
关闭 WiFi socket
|
||||
重置连接状态
|
||||
尝试切换到 4G
|
||||
↓
|
||||
上层检测到连接断开:
|
||||
重新 connect_server() → 自动选择 4G
|
||||
|
||||
10. 现在使用的相机,其实是支持更大的分辨率的,比如说1920*1280,但是由于我们的图像处理,拍照处理之后很容易触发OOM。
|
||||
|
||||
11. 环数计算流程:
|
||||
现在设备侧的目标是:算出箭点相对靶心的偏移(dx,dy),单位是物理厘米(cm),然后把它作为 x,y 上报给后端;后端再去算环。
|
||||
设备侧本身不直接算环数,它算的是偏移与距离,并上报。
|
||||
|
||||
算法流程(一次射箭从触发到上报)
|
||||
1) 触发后取一帧图
|
||||
在 process_shot() 里读取相机帧并调用 analyze_shot(frame)
|
||||
2) 确定激光点(laser_point)
|
||||
|
||||
analyze_shot() 第一步先确定激光点 (x,y)(像素坐标):
|
||||
|
||||
硬编码:config.HARDCODE_LASER_POINT=True → 用 laser_manager.laser_point
|
||||
已校准:laser_manager.has_calibrated_point() → 用校准值
|
||||
动态模式:先 detect_circle_v3(frame, None) 粗估距离,再根据距离反推激光点
|
||||
代码在:
|
||||
|
||||
if config.HARDCODE_LASER_POINT:
|
||||
...
|
||||
elif laser_manager.has_calibrated_point():
|
||||
...
|
||||
else:
|
||||
_, _, _, _, best_radius1_temp, _ = detect_circle_v3(frame, None)
|
||||
distance_m_first = estimate_distance(best_radius1_temp) ...
|
||||
laser_point = laser_manager.calculate_laser_point_from_distance(distance_m_first)
|
||||
3) 优先走三角形路径(成功就直接用于上报 x/y)
|
||||
如果 config.USE_TRIANGLE_OFFSET=True,先尝试识别靶面四角三角形标记:
|
||||
|
||||
if getattr(config, "USE_TRIANGLE_OFFSET", False):
|
||||
K, dist_coef, pos = _get_triangle_calib()
|
||||
img_rgb = image.image2cv(frame, False, False)
|
||||
tri = try_triangle_scoring(img_rgb, (x, y), pos, K, dist_coef, ...)
|
||||
if tri.get("ok"):
|
||||
return {... "dx": tri["dx_cm"], "dy": tri["dy_cm"], "distance_m": tri.get("distance_m"), ...}
|
||||
这一步里 try_triangle_scoring() 做了两件事(都在 triangle_target.py):
|
||||
|
||||
单应性(homography):把激光点从图像坐标映射到靶面坐标系,得到(dx,dy)(cm)
|
||||
PnP:用识别到的角点与相机标定,估算 相机到靶的距离 distance_m
|
||||
关键代码:
|
||||
|
||||
ok_h, tx, ty, _H = homography_calibration(...)
|
||||
out["dx_cm"] = tx
|
||||
out["dy_cm"] = -ty
|
||||
out["distance_m"] = dist_m
|
||||
out["distance_method"] = "pnp_triangle"
|
||||
注意:这里 dy_cm 取了负号,是为了和现网约定一致(laser_manager.compute_laser_position 的坐标方向)。
|
||||
|
||||
4) 三角形失败 → 回退圆形/椭圆靶心检测(兜底)
|
||||
如果三角形不可用或识别失败,就走传统靶心检测:
|
||||
|
||||
detect_circle_v3(frame, laser_point) 找黄心/红心、半径、椭圆参数
|
||||
用 laser_manager.compute_laser_position() 把像素偏移换算成厘米偏移(dx,dy)
|
||||
在 shoot_manager.py:
|
||||
|
||||
result_img, center, radius, method, best_radius1, ellipse_params = detect_circle_v3(frame, laser_point)
|
||||
if center and radius:
|
||||
dx, dy = laser_manager.compute_laser_position(center, (x, y), radius, method)
|
||||
distance_m = estimate_distance(best_radius1) ...
|
||||
在 laser_manager.compute_laser_position()(核心换算逻辑):
|
||||
|
||||
r = radius * 5
|
||||
target_x = (lx-cx)/r*100
|
||||
target_y = (ly-cy)/r*100
|
||||
return (target_x, -target_y)
|
||||
这里 (像素差)/(radius*5)*100 是你们旧约定下的“像素→厘米”比例模型(并且 y 方向同样取负号)。
|
||||
|
||||
5) 上报数据:把(dx,dy) 作为 x/y 发给后端
|
||||
最终上报发生在 process_shot(),直接把 dx,dy 填到 inner_data["x"],["y"]:
|
||||
|
||||
srv_x = round(float(dx), 4) if dx is not None else 200.0
|
||||
srv_y = round(float(dy), 4) if dy is not None else 200.0
|
||||
inner_data = {
|
||||
"x": srv_x,
|
||||
"y": srv_y,
|
||||
"d": round((distance_m or 0.0) * 100),
|
||||
"m": method if method else "no_target",
|
||||
"offset_method": offset_method,
|
||||
"distance_method": distance_method,
|
||||
...
|
||||
}
|
||||
network_manager.safe_enqueue(...)
|
||||
x,y:物理厘米(cm)
|
||||
d:相机到靶距离(m→cm,乘 100;三角形成功时来自 PnP)
|
||||
m/offset_method/distance_method:标记本次用的算法路径(triangle / yellow / pnp 等)
|
||||
后端收到 x,y 后,再用你之前给的 Go 公式 CalculateRingNumber(x,y,tenRingRadius) 计算环数。
|
||||
|
||||
你现在的“环数计算”实际依赖关系
|
||||
最好路径(快+稳):三角形 → dx,dy(单应性) + distance_m(PnP)
|
||||
兜底路径:圆/椭圆靶心 → dx,dy(基于黄心半径比例/透视校正) + distance_m(黄心半径估距)
|
||||
|
||||
12. 4g模块上传文件:
|
||||
|
||||
Upload images from MaixCam to Qiniu cloud via ML307R 4G module's AT commands. The HTTP body requires multipart/form-data with real CR/LF bytes (0x0D 0x0A) in boundaries.
|
||||
Methods Tried
|
||||
# Method AT Commands Result Root Cause
|
||||
1 Raw binary, no encoding MHTTPCONTENT with raw bytes + length param ERROR at first chunk CR/LF in binary data terminates AT command parser
|
||||
2 Encoding mode 2 (escape) MHTTPCFG="encoding",0,2 + \r\n escapes Server 400 Bad Request Module sends literal text \r\n to server, NOT actual 0x0D 0x0A bytes. Multipart body is garbled
|
||||
3 Encoding mode 1 (hex) MHTTPCFG="encoding",0,1 + hex-encoded data CME ERROR: 650/50 Firmware doesn't properly support hex mode for MHTTPCONTENT
|
||||
4 No chunked mode Skip MHTTPCFG="chunked" CME ERROR: 65 Module requires chunked mode to accept MHTTPCONTENT at all
|
||||
5 Single large MHTTPCONTENT All data in one command (2793 bytes) +MHTTPURC: "err",0,5 (timeout) Possible buffer limit; module hangs then times out
|
||||
6 Per-chunk HTTP instance (OTA style) CREATE→POST→DELETE per chunk Not feasible Each instance = separate HTTP request; Qiniu needs complete body in single POST
|
||||
Conclusion: AT HTTP layer (MHTTPCONTENT) is fundamentally broken for binary uploads.
|
||||
The Solution: Raw TCP Socket (MIPOPEN + MIPSEND)
|
||||
Bypass the AT HTTP layer entirely. Open a raw TCP connection and send a hand-crafted HTTP POST:
|
||||
plaintext
|
||||
AT+MIPCLOSE=3 // Clean up old socket
|
||||
AT+MIPOPEN=3,"TCP","upload.qiniup.com",80 // Raw TCP connection
|
||||
AT+MIPSEND=3,1024 → ">" → [raw bytes] → OK // Binary-safe!
|
||||
AT+MIPSEND=3,1024 → ">" → [raw bytes] → OK
|
||||
AT+MIPSEND=3,766 → ">" → [raw bytes] → OK
|
||||
// Response: +MIPURC: "rtcp",3,<len>,HTTP/1.1 200 OK...
|
||||
AT+MIPCLOSE=3
|
||||
Why it works:
|
||||
MIPSEND enters prompt mode (>) — after the >, the AT parser treats ALL bytes as data, including CR/LF
|
||||
We construct the complete HTTP request ourselves (headers + Content-Length + multipart body) with real CRLF bytes
|
||||
|
||||
Key bug found during integration: _send_chunk() wrapped calls in self.at._cmd_lock, but self.at.send() also acquires the same lock internally — threading.Lock() is not reentrant, causing deadlock. Fixed by removing the outer lock (the network_manager.get_uart_lock() already provides thread safety).Trade-off: UART is locked during the entire upload, so heartbeats pause. For small JPEG files (~2-80KB), this is 5-20 seconds — acceptable if server heartbeat timeout is generous
|
||||
|
||||
|
||||
13. 算环数算法1:「黄心 + 红心」椭圆/圆:主要在 vision.py 的 detect_circle_v3() 里完成:颜色先用 HSV 做掩码,再在轮廓上做面积、圆度筛选,黄圈用椭圆拟合,红圈预先筛成候选,最后用几何关系配对。
|
||||
|
||||
1. 黄色怎么判、范围是什么?
|
||||
图像先转 HSV(cv2.COLOR_RGB2HSV,注意输入是 RGB)。
|
||||
饱和度 S 整体乘 1.1 并限制在 0–255(让黄色更「显」一点)。
|
||||
黄色 inRange(OpenCV HSV,H 多为 0–179):
|
||||
通道 下限 上限
|
||||
H 7 32
|
||||
S 80 255
|
||||
V 0 255
|
||||
在黄掩码上找轮廓后,还要满足:面积 > 50,圆度 > 0.7(circularity = 4π·面积/周长²),且点数 ≥5 才 fitEllipse 当黄心椭圆。
|
||||
|
||||
2. 红色怎么判、范围是什么?
|
||||
红色在 HSV 里跨 0°,所以用 两段 H 做并集:
|
||||
两段分别是:
|
||||
H 0–10,S 80–255,V 0–255
|
||||
H 170–180,S 80–255,V 0–255
|
||||
红轮廓候选:面积 > 50,圆度 > 0.6(比黄略松),再拟合椭圆或最小外接圆得到圆心和半径。
|
||||
|
||||
3. 「黄心」和「红心」怎样算一对?(几何范围)
|
||||
对每个黄圈,在红色候选里找第一个满足:
|
||||
|
||||
两圆心距离 dist_centers < yellow_radius * 1.5
|
||||
红半径 red_radius > yellow_radius * 0.8(红在外圈、略大)
|
||||
dist_centers = math.hypot(ddx, ddy)
|
||||
if dist_centers < yellow_radius * 1.5 and rc["radius"] > yellow_radius * 0.8:
|
||||
小结:黄色 = HSV H∈[7,32]、S≥80(且 S 放大 1.1)+ 形态学闭运算 + 面积/圆度;红色 = 两段 H(0–10 与 170–180)、S≥80 + 闭运算 + 面积/圆度;配对用 同心/包含 的距离与半径比例阈值。若你还关心 laser_manager.py 里「激光红点」的另一套阈值(LASER_*),那是另一条链路,和靶心黄/红 HSV 可以分开看。
|
||||
|
||||
14. 算环数算法2:
|
||||
使用单应性矩阵计算:镜头中心点(照片中心像素)到虚拟平面的转换。它不需要知道相机在 3D 空间中的具体位置,直接通过单应性矩阵 H的逆运算,将 2D 像素“翻译”成虚拟平面上的 2D 坐标。
|
||||
|
||||
一、转换的本质:2D 到 2D 的“查字典”
|
||||
单应性变换(Homography)是平面到平面的映射。它不处理 3D 空间中的“投影线”,而是直接建立图像像素 (u,v) 与虚拟平面坐标 (x,y) 的一一对应关系。
|
||||
你可以把单应性矩阵 H想象成一本“翻译字典”:
|
||||
正变换 H:已知靶纸上的真实位置 (x,y),查字典得到它在照片上哪个像素 (u,v)。
|
||||
逆变换 H−1:已知照片上的像素 (u,v)(如镜头中心点),查字典反推它在靶纸上的真实位置 (x,y)。
|
||||
这个“虚拟平面”就是你的靶纸平面(Z=0 的世界坐标系)。算法没有在物理上移动任何点,只是在做坐标系的换算。
|
||||
|
||||
二、详细步骤:镜头中心点如何“落地”
|
||||
|
||||
相机分辨率是 640x480,镜头中心点(光轴与图像的交点)通常是 (u0,v0)=(320,240)。
|
||||
1. 输入:镜头中心点(像素)
|
||||
2. 核心运算:乘以逆矩阵
|
||||
通过 4 个黑色三角形的角点(已知真实坐标)计算出了单应性矩阵 H。现在使用它的逆矩阵 H−1
|
||||
3. 输出:虚拟平面上的落点(物理坐标)
|
||||
计算后,你会得到:(xhit,yhit)
|
||||
这就是镜头中心点对应的靶纸上的真实位置(单位:毫米)。
|
||||
4. 计算环数
|
||||
由于虚拟平面原点 (0,0)就是靶纸圆心,直接计算欧氏距离。
|
||||
这个 d就是箭着点偏离圆心的真实物理距离,直接用于环数判定。
|
||||
@@ -0,0 +1,25 @@
|
||||
你现在要防的是“别人拿到设备/拿到代码包后,能伪造请求、刷接口、下发恶意 OTA、甚至劫持通信”。单靠隐藏 Python 源码只能提高门槛,真正的安全要靠协议和密钥设计。结合你仓库里实际内容,建议你重点隐藏/整改这些点(按风险排序)。
|
||||
|
||||
|
||||
1.4 OTA 下发“url”如果缺少强校验,就是远程代码执行入口
|
||||
你 OTA 逻辑里从服务器指令拿到 url 就去下载并替换文件/重启(这是正常 OTA),但安全性取决于:
|
||||
是否只允许白名单域名/路径
|
||||
是否强制 https 并校验服务器证书
|
||||
是否对 OTA 包做签名校验(最关键)
|
||||
你这里能看到固定域名 static.shelingxingqiu.com 的特殊处理(ota_manager.py 里还在纠结 http/https),这块一定要“服务端签名 + 设备端验签”,否则隐藏源码也没用。
|
||||
|
||||
2. 建议隐藏(中风险,但很容易被人利用)
|
||||
2.1 所有服务器地址/端口/API 路径(可被用于扫描、压测、撞库、协议逆向)
|
||||
这些在 config.py 是明文:
|
||||
SERVER_IP = "stcp.shelingxingqiu.com"SERVER_PORT = 50005HTTP_URL = "http://ws.shelingxingqiu.com"HTTP_API_PATH = "/home/shoot/device_fire/arrow/fire"
|
||||
注意:即使你把它们藏进 .so,攻击者仍可能通过抓包/观察 DNS/SNI/流量拿到域名或 IP,所以“隐藏”只能降低静态分析风险,不能替代鉴权/签名。
|
||||
|
||||
|
||||
|
||||
3. 最有效的“防黑”措施(比隐藏更关键)
|
||||
|
||||
所有上报/控制命令加签名 + nonce/timestamp + 服务端防重放(别人抓到一次包也不能复用)
|
||||
OTA 包必须做签名校验(设备端内置公钥,下载后验签通过才应用)
|
||||
|
||||
如果你告诉我:你们服务端目前能不能改协议(例如新增签名字段、下发 challenge、做 OTA 签名),我可以按“最小改动但提升最大安全”的顺序,帮你规划一套从现状平滑升级的方案。
|
||||
|
||||
Reference in New Issue
Block a user