一、前几章内容
阶段 | 做了什么 | 关键实测 |
|---|---|---|
平台打通 | 给昉·星光2(JH7110,riscv64)挂上 Hailo-8L(13 TOPS INT8),用 HailoRT 4.19.0 + TAPPAS 3.30.0 的 GStreamer 元素拼流水线 | yolov8m 纯 NPU 6.53 FPS / 硬件延迟 162.4 ms;极轻量的 SSD MobileNetV1 达 367 FPS |
瓶颈定位 | 定量分析"慢在哪里" | PCIe Gen2 x1 带宽只用了 1.8%;真正的约束是主机侧前处理的 CPU(61.6%)——该 CPU 指令集没有 |
人脸识别 | 板端推理 → 本机对齐 → 板端比对的两段式流水线 | SCRFD 2.5g 21.50 FPS / 46.5 ms;10 张盲测命中 2 张(0.980 / 0.662),不命中的最高 0.344 |
这几篇已发布,细节不重复。
二、头像追踪
人脸识别解决"这是谁",下一个问题是"他在看哪儿"。我做了一次统一协议采集:一次录完、四个方案一起评,免得每个方案单独采一遍。
图 :四个方案的定案,以及它直接决定了最终原型里的哪些参数。
三、过程
实时原型的链路是:板端逐帧导出特征 → PC 桥接 → 浏览器里一颗球跟着头动。链路不难,难的是控制律。
图 :同一段 366 秒录制重放。旧的红线根本不单调——头几乎不动时球已在右边,头转到满幅时球只走到约 1/3 屏。
正解是把幅度和平滑拆成两个独立的量:幅度交给标定(算出目标位置),平滑交给限速。两条控制律因此共用同一个目标位置,只在"怎么走过去"上不同。
四、最终成品:切水果游戏
到这一步追踪已经"能用了",但"能用"很含糊——曲线好看不代表人用着舒服。
选切水果,是因为它把追踪质量直接变成可感知的结果:切得中就是切得中,没有中间地带。而且它一次考核了所有环节:延迟大 → 追不上;抖动大 → 空挥;幅度不够 → 够不到屏幕边;检出掉 → 贴边的水果切不到。
五:效果
六、结论
这套组合是可用的。 RISC-V + Hailo-8L 在 1280×720 下稳定输出约 25 FPS 的逐帧检测,足够驱动实时的头部追踪。
眼球控制这条路,用现成的人脸关键点模型走不通。 横向眼动信号只有头动的 1/38,不是算法问题,是 5 个关键点在物理上量不到。
头姿控制的关键是"把幅度和平滑拆开"。 凡是参考量取自被控对象自身的控制律,幅度必然天生封顶;要幅度对得上,必须有明确的目标位置。
七、主要代码
检测:
set -u
TAPPAS=/usr/share/tappas
POSTDIR=$TAPPAS/apps/h8/gstreamer/libs/post_processes
RESDIR=$TAPPAS/apps/h8/gstreamer/vf2/cascading_networks/resources
FUNC_OVERRIDE=""
SO_OVERRIDE=""
# --------- 模型组合 ---------
USE_LIGHTFACE=0
# ★ 重要:TAPPAS 各后处理库都导出一个通用入口 `filter`(C 符号)。
# 所以 function-name 一律写 filter。
scrfd_cfg() {
HEF=${SCRFD_HEF:-/home/user/scrfd_2.5g.hef}
# 系统自带的 libscrfd_post.so 硬编码层名 conv46/47/48|conv52/53/54|conv55/56/57,
# 与 Model Zoo 版 HEF(conv42/43/44|conv49/50/51|conv55/56/57)不符 → 首帧 SIGSEGV。
# 因此默认用打过层名补丁的副本(见 diag/patch_scrfd_so.py),系统原文件未改动。
if [ -f /home/user/libscrfd_post_fixed.so ]; then
SO=${SO_OVERRIDE:-/home/user/libscrfd_post_fixed.so}
else
SO=${SO_OVERRIDE:-$POSTDIR/libscrfd_post.so}
fi
FUNC=${FUNC_OVERRIDE:-scrfd_2_5g}
CFG=${SCRFD_CFG:-/home/user/scrfd.json}
MODEL_NAME="scrfd_2.5g"
}
lightface_cfg() {
HEF=$RESDIR/lightface_slim.hef
SO=${SO_OVERRIDE:-$POSTDIR/libface_detection_post.so}
FUNC=${FUNC_OVERRIDE:-filter}
CFG=${LIGHTFACE_CFG:-/home/user/lightface.json}
MODEL_NAME="lightface_slim (已废)"
}
BATCH=1
MODE=video
VIDEO=$RESDIR/face_detection.mp4
IMAGE=""
IMG_SEC=2
DEV=/dev/video4
FRAMES=300
SNAP=0
SHOW=0
PRINT_ONLY=0
EXPORT=0
# 显式翻转
# 摄像头源默认 180(本板物理倒装)。采集类文件源必须显式 --flip 180,
# 否则送进 SCRFD 的是倒置人脸 → 检出率 0% 且不报任何错(本板踩过两次)。
FLIP=""
SNAPDIR=/tmp/faceframes
JSONOUT=/tmp/face.json
LOG=/tmp/face_run.log
Q="queue leaky=no max-size-buffers=30 max-size-bytes=0 max-size-time=0"
usage() {
sed -n '/^# 用法:/,/^# =====/p' "$0" | sed 's/^# \{0,1\}//'
exit 0
}
while [ $# -gt 0 ]; do
case "$1" in
--cam) MODE=cam ;;
--image) MODE=image; shift; IMAGE="$1" ;;
--video) MODE=video; shift; VIDEO="$1" ;;
--dev) shift; DEV="$1" ;;
--frames) shift; FRAMES="$1" ;;
--snap) shift; SNAP="$1" ;;
--export) EXPORT=1 ;;
--hef) shift; SCRFD_HEF="$1" ;;
--cfg) shift; SCRFD_CFG="$1" ;;
--func) shift; FUNC_OVERRIDE="$1" ;;
--so) shift; SO_OVERRIDE="$1" ;;
--show) SHOW=1 ;;
--print) PRINT_ONLY=1 ;;
--flip) shift; FLIP="$1" ;;
--lightface) USE_LIGHTFACE=1 ;;
-h|--help) usage ;;
*) echo "未知参数:$1(用 --help 看用法)"; exit 1 ;;
esac
shift
done
if [ "$USE_LIGHTFACE" = "1" ]; then lightface_cfg; else scrfd_cfg; fi
# ---------- 翻转参数校验----------
case "${FLIP:-}" in
""|identity|90r|180|90l|horiz|vert|ul-lr|ur-ll|auto) ;;
*) echo "[错误] --flip 取值非法:$FLIP"
echo " 可用:identity|90r|180|90l|horiz|vert|ul-lr|ur-ll|auto"
exit 1 ;;
esac
# ---------- 前置检查 ----------
[ -f "$HEF" ] || { echo "[错误] 找不到模型 $HEF"; exit 1; }
[ -f "$SO" ] || { echo "[错误] 找不到后处理库 $SO"; exit 1; }
[ -f "$CFG" ] || { echo "[错误] 找不到 config $CFG"; exit 1; }
if [ "$MODE" = "image" ]; then
# 单张照片:hailonet 需要连续帧流(单帧会卡在 PREROLLING),
# 所以先用板端 ffmpeg 把它循环成一小段视频再走 --video 逻辑。
[ -f "$IMAGE" ] || { echo "[错误] 找不到图片 $IMAGE"; exit 1; }
mkdir -p /tmp/eimg
VIDEO=/tmp/eimg/one.mp4
# H.264 要求宽高均为偶数,而人像照片常带奇数边(如 443×452)→ 先裁到偶数
if ! ffmpeg -y -loglevel error -loop 1 -i "$IMAGE" -t "$IMG_SEC" -r 30 \
-vf "crop=trunc(iw/2)*2:trunc(ih/2)*2" \
-c:v libx264 -preset ultrafast -pix_fmt yuv420p "$VIDEO"; then
echo "[错误] 图片转视频失败(建议 JPG/PNG,HEIC 需先转换)"
exit 1
fi
echo " [图片] $(basename "$IMAGE") -> ${IMG_SEC}s@30fps 视频($(du -h "$VIDEO" | cut -f1))"
MODE=video
[ "$SNAP" -eq 0 ] && SNAP=1 # 单图默认至少留 1 张带框结果
SRC="filesrc location=$VIDEO name=src_0 ! decodebin"
SRC_DESC="$(basename "$IMAGE")(单张图片)"
elif [ "$MODE" = "cam" ]; then
[ -e "$DEV" ] || { echo "[错误] 摄像头节点不存在:$DEV"; exit 1; }
SRC="v4l2src device=$DEV name=src_0 ! videoflip video-direction=${FLIP:-180}"
SRC_DESC="$DEV(USB 摄像头,翻转 ${FLIP:-180})"
else
[ -f "$VIDEO" ] || { echo "[错误] 找不到视频 $VIDEO"; exit 1; }
SRC="filesrc location=$VIDEO name=src_0 ! decodebin"
if [ -n "${FLIP:-}" ]; then
# 文件源默认不翻转(保持旧行为);采集类素材是倒装的,必须显式 --flip 180
# videoconvert 兜底:decodebin 可能吐 NV12,而 videoflip 不认全部格式
SRC="$SRC ! videoconvert n-threads=2 ! videoflip video-direction=$FLIP"
SRC_DESC="$(basename "$VIDEO")(翻转 $FLIP)"
else
SRC_DESC="$(basename "$VIDEO")"
fi
fi
# ---------- 人脸检测流水线 ----------
CORE="$Q ! videoscale qos=false n-threads=2 ! video/x-raw,pixel-aspect-ratio=1/1 \
! $Q ! videoconvert n-threads=2 qos=false \
! $Q ! hailonet hef-path=$HEF batch-size=$BATCH \
! $Q ! hailofilter function-name=$FUNC so-path=$SO config-path=$CFG qos=false"
if [ "$EXPORT" = "1" ]; then
# hailoexportfile 是 BaseTransform(sink+src 都有),可直接串进链路;
# 属性名是 location,不是 file-path;它没有 num-buffers,会在 EOS 时一次性落盘。
rm -f "$JSONOUT"
CORE="$CORE ! $Q ! hailoexportfile location=$JSONOUT"
fi
CORE="$CORE ! $Q ! hailooverlay qos=false \
! $Q ! videoconvert n-threads=2 qos=false ! $Q"
if [ "$SNAP" -gt 0 ]; then
mkdir -p "$SNAPDIR"; rm -f "$SNAPDIR"/f*.jpg
# 注意:multifilesink 没有 num-buffers 属性(只有 max-files,达上限循环删旧),
# 因此这里的 N 是"最终保留 N 张",实际处理的仍是整段视频。
SINK="jpegenc quality=85 ! multifilesink location=$SNAPDIR/f%03d.jpg max-files=$SNAP"
elif [ "$SHOW" = "1" ]; then
export DISPLAY=${DISPLAY:-:10}
SINK="fpsdisplaysink video-sink=clutterautovideosink text-overlay=false"
elif [ "$FRAMES" -gt 0 ]; then
SINK="fakesink sync=false num-buffers=$FRAMES"
else
SINK="fakesink sync=false"
fi
PIPELINE="gst-launch-1.0 -e $SRC ! $CORE ! $SINK"
echo "=============================================="
echo " 人脸检测 · $MODEL_NAME · Hailo-8L"
echo "=============================================="
echo " 模型 : $(basename "$HEF") ($(du -h "$HEF" | cut -f1))"
echo " 后处理 : $(basename "$SO")"
echo " function : $FUNC"
echo " config : $(basename "$CFG")"
echo " 输入源 : $SRC_DESC"
if [ "$SNAP" -gt 0 ]; then
echo " 输出 : 保存 $SNAP 张带框帧 -> $SNAPDIR/"
elif [ "$SHOW" = "1" ]; then
echo " 输出 : 显示窗口 (DISPLAY=$DISPLAY)"
else
echo " 输出 : fakesink(计时模式)"
fi
[ "$EXPORT" = "1" ] && echo " 对象导出 : $JSONOUT"
echo "----------------------------------------------"
if [ "$PRINT_ONLY" = "1" ]; then
echo "$PIPELINE"
exit 0
fi
T0=$(date +%s%N)
# ── 安全网 ────────────────────────────────────────────────────
# gst-launch 在段错误时会"Captured SIGSEGV + Spinning"而不退出,
# 从而一直占着 /dev/hailo0 → 下次运行报 HAILO_OUT_OF_PHYSICAL_DEVICES(74)。
# 两道保险:① 开跑前清掉残留(-x 精确匹配进程名,不能用 -f,否则会连自己也杀掉);
# ② 用 timeout 包住,卡死时自动收尸。
pkill -9 -x gst-launch-1.0 2>/dev/null && { echo " [清理] 发现残留 gst-launch,已强杀"; sleep 1; }
# 超时秒数:外部显式给了 RUNSEC 就用它(摄像头等无 EOS 的源必须靠它收尾)
if [ -z "${RUNSEC:-}" ]; then
RUNSEC=$(( FRAMES / 8 + 60 ))
# --snap 模式用 multifilesink(没有 num-buffers),会把整段视频跑完,所以要放宽
if [ "$SNAP" -gt 0 ] && [ "$RUNSEC" -lt 200 ]; then RUNSEC=200; fi
fi
eval "timeout --signal=KILL -k 5 ${RUNSEC}s $PIPELINE" > "$LOG" 2>&1
RC=$?
T1=$(date +%s%N)
WALL=$(awk -v a=$T0 -v b=$T1 'BEGIN{printf "%.3f", (b-a)/1e9}')
[ "$RC" = "137" ] && { echo " [!] 超时(${RUNSEC}s)被强杀——pipeline 很可能卡死"; pkill -9 -x gst-launch-1.0 2>/dev/null; }
echo
if [ "$SNAP" -gt 0 ]; then
N=$(ls -1 "$SNAPDIR"/f*.jpg 2>/dev/null | wc -l)
echo " 已保存帧数 : $N 张 -> $SNAPDIR/"
echo " 墙钟耗时 : $WALL 秒"
[ "$N" -gt 0 ] && echo " 平均帧率 : $(awk -v n=$N -v w=$WALL 'BEGIN{printf "%.2f", n/w}') FPS(含启动开销)"
else
DUR=$(grep -o 'Execution ended after [^ ]*' "$LOG" | head -1 | awk '{print $4}')
if [ -n "${DUR:-}" ]; then
# 注意:-v 传进来的是标量,不能写 t[1];必须用 split 拆
SEC=$(awk -v t="$DUR" 'BEGIN{split(t, a, ":"); print a[1]*3600 + a[2]*60 + a[3]}')
echo " gst 自报耗时: $DUR($SEC 秒,不含启动)"
# ★ FRAMES=0 表示"跑完整段视频"(采集素材走这条),此处不能拿 0 做除数,
# 否则会打出 `awk: division by zero attempted` —— 看着像报错,实际无害。
# 帧数改由下方「导出帧数」给出。
if [ "$FRAMES" -gt 0 ]; then
echo " 实测帧数 : $FRAMES 帧"
echo " 平均帧率 : $(awk -v n=$FRAMES -v d=$SEC 'BEGIN{printf "%.2f", n/d}') FPS"
echo " 平均延迟 : $(awk -v n=$FRAMES -v d=$SEC 'BEGIN{printf "%.1f", d/n*1000}') ms/帧"
else
echo " 实测帧数 : 整段视频(--frames 0)→ 帧数与帧率见下方「导出帧数」"
fi
else
echo " 墙钟耗时 : $WALL 秒"
[ "$FRAMES" -gt 0 ] && echo " 平均帧率 : $(awk -v n=$FRAMES -v w=$WALL 'BEGIN{printf "%.2f", n/w}') FPS(含启动开销)"
fi
fi
if [ "$EXPORT" = "1" ] && [ -f "$JSONOUT" ]; then
echo " 导出 JSON : $JSONOUT ($(du -h "$JSONOUT" | cut -f1))"
python3 - "$JSONOUT" <<'PYEOF' 2>/dev/null || true
import json, sys
d = json.load(open(sys.argv[1]))
c = [len(x.get("HailoROI", {}).get("SubObjects", [])) for x in d]
print(" 导出帧数 : %d 帧" % len(c))
if c:
print(" 总检测数 : %d" % sum(c))
print(" 每帧检测 : 最小 %d / 最大 %d / 平均 %.2f" % (min(c), max(c), sum(c) / len(c)))
cf = [s["HailoDetection"]["confidence"]
for x in d[:200] for s in x.get("HailoROI", {}).get("SubObjects", [])
if "HailoDetection" in s]
if cf:
cf.sort()
print(" 置信度 : 最小 %.3f / 中位 %.3f / 最大 %.3f (唯一值 %d 个)"
% (cf[0], cf[len(cf)//2], cf[-1], len(set(round(v,3) for v in cf))))
PYEOF
fi
echo "----------------------------------------------"
ERRPAT='error|failed|segmentation|undefined symbol|caught sigsegv|spinning'
ERRS=$(grep -ciE "$ERRPAT" "$LOG" 2>/dev/null)
ERRS=${ERRS:-0}
if [ "$ERRS" -gt 0 ]; then
echo " [警告] 日志里有 $ERRS 行疑似错误,前 5 行:"
grep -iE "$ERRPAT" "$LOG" | head -5 | sed 's/^/ /'
echo " 完整日志:$LOG"
exit $RC
fi
echo " [OK] 运行期间没有 ERROR / 段错误"
echo " 完整日志:$LOG"
exit $RC
特征提取
set -u
TAPPAS=/usr/share/tappas
POSTDIR=$TAPPAS/apps/h8/gstreamer/libs/post_processes
HEF_REC=${HEF_REC:-/home/user/arcface_mobilefacenet.hef}
SO_REC=${SO_REC:-$POSTDIR/libface_recognition_post.so}
FUNC_REC=${FUNC_REC:-arcface_nv12}
CROPDIR=/tmp/aln
VIDEO=""
MODE=none
SNAP=0
PRINT_ONLY=0
FRAMES=0
PER_SEC=${PER_SEC:-1}
JSONOUT=/tmp/embed.json
SNAPDIR=/tmp/embed_frames
LOG=/tmp/embed_run.log
Q="queue leaky=no max-size-buffers=3 max-size-bytes=0 max-size-time=0"
usage() { sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit 0; }
while [ $# -gt 0 ]; do
case "$1" in
--video) MODE=video; shift; VIDEO="$1" ;;
--crops) MODE=crops; shift; CROPDIR="$1" ;;
--snap) shift; SNAP="$1" ;;
--per-sec) shift; PER_SEC="$1" ;;
--print) PRINT_ONLY=1 ;;
-h|--help) usage ;;
*) echo "未知参数:$1"; exit 1 ;;
esac
shift
done
for f in "$HEF_REC" "$SO_REC"; do
[ -f "$f" ] || { echo "[错误] 缺少文件:$f"; exit 1; }
done
HERE=$(cd "$(dirname "$0")" && pwd)
if [ "$MODE" = "crops" ]; then
N=$(ls "$CROPDIR"/*.png "$CROPDIR"/*.jpg 2>/dev/null | wc -l)
[ "$N" -gt 0 ] || { echo "[错误] $CROPDIR 下没有图片"; exit 1; }
# 每张图占 PER_SEC 秒(默认 1s=30 帧)。用 112 letterbox,避免拉伸变形。
bash "$HERE/e-face-enroll-build.sh" "$CROPDIR" /tmp/embed_src.mp4 112 "$PER_SEC" || exit 1
VIDEO=/tmp/embed_src.mp4
FRAMES=$((N * 30 * PER_SEC))
SRC_DESC="$N 张对齐小脸 -> 视频"
elif [ "$MODE" = "video" ]; then
[ -f "$VIDEO" ] || { echo "[错误] 找不到视频 $VIDEO"; exit 1; }
SRC_DESC="$(basename "$VIDEO")"
else
echo "需要 --video 或 --crops(用 --help 看用法)"
exit 1
fi
[ "$FRAMES" -eq 0 ] && FRAMES=300
CORE="videoscale qos=false ! $Q ! \
hailonet hef-path=$HEF_REC batch-size=1 vdevice-key=1 ! $Q ! \
hailofilter name=recognition so-path=$SO_REC function-name=$FUNC_REC qos=false ! $Q ! \
hailoexportfile location=$JSONOUT ! $Q ! videoconvert n-threads=2 qos=false ! $Q"
if [ "$SNAP" -gt 0 ]; then
mkdir -p "$SNAPDIR"; rm -f "$SNAPDIR"/f*.jpg
SINK="jpegenc quality=90 ! multifilesink location=$SNAPDIR/f%03d.jpg max-files=$SNAP"
else
SINK="fakesink sync=false"
fi
PIPELINE="gst-launch-1.0 -e filesrc location=$VIDEO name=src_0 ! decodebin ! videoconvert qos=false ! $CORE ! $SINK"
echo "=============================================="
echo " 只跑 ArcFace 提特征 · Hailo-8L"
echo "=============================================="
echo " 模型 : $(basename "$HEF_REC") / function=$FUNC_REC"
echo " 输入 : $SRC_DESC"
echo " 输出 : $JSONOUT"
echo "----------------------------------------------"
if [ "$PRINT_ONLY" = "1" ]; then echo "$PIPELINE"; exit 0; fi
pkill -9 -x gst-launch-1.0 2>/dev/null; sleep 1
rm -f "$JSONOUT"
RUNSEC=${RUNSEC:-$(( FRAMES / 10 + 40 ))}
T0=$(date +%s%N)
timeout --signal=KILL -k 5 "$RUNSEC" bash -c "$PIPELINE" > "$LOG" 2>&1
RC=$?
T1=$(date +%s%N)
WALL=$(awk -v a=$T0 -v b=$T1 'BEGIN{printf "%.3f", (b-a)/1e9}')
if grep -q 'Caught SIGSEGV' "$LOG" 2>/dev/null; then
echo " [失败] 段错误,日志:$LOG"; grep -iE 'error|symbol' "$LOG" | head -5; exit 1
fi
if [ "$RC" = "137" ]; then
echo " [警告] 达到 ${RUNSEC}s 上限被强杀(未收到 EOS)"
fi
echo " 墙钟耗时 : $WALL 秒"
if [ -f "$JSONOUT" ]; then
echo " 导出 JSON : $JSONOUT ($(du -h "$JSONOUT" | cut -f1))"
python3 - <<PY
import json
try:
d = json.load(open("$JSONOUT"))
n = sum(1 for fr in d if "HailoMatrix" in json.dumps(fr)[:100000] or "HailoMatrix" in str(fr)[:100000])
print(" 帧数 : %d" % len(d))
except Exception as e:
print(" JSON 解析失败: %s" % e)
PY
fi
if [ "$SNAP" -gt 0 ]; then
echo " 输入帧 : $(ls "$SNAPDIR"/*.jpg 2>/dev/null | wc -l) 张 -> $SNAPDIR/"
fi
grep -E 'Got EOS|Execution ended' "$LOG" | sed 's/^/ /'
exit $RC
头部追踪
set -u
TAPPAS=/usr/share/tappas
POSTDIR=$TAPPAS/apps/h8/gstreamer/libs/post_processes
HEF=${SCRFD_HEF:-/home/user/scrfd_2.5g.hef}
if [ -f /home/user/libscrfd_post_fixed.so ]; then
SO=${SCRFD_SO:-/home/user/libscrfd_post_fixed.so}
else
SO=${SCRFD_SO:-$POSTDIR/libscrfd_post.so}
fi
FUNC=scrfd_2_5g
CFG=${SCRFD_CFG:-/home/user/scrfd.json}
DEV=${DEV:-/dev/video4}
ZMQADDR=${GAZE_LIVE_ZMQ:-tcp://\*:9501}
MODE=live
VIDEO=""
SEC=1800
N=150
FLIP=180
while [ $# -gt 0 ]; do
case "$1" in
--sec) shift; SEC="$1" ;;
--flip) shift; FLIP="$1" ;;
--file) shift; MODE=file; VIDEO="$1" ;;
--n) shift; N="$1" ;;
--dev) shift; DEV="$1" ;;
--zmq) shift; ZMQADDR="$1" ;;
-h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "未知参数:$1"; exit 1 ;;
esac
shift
done
[ -f "$HEF" ] || { echo "[错误] 找不到模型 $HEF"; exit 1; }
flip_seg() {
case "$1" in
none|identity) echo "" ;;
horiz) echo "videoflip video-direction=horiz ! " ;;
vert) echo "videoflip video-direction=vert ! " ;;
180) echo "videoflip video-direction=180 ! " ;;
*) echo "[错误] 未知 flip:$1(可用 none|horiz|vert|180)" >&2; exit 1 ;;
esac
}
# 共同主干(hailofilter 之后各模式尾部不同)
CORE="videoscale qos=false n-threads=2 ! video/x-raw,pixel-aspect-ratio=1/1 ! \
queue leaky=downstream max-size-buffers=2 max-size-bytes=0 max-size-time=0 ! \
videoconvert n-threads=2 qos=false ! \
queue leaky=downstream max-size-buffers=2 max-size-bytes=0 max-size-time=0 ! \
hailonet hef-path=$HEF batch-size=1 ! \
queue leaky=downstream max-size-buffers=2 max-size-bytes=0 max-size-time=0 ! \
hailofilter function-name=$FUNC so-path=$SO config-path=$CFG qos=false ! \
hailoexportzmq address=$ZMQADDR qos=false"
echo "===== 实时特征导出 · scrfd_2.5g · Hailo-8L ====="
echo " ZMQ 地址 : $ZMQADDR(逐帧 HailoObjects JSON)"
pkill -9 -x gst-launch-1.0 2>/dev/null && { echo " [清理] 强杀残留 gst"; sleep 1; }
if [ "$MODE" = "file" ]; then
[ -f "$VIDEO" ] || { echo "[错误] 找不到视频 $VIDEO"; exit 1; }
echo " 模式 : 文件源 $VIDEO($N 帧)"
PIPELINE="gst-launch-1.0 -e filesrc location=$VIDEO ! decodebin ! $CORE ! \
fakesink sync=false num-buffers=$N"
eval "timeout --signal=KILL -k 5 $((N / 8 + 120))s $PIPELINE" > /tmp/gaze_live.log 2>&1
RC=$?
else
[ -e "$DEV" ] || { echo "[错误] 摄像头不存在: $DEV"; exit 1; }
FL=$(flip_seg "$FLIP")
echo " 模式 : 实时 $DEV(翻转 ${FLIP}),运行 ${SEC}s"
PIPELINE="gst-launch-1.0 -e v4l2src device=$DEV ! ${FL}\
queue leaky=downstream max-size-buffers=2 max-size-bytes=0 max-size-time=0 ! $CORE ! \
fakesink sync=false"
eval "timeout --signal=KILL -k 5 ${SEC}s $PIPELINE" > /tmp/gaze_live.log 2>&1
RC=$?
fi
echo " gst 退出码: $RC"
[ "$RC" = "137" ] && echo " [i] 到达时长上限被强杀(正常收尾方式)"
echo "===== 错误扫描 ====="
ERR=$(grep -ciE 'error|failed|segmentation|undefined symbol|caught sigsegv|spinning' /tmp/gaze_live.log 2>/dev/null)
ERR=${ERR:-0}
if [ "$ERR" -gt 0 ]; then
grep -iE 'error|failed|segmentation|undefined symbol|caught sigsegv|spinning' /tmp/gaze_live.log | head -6 | sed 's/^/ /'
exit 1
fi
echo " 无 ERROR / 段错误(完整日志 /tmp/gaze_live.log)"
echo "DONE"
