
清空记录
历史记录
取消
清空记录
历史记录

很多项目里"相机"和"AI识别"是两套东西:相机负责拍照,AI识别另起一个程序跑推理。本文基于触觉智能RK3576开发板Purple Pi OH2开发板制作的采集终端(RkDataTerminal)Demo里做的另一种结合方式——把NPU检测直接嵌进采集链路:相机取帧→模型出框→框烧进照片→连同当时的通道数据一起归档上报;值守时更是1fps持续检测,人/动物经过就自动拍下带框的留证照片。
采集终端演示视频:
本文给出链路、关键代码、实测数据,以及"AI 如何部署上板"的完整步骤
目标:不是"拍照 + 识别",而是"拍下即判"
传统做法:相机拍一张图存盘,再让另一个程序读图去推理——两者时间对不上、数据也对不上。
我们要的是一次采集完成三件事:
拍摄:拿到现场画面(拍照 4K / 值守低帧率取帧);
判断:同一时刻送 NPU 推理,得到目标类别与位置;
留寸:把检测框和当时的采集数据烧进同一张图并入库,附带结构化 JSON。
一句话:证据自证——图上既有现场,也有判定,还有当时的数据。
总体设计:三条链路,一张图看懂
三条链路的分工:
| 拍照留证 | |||
| 值守检测 | |||
| 远程查看 |
相机侧:两条取帧路径
Camera2 下"预览 / 拍照 / 录像"通常不能同会话共存(尤其这套 legacy 包装的 HAL),所以拍照采用按需重建会话:
预览会话(preview + 值守YUV) └─ takePhoto → 换成 still 会话(preview + JPEG ImageReader) └─ 出图后 restorePreview() → 回到预览会话
代价是每次重建约1.2s(HAL实测),所以拍照不能太频繁——这也是后面"值守 15s冷却"的由来之一。
值守要在不打断预览的前提下持续拿帧。做法是给预览会话再挂一个720p的YUV ImageReader,作为重复请求的第二个target。
这里有个极易踩的坑:如果回调里觉得"没到检测节奏"就直接return、不取帧,两个缓冲很快占满,相机侧没有空 buffer 就会停摆——实测帧率掉到~6 秒一帧。
正确写法是每回调必排空,节奏另判:
// CameraController.onWatchFrame()
image = reader.acquireLatestImage(); // 先取走最新帧 → 缓冲永远有空位
if (now - mLastWatchMs < MIN_INTERVAL) { // 到了节奏才往下送
return; // 已经 drain,直接丢不影响相机
}
mLastWatchMs = now;
mWatchListener.onWatchFrame(w, h, nv21); // 交给检测线程一句话记住:ImageReader 必须"随到随清",节流逻辑放后面。
另外两条相机侧硬约束:
预览分辨率必须 ≤1080P:试过 4K 预览,会把 legacy HAL 直接拖崩(device error 3);
拍照/录像期间值守暂停:会话被占用,恢复预览后值守自动续上。
AI 侧:从一帧图像到一组框
模型输入固定640×640,而相机画面是16:9(如1280×720)。直接拉伸会变形,所以用letterbox:等比缩放+居中补边(补灰色114)。
关键是把缩放比和补边量记下来,后面才能把模型输出的框坐标反算回原图:
float scale = Math.min(640f / w, 640f / h); // 等比 int nw = round(w * scale), nh = round(h * scale); int padX = (640 - nw) / 2, padY = (640 - nh) / 2; // 补边量 // …缩放绘制到 640x640,再把像素读成 byte[] RGB 喂给 native
不假设固件预装了 NPU 运行时,把两样东西直接打进 APK:
Android.bp 里真正关键的两行:
cc_library_shared {
name: "librkdt_yolo",
stl: "c++_static", // 必须静态 libc++:App 命名空间没有动态 libc++.so
}
android_app {
name: "RkDataTerminal",
jni_libs: ["librkdt_yolo", "librknnrt"], // 两个 .so 一起内嵌进 APK
}native 侧(jni/Detector.cpp + 移植自官方 demo 的 postprocess.cc)职责很单一:
rknn_inputs_set(uint8 RGB, NHWC) → rknn_run → 三头 int8 输出 → 反量化(zp/scale) + 置信度过滤 + NMS → 出框 class/score/box
出框坐标先在 640 坐标系里,再用 4.1 记下的 scale/padX/padY 反算回原图:
原图坐标 = (框坐标 − pad) / scale
这就是"结合"的第一处细节:相机给的尺度和模型要的尺度不一样,中间的 letterbox + 逆映射是必须补齐的一环。
两条入口共用一套"letterbox→native→逆映射"逻辑,仅喂进去像素来源不同。
结合点:什么时候让 AI 干活
takePhoto → JPEG(4K) ├─ 送 NPU 检测 → dets[] ├─ 画水印:站点/SN/时间/8 路通道值 + dets 的框与类别 └─ 入库:记录 JSON 里追加 detections[],图为带框的 _wm.jpg
检测在原始大图上算,水印在JPEG上,两者坐标系一致⇒图上的框就是真实位置。
值守每秒判一帧,命中就自动走上面的"拍照即判":
// handleWatchDetections()
if (best != null && best.score >= TRIGGER_SCORE // 0.55
&& now - mLastAlertMs > ALERT_COOLDOWN_MS) { // 15s 冷却
mLastAlertMs = now;
mAutoNote = label(best.cls) + " " + best.score; // 写进水印/摘要
triggerCapture(); // 自动留证
}触发后的记录摘要会带上 [值守 person 0.88] 前缀——翻记录时一眼就知道"为什么拍了这张"。
AI 怎么部署到板子上(三条路,一次说清)
前面讲的是"怎么用 AI",这一节专门讲"AI 怎么上板"——模型放哪、运行时从哪来、怎么编进固件、怎么快速验证、怎么换成自己的模型。
RK3576自带 NPU(6 TOPS),但要在系统里能用,需要两样东西到位,上板先自检:
adb shell ls /dev/dri/ # 期望看到 renderD129(NPU 的 DRM 渲染节点) adb shell dmesg | grep -i rknpu # 需 adb root;期望 "RKNPU driver v0.9.8" 之类日志 adb shell zcat /proc/config.gz | grep RKNPU # 期望 CONFIG_ROCKCHIP_RKNPU=y
我们这块板实测:驱动 v0.9.8 已随固件就位,/dev/dri/renderD129 权限 666。 NPU 驱动属于固件/kernel 层,App 侧只负责"带 runtime + 带模型",碰不到驱动。
这是产品发布路径,一次编译,整机自带 AI 能力:
cc_library_shared {
name: "librkdt_yolo", // 我们的 JNI(Detector.cpp + postprocess.cc)
stl: "c++_static", // 静态 libc++,App 命名空间没有动态版
shared_libs: ["librknnrt", "liblog"],
}
android_app {
name: "RkDataTerminal",
asset_dirs: ["assets"], // 模型随包
jni_libs: ["librkdt_yolo", "librknnrt"], // 两个 so 内嵌
}librknnrt 直接引用树内 hardware/rockchip/rknpu2/runtime/Android.bp 的同名模块,不用改固件。
只想快速验证、不想等整包烧录时:
# ① 只编这个 App mmm vendor/rockchip/common/apps/RkDataTerminal # ② 把两个 .so 注入 APK 的 lib/arm64-v8a/(zip 重打包) # ③ 用 AOSP apksigner + testkey 重签 # ④ 装机 adb install -r RkDataTerminal_signed.apk adb shell pm grant com.rockchip.rkdataterminal android.permission.CAMERA
为什么必须注入 .so:系统 App 的 native 库由 Soong 装在 /system/lib64,APK 里并不含; 而用 adb install 装成数据包时没有 system 权限,App 的 native 命名空间不允许从 /system/lib64 dlopen 任意库 (会报 library ... is not accessible for the namespace)。所以调试期把so内嵌进APK最省事——正式发布仍走路径 A。
adb shell ls -l /system/lib64/librknnrt.so # runtime 在位(路径 A 编出来的固件) adb shell ls /dev/dri/ # renderD129 在 adb logcat -s RkTermYolo # 首次检测时会打印: init ok model=640x640 out=3
再注入一张测试图,看是否出框(最直接的验收):
adb push bus.jpg /sdcard/Android/data/com.rockchip.rkdataterminal/files/bus.jpg adb shell am start -n com.rockchip.rkdataterminal/.MainActivity \ --es testjpg /sdcard/Android/data/com.rockchip.rkdataterminal/files/bus.jpg adb logcat -d -s RkTermMain # 期望打印 person 0.88 / bus 0.69 等 5 个框
> 转换在PC上做,板子不参与训练/转换,板子只负责推理。
(3)替换:把新的.rknn与类别清单放进assets/,覆盖原文件即可;
结果落地:水印、归档、上报、远程看
水印:半透明横幅(站点/SN/时间/8 路通道值)+ 检测框与类别置信度,直接烧进 JPEG;
归档:SQLite 一条记录 = 带框的图 + 全通道快照 JSON + detections[];
上报:multipart/form-data POSTA到服务器
(字段 site/device/time/kind/summary/data+附件图),服务器返回 2xx 才标记已上报,失败保留下轮重试;
远程看:App内嵌Web服务:8080,浏览器输板子IP就能看实时画面(MJPEG)、值守状态、记录列表。
实测数据(RK3576/Android 14)
踩坑清单(照做省半天)&如何复现
# 1) 拍照即判:把测试图放进 App 目录,注入一帧看检测结果 adb push bus.jpg /sdcard/Android/data/com.rockchip.rkdataterminal/files/bus.jpg adb logcat -c adb shell am start -n com.rockchip.rkdataterminal/.MainActivity \ --es testjpg /sdcard/Android/data/com.rockchip.rkdataterminal/files/bus.jpg adb logcat -d -s RkTermMain # 打印 5 个框 # 2) 值守触发链:注入一帧走完整"判定→自动留证" adb shell am start -n com.rockchip.rkdataterminal/.MainActivity \ --es watchtestjpg /sdcard/Android/data/com.rockchip.rkdataterminal/files/bus.jpg # 记录里会出现 [值守 person 0.88] … 的留证 # 3) 板载 Web:浏览器打开 http://<板子IP>:8080 看画面/状态/记录
结语:结合的本质是"三对齐"
回顾整条链路,"相机 + AI"能结合好,靠的是三件事对齐:
尺度对齐:相机的分辨率 ≠ 模型输入,letterbox+逆映射补齐;
时序对齐:检测时刻 = 拍照时刻,图与数据同一瞬间,证据才自证;
数据对齐:框、类别、通道快照、图,全部落在同一条记录里。
做到这三点,"拍照"就不再只是拍照,而是一次带判定的采集。
Purple Pi OH2开发板,是触觉智能RK3576核心板SOM7609系列配套的评估开发板,搭载4核A72+4核A53+M0多核异构处理器,主频2.2GHz,集成6Tops算力 NPU,轻松应对AI推理、边缘计算与轻量级AI端侧学习。配套完善SDK、丰富资料与Demo,有助于项目前期快速开发验证。
触觉智能SOM7609系列核心板尺寸仅40.5×40.5mm,专为空间受限场景打造。基于RK3576/RK3576J SoC,集成高性能GPU与VPU,支持4K视频编解码与多屏异显;提供PCIe、USB3.2、双路千兆以太网、MIPI-DSI/CSI、eDP、CAN、SPI、I2C等丰富接口,灵活扩展。支持Linux、Android、开源鸿蒙OpenHarmony等丰富系统,配套丰富软硬件资料与技术支持,助力产品快速落地。
