手势点灯做到第四个任务,前三个任务里那些能让人兴奋的东西基本都磨完了:灯能亮、能调亮度、按键能切模式。到这一步,项目真正的门槛才露出来——手势识别数字,而且限定条件是"基于手指弯曲程度"。这跟拿手掌框个矩形做二分类完全不是一回事。你要判断的是五根手指各自弯到了什么程度,再把五个独立的状态合成一个数字,最后让这个数字驱动灯。中间任何一根手指判错,输出就是另一个数。
我把这套东西从零跑通、又在几个人手上反复试过之后,最大的感受是:识别手不难,难的是让识别结果稳定到可以用作控制信号。摄像头抖一帧、手指弯到一半、手掌侧转三十度,角度值就飘了。所以这篇不打算只讲"怎么算出弯曲角度",而是把一个能真正接到灯上的完整链路拆开讲:几何怎么算、阈值怎么定、抖动怎么压、硬件方案怎么做对照。
适合谁看:做过一点图像处理、想把这套东西接到真实设备上的朋友;或者用柔性弯曲传感器做可穿戴交互、卡在标定和滤波上的朋友。代码用 Python 写,逻辑是通用的,换成别的框架也能照着搬。
1. 手势点灯任务四的整体设计与技术路线取舍
1.1 任务四在整个系列里到底解决了什么问题
前三个任务本质上是"输入通道"的替换:物理按键换成触摸、触摸换成旋钮、旋钮换成遥控。输入信号的语义都很简单——开、关、加、减。到了任务四,输入的语义从一维变成了多维:你要从一只手的状态里读出一个 0 到 5 的离散数,并且这个数必须能被后续的灯控逻辑当成"档位"或"数量"来用。
这一步的意义在于,它把项目从"开关控制"推进到了"参数控制"。灯不再只有亮和灭,而是有了 6 个明确的档位。同时它也引入了一个在真实产品里非常常见的问题:人机交互中的意图确认。手在画面里晃一下不代表用户想改档位,只有稳定地比出某个手势才算数。所以任务四的技术重心其实有两块,一块是几何量算得准,一块是状态机判得稳,后者往往比前者更影响体验。
我在设计时把整条链路拆成了五个独立环节:关键点提取、角度计算与归一化、单指状态二值化、五指状态合成数字、数字稳定与灯控映射。每个环节都能单独测试、单独替换,出问题时能快速定位是哪一层的问题,而不是对着一个端到端函数瞎猜。这个拆分习惯是我踩过坑之后养成的,早期把所有逻辑塞在一个循环里,改一个阈值要重跑一遍视频,效率极低。
1.2 视觉关键点与柔性弯曲传感器:两条路线的实际取舍
做"手指弯曲程度"这件事,行业里主流就两条路。一条是用摄像头加手部关键点检测,从坐标反推关节角度;另一条是直接把柔性弯曲传感器贴在手指背面,测电阻变化。这两条路的成本结构、精度特性、使用场景完全不同,选错了后面全是返工。
| 对比维度 | 视觉关键点方案 | 柔性弯曲传感器方案 |
|---|---|---|
| 硬件成本 | 一个普通摄像头即可,几乎零增量成本 | 每根手指一个传感器,加 ADC 与走线,成本随手指数线性上升 |
| 精度特性 | 绝对角度有偏差,但相对趋势稳定 | 单指角度分辨率高,但个体差异大、需要逐人标定 |
| 佩戴负担 | 无 | 需要手套或绑带,长期佩戴不适 |
| 环境依赖 | 强依赖光照与背景,逆光和杂乱背景会掉点 | 几乎不受光照影响 |
| 可扩展性 | 换模型就能识别更多手势 | 想加指节就得加传感器 |
| 调试难度 | 可视化好,关键点画出来一眼能看出问题 | 只能看数值曲线,靠经验判断 |
我的选择是以视觉方案为主线,因为它的调试反馈最直观,改一行代码就能看到关键点位置的变化,学习成本低。硬件方案我在第 5 节单独讲,它更适合做可穿戴原型或者对隐私敏感、不适合开摄像头的场景。这里要明确一点:视觉方案算出来的角度是"看起来的弯曲程度",不是你手指真实的关节角,手掌正对镜头和侧对镜头时同一个手势算出来的角度能差二十度。这个偏差不能消除,只能靠归一化和阈值区间去容纳,后面的章节会具体处理。
1.3 从画面到灯光的完整数据流与状态机
整条链路我按帧驱动,每一帧走一遍,但灯的更新不是每帧都做,而是由状态机控制。数据流是这样的:摄像头取帧,交给关键点检测拿到 21 个三维坐标,接着做尺度归一化,算出五根手指各自的弯曲度,二值化成五个布尔值,合成一个 0 到 5 的数字,送进稳定器,只有稳定器确认后才更新灯光。
状态机我设计了三个状态:无手、候选、锁定。没有检测到手时进入无手状态,此时灯光保持上一次的值,不归零,这一点很关键——用户不可能一直把手举在镜头前,手放下灯就灭会非常恼人。检测到手但数字还没稳定时进入候选状态,灯光不动。连续若干帧数字一致才进入锁定状态,此时才真正下发控制指令。
这里有一个容易忽略的细节:离开超时时间。我最初设的是 0.5 秒,结果用户比完手势稍微停顿一下灯就掉回默认值。后来改成 1.2 秒,并且区分"检测不到手"和"检测到手但数字无效"两种情况,前者才计入超时,后者只是维持当前锁定值。这个改动让实际体验好了非常多,也说明这类项目里参数不是拍脑袋定的,得在真人身上反复试。
2. 把"弯曲程度"变成可计算的数
2.1 21 个关键点的结构与你真正需要的那几个
手部关键点检测的输出是一个固定结构:21 个点,每个点带 x、y、z 三个分量。x、y 是归一化到图像宽高的比例值,z 是相对深度,量纲和 x 差不多,但不是真实物理距离。理解这一点很重要,因为很多人第一次拿到 z 就以为是毫米,结果算出来的角度完全不对。
这 21 个点的编号是固定的,从手腕开始,依次是拇指的四个点、食指的四个点、中指、无名指、小指。每一根手指的四个点分别是掌指关节、近端指间关节、远端指间关节和指尖。实际做弯曲判定,用不到全部 21 个点,每根手指三个点就够了,但手腕点和小指的掌指关节这两个"锚点"必须留着,它们是做尺度归一化和掌平面计算的基准。
| 部位 | 关键点索引 | 用途 |
|---|---|---|
| 手腕 | 0 | 尺度基准、掌平面定义 |
| 拇指 | 1、2、3、4 | 单独处理,用 2-3-4 或距离比 |
| 食指 | 5、6、7、8 | 6 为顶点算夹角 |
| 中指 | 9、10、11、12 | 10 为顶点算夹角 |
| 无名指 | 13、14、15、16 | 14 为顶点算夹角 |
| 小指 | 17、18、19、20 | 18 为顶点算夹角,17 参与掌宽计算 |
我在代码里把这份映射写成常量字典,而不是到处硬编码索引。原因是后来做可配置化时,需要屏蔽某根手指(比如用户小指受伤不方便),有字典就能只改配置不改逻辑。这种小设计在原型阶段看不出价值,一旦要维护就省很多事。
2.2 用向量夹角量出真实的弯曲角
弯曲程度的数学表达,最直接的就是关节夹角。以食指为例,取掌指关节、近端指间关节、指尖三个点,以近端指间关节为顶点,算两条边之间的夹角。手指完全伸直时这个角接近 180 度,握拳时接近 40 到 60 度。
计算用向量点积:两条边的向量做点积,除以两个向量的模长乘积,得到余弦值,再取反余弦转成角度。写代码时必须做两件事,一是向量模长接近零时要提前返回,否则会除零;二是余弦值要钳制在 -1 到 1 之间,因为浮点误差可能让它变成 1.0000001,反余弦直接抛异常。这两个细节我在不同项目里都栽过,属于必写不可的防御性代码。
import math def vector(a, b): return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) def norm(v): return math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) def joint_angle(a, b, c): """以 b 为顶点,计算 a-b-c 的夹角,返回角度制""" v1 = vector(a, b) v2 = vector(c, b) n1, n2 = norm(v1), norm(v2) if n1 < 1e-6 or n2 < 1e-6: return 0.0 cos_val = (v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2]) / (n1 * n2) cos_val = max(-1.0, min(1.0, cos_val)) return math.degrees(math.acos(cos_val))这里有个取舍要说清楚:顶点选近端指间关节,是因为这个关节的活动范围最大、对弯曲最敏感。如果选远端指间关节,手指半弯时角度变化不明显,阈值很难分。实测下来用近端指间关节做顶点,伸直和弯曲两个状态的角度差通常在 60 度以上,判别余量充足。
2.3 尺度归一化与掌平面投影:解决手掌朝向带来的漂移
直接用原始坐标算角度,会遇到一个很烦的问题:手离镜头远近不同,关键点的绝对距离变化很大,虽然角度本身是尺度无关的,但后续要用到的距离类特征会失真。所以我做了两步处理。
第一步是尺度归一化,用掌宽作为基准。掌宽定义为食指掌指关节到小指掌指关节的欧氏距离,这个值在同一个人、同一姿态下相对稳定,可以作为"这个人此刻手有多大"的度量。所有距离类特征都除以掌宽,得到无量纲的比值,这样不同手型、不同距离下的阈值就能共用一套。
第二步是掌平面投影。手掌侧转时,三维空间里的弯曲角会被压缩,看起来像"弯了一点"。解决办法是用手腕、食指掌指关节、小指掌指关节三点定义一个平面,求出法向量,再把各点投影到这个平面上,用二维坐标重算角度。这样手掌在空间里怎么转,算出来的都是"手掌自身坐标系下"的弯曲角。
注意:掌平面法向量要用叉积算,两个基向量的顺序不能反,反了法向量方向就翻转,投影结果会镜像。另外当手掌几乎与镜头平行、三点接近共线时,法向量会退化,此时应该回退到三维角度,而不是硬算投影。我加了一个判断:三点构成的三角形面积小于掌宽的平方的百分之几,就直接跳过投影。
这套处理做完,同一个人做同一个手势,在不同朝向下的角度波动能从二十多度压到七八度以内,阈值就能定得比较死了。
2.4 拇指为什么必须单独处理
四根手指用同一套逻辑很顺,拇指不行。原因在于拇指的关节结构和运动方式跟其他手指不同,它既有弯曲也有外展,做"比一"的手势时拇指是收在掌心的,做"比五"时是张开的,但这两种情况拇指的弯曲角可能差不多,因为它主要是在做内收和外展的旋转动作。
我用的是混合判据:先算拇指的关节夹角作为辅助,主判据用距离比——拇指尖到小指掌指关节的距离,除以掌宽。拇指张开时这个比值大,收拢时明显变小。实测两个状态在比值上的差距通常有 0.4 以上,比单纯看角度稳得多。
def thumb_open(lm, palm_width): tip = lm[4] anchor = lm[17] d = norm(vector(tip, anchor)) ratio = d / max(palm_width, 1e-6) return ratio这个比值还需要配合一个角度条件才能算稳:如果拇指尖靠近食指掌指关节,即使距离比不小,也可能是"捏"的动作。所以最终判据是比值大于上阈值、且夹角大于某个下限,两者同时满足才算张开。这种多条件与逻辑在小样本上可能显得啰嗦,但对降低误判确实有效。
3. 从角度到数字:判定、编码与灯控实现
3.1 单指状态的二值化与滞回阈值的必要性
有了角度,下一步是把它变成"伸直"和"弯曲"两个状态。最直接的做法是定一个阈值,比如弯曲角小于 90 度算弯曲,大于算伸直。问题在于手停在中间位置时,角度会在阈值附近反复跳动,输出就会在 0 和 1 之间抖,灯跟着闪。
解决办法是滞回判定,也叫双阈值。设两个阈值,上阈值比如 105 度,下阈值比如 50 度。当前状态是弯曲时,只有角度涨过上阈值才切到伸直;当前状态是伸直时,只有角度跌到下阈值才切到弯曲。中间那段"死区"不做状态切换,这样在边界附近的小幅抖动会被完全吸收。
class FingerState: def __init__(self, low=50.0, high=105.0): self.low = low self.high = high self.straight = False def update(self, angle_deg): if self.straight and angle_deg < self.low: self.straight = False elif not self.straight and angle_deg > self.high: self.straight = True return self.straight这两个数值不是随便定的。我的方法是录一段包含"从握拳慢慢张开"的素材,把每根手指的角度曲线打出来,找到直方图上的双峰位置,两个峰之间的谷底就是死区中心,然后上下各留十五度左右的余量。这个方法在任何二值化场景都通用——先看数据分布,再定阈值,而不是先定阈值再看效果。
3.2 五指状态合成数字:编码方式与歧义处理
五根手指各有一个布尔状态,合成一个 0 到 5 的数字,最稳的编码是按位。给拇指、食指、中指、无名指、小指分别分配 1、2、4、8、16 的权重,一个手势就是一个 5 位的二进制数,然后建一张映射表把它翻译成数字。
| 数字 | 拇指 | 食指 | 中指 | 无名指 | 小指 | 位编码 |
|---|---|---|---|---|---|---|
| 0 | 弯 | 弯 | 弯 | 弯 | 弯 | 0 |
| 1 | 弯 | 直 | 弯 | 弯 | 弯 | 2 |
| 2 | 弯 | 直 | 直 | 弯 | 弯 | 6 |
| 3 | 弯 | 直 | 直 | 直 | 弯 | 14 |
| 4 | 弯 | 直 | 直 | 直 | 直 | 30 |
| 5 | 直 | 直 | 直 | 直 | 直 | 31 |
用位编码而不是"数伸直的手指数",是因为位编码能唯一还原是哪几根手指伸着,便于后续做更多手势扩展。如果只是数个数,那"拇指+中指伸出"和"食指+中指伸出"都会被算成 2,语义就丢了。位编码的代价是必须穷举有效组合,表里没登记的编码要当作无效手势丢弃,不能硬转成最近的数字,否则用户一个奇怪的手势就可能点亮一个错误的档位。
这里有个必须提前约定的事:数字 3 的表达方式不唯一。有人用拇指加食指加中指,有人用食指、中指、无名指。我不去争论哪个"标准",只要求项目内部固定一种并写在说明里。原型阶段我建议只用 0、1、2、5 这四个最不容易混淆的手势,等稳定了再放开 3 和 4,因为从 3 开始涉及无名指和小指的独立控制,这两根手指的独立性在很多人的手上并不好。
3.3 数字到灯效的映射:档位、数量与颜色
识别出数字之后,怎么驱动灯,取决于你想做哪种演示。我做过三种映射,各有适用场景。
第一种是数量映射,识别到数字 n 就点亮 n 个灯珠,0 全灭。这种最直观,一数就懂,适合做展示。缺点是需要多颗独立可控的灯珠,接线稍微复杂。
第二种是亮度档位映射,把 0 到 5 线性映射到 PWM 占空比 0 到满值。这种只需要一路 PWM,硬件最简单,但视觉差异不如数量映射明显,因为人眼对亮度是非线性感知的。
def number_to_duty(n, full=255): if n <= 0: return 0 return int(round(full * n / 5))第三种是颜色映射,五个数字对应五个色相,用可变色灯珠实现。这种视觉冲击最强,但需要用户记住颜色和数字的对应关系,认知负担大。
我做的时候加了软启动:不要让灯瞬间从 0 跳到满值,而是用一两百毫秒做渐变。人眼对这种突变很敏感,会显得廉价;渐变之后整个交互的质感完全不一样。这个细节跟识别算法无关,但对成品的观感影响极大,属于投入产出比很高的改动。
3.4 完整可运行的主循环
把前面的模块串起来,主循环的结构就很清晰了。取帧、检测、算角度、更新状态、算数字、喂稳定器、更新灯光。每一步之间做好异常处理,尤其是检测不到手的分支必须显式处理,不能让它走进后面的几何计算里。
def process_frame(frame, detector, states, stabilizer): result = detector.detect(frame) if result is None: stabilizer.tick_missing() return stabilizer.current, None lm = result.landmarks palm_width = norm(vector(lm[5], lm[17])) straight = {} straight["thumb"] = judge_thumb(lm, palm_width) for name, (mcp, pip, tip) in FINGER_TRIPLETS.items(): ang = joint_angle(lm[mcp], lm[pip], lm[tip]) straight[name] = states[name].update(ang) code = 0 for name, weight in WEIGHTS.items(): if straight[name]: code |= weight number = CODE_TABLE.get(code, None) if number is None: stabilizer.tick_invalid() else: stabilizer.push(number) return stabilizer.current, straightCODE_TABLE用字典而不是数组,是为了让无效编码返回 None,语义清晰。实际跑的时候还要注意,每一帧都新建角度对象是浪费,状态对象要在循环外初始化并复用,否则滞回逻辑就失效了——每次都是初始状态,等于没有滞回。这个错误我见过不止一次,形式上代码有滞回,实际上每次都是新建的。
4. 稳定性:防抖策略、个体标定与问题排查
4.1 三层防抖的设计与参数取值
识别结果要能当控制信号用,防抖是绕不开的。我做了三层,从快到慢,各自解决不同频率的抖动。
第一层是帧内滤波,对关键点坐标做指数移动平均,系数取 0.5 到 0.6。这一层压的是检测器本身的高频噪声,单帧的跳变。系数不能太小,否则手快速移动时会有拖影,手指看起来像被黏住了。
第二层是滞回,就是前面讲的单指双阈值,压的是角度在阈值附近的中频抖动。
第三层是时间窗投票,维护一个长度为 7 的队列,只有队列里出现次数最多的数字超过 3 票,才把它确认为当前输出。这一层压的是偶发的错误识别,比如某一帧无名指被误判成伸直。
from collections import deque, Counter class Stabilizer: def __init__(self, window=7, votes=3, hold_seconds=1.2, fps=30): self.buf = deque(maxlen=window) self.votes = votes self.current = None self.missing_frames = 0 self.hold_frames = int(hold_seconds * fps) def push(self, number): self.missing_frames = 0 self.buf.append(number) if len(self.buf) < self.votes: return number, count = Counter(self.buf).most_common(1)[0] if count >= self.votes: self.current = number def tick_missing(self): self.missing_frames += 1 if self.missing_frames > self.hold_frames: self.buf.clear() self.current = None参数的选择有逻辑:窗口 7 帧,在 30 帧每秒下大约 230 毫秒,人的动作切换通常在 300 毫秒以上,所以不会误伤真实动作;票数 3 相当于要求队列里接近一半的帧一致,能过滤掉单帧跳变但不会让响应变得迟钝。如果现场光照较差、识别噪声大,可以把窗口加到 9 到 11,但要注意响应延迟会同步增加,超过 400 毫秒用户就会觉得"卡"。
4.2 一次标定解决手型差异
不同人的手指长度、关节柔韧性差别很大,同一套固定阈值在 A 同学手上很准,在 B 同学手上可能"4"永远识别不出来。解决办法是做一次轻量的个人标定:程序启动后提示用户依次做三个动作——五指张开、握拳、只伸食指——采集若干帧,记录每根手指在这三种状态下角度的均值和标准差,然后用均值加减若干倍标准差生成个人阈值。
标定还有个隐性收益:它能顺带确定用户的左右手。检测器通常会给出左右手判定,但在镜像画面里这个判定是反的。镜像不镜像直接决定了拇指在图像左边还是右边,进而影响拇指判据里的锚点选择,搞反了拇指永远判错。我习惯在标定阶段让用户做一个拇指明确的动作,用几何位置反推手性,而不是完全信任检测器的标签。
提示:标定数据不要太少,每个动作至少采 20 帧,并且要覆盖用户自然的动作速度。太快会导致采样期间姿态还没到位,均值被污染。
4.3 常见问题速查表
这套东西跑起来之后,我遇到过的问题基本集中在下面几类。整理成表,方便对着现象找原因。
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 数字频繁跳变 | 阈值无滞回,或滞回区间太窄 | 拉开双阈值间距,加大时间窗 |
| 某根手指永远判错 | 关键点索引写错,或该指被遮挡 | 把关键点画在画面上逐个核对 |
| 拇指张开识别不到 | 手性判反,或只用了角度判据 | 检查镜像设置,改用距离比 |
| 手掌侧转就识别错 | 未做掌平面投影 | 用三锚点求法向量后投影 |
| 手靠近镜头就乱 | 关键点抖动放大 | 用掌宽归一化并限制交互距离 |
| 换个人就失灵 | 阈值是原作者的个人值 | 做一次三点标定 |
| 灯响应迟滞 | 时间窗过长或帧率过低 | 降分辨率提帧率,缩短窗口 |
| 手放下灯立刻灭 | 离开超时时间太短 | 把保持时间提到 1 秒以上 |
还有一类问题不属于算法,属于交互设计。比如用户比完手势习惯性把手放下,如果系统在无手状态下清零,体验就很差。这类问题只有在真人身上试才会暴露,光看代码是看不出来的。我现在的习惯是每做完一个功能,找两三个没参与开发的人来用五分钟,往往能发现一堆自己想不到的问题。
5. 硬件路线:用柔性弯曲传感器做同一件事
5.1 分压电路与量程设计
柔性弯曲传感器本质上是一个随弯曲变形的可变电阻,伸直时阻值小,弯到九十度时阻值可能翻两三倍。要把它接到单片机的模拟输入上,最常用的是分压电路:传感器和一个固定电阻串联,取固定电阻上的电压送进 ADC。
计算固定电阻取值时,先要拿到传感器的两个典型阻值,伸直记为 R_min,最大弯曲记为 R_max。理想情况下固定电阻取两者的几何平均,也就是 R_min 和 R_max 乘积开平方,这样在整个变化范围内输出电压都落在量程中间,不会一头饱和一头分辨率不足。假设某型号传感器伸直约 25 千欧、弯曲约 75 千欧,几何平均约 43 千欧,实际取标称值 47 千欧就行,差一点影响不大。
| 参数 | 典型值 | 说明 |
|---|---|---|
| 伸直阻值 | 20 到 30 千欧 | 不同批次差异明显,必须实测 |
| 弯曲阻值 | 60 到 100 千欧 | 与弯曲角度非线性相关 |
| 固定电阻 | 47 千欧 | 取几何平均附近的标称值 |
| 采样位宽 | 10 或 12 位 | 12 位分辨率更充裕 |
接线时要注意传感器是两端器件,没有极性,但走线要顺着手指方向固定,避免引线被拉扯产生额外形变,那会被误当成弯曲信号。我在第一版里就把线随便一绑,结果每次手指一动,读数先跳一下,排查了半天才发现是线的问题。
5.2 校准、滤波与角度映射
硬件路线的原始读数是 ADC 值,直接用来判状态会很难看,必须先做两步处理。
第一步是归一化。让用户做两次极值动作,完全伸直读一次,最大弯曲读一次,得到 raw_min 和 raw_max,之后所有读数映射到 0 到 1 之间,0 表示完全伸直,1 表示最大弯曲。这个映射必须每个用户单独做,因为不同人手指粗细不同,贴合同一款传感器产生的形变量差异很大。
第二步是滤波。我用的是指数移动平均,系数 0.15 到 0.25 之间,兼顾平滑与响应。
const int PIN_FLEX = A0; float ema = -1; void setup() { Serial.begin(115200); } void loop() { int raw = analogRead(PIN_FLEX); if (ema < 0) ema = raw; ema = 0.2 * raw + 0.8 * ema; float norm = (ema - RAW_MIN) / (float)(RAW_MAX - RAW_MIN); norm = constrain(norm, 0.0, 1.0); bool bent = norm > 0.55; Serial.print(raw); Serial.print("\t"); Serial.print(norm); Serial.print("\t"); Serial.println(bent); delay(20); }这段代码里RAW_MIN和RAW_MAX是要根据标定结果替换的常量。判断阈值 0.55 也不是固定值,它是"伸直态和弯曲态的分界点在中点的偏侧",一般取 0.5 上下,具体要看两种状态的实际分布有没有重叠。
5.3 两条路线的实际使用感受
从工程角度讲,硬件路线的优势是确定性。弯曲多少就是多少,不受光照、背景、衣服颜色影响,在强光车间或者户外都稳。它的劣势是佩戴成本和标定成本,五根传感器做一副手套,走线、固定、清洗都很麻烦,而且换个人就得重新标定一遍。
视觉路线反过来,零穿戴、上手就能用,但环境一变就掉链子,逆光、暗光、背景杂乱都会让关键点检测失败。我在实际项目里的做法是视觉做主力、传感器做兜底:白天用摄像头,环境变差时切到手套。这个混合方案成本高,但对于要长期使用的场景是值得的。
还有一点值得提:传感器方案能做到视觉方案做不到的事,比如识别微小的力度变化。手指用力的程度会导致贴片形变不同,虽然精度不高,但用来做个"轻握/紧握"的二级判断是够的。这类信息在视觉里几乎拿不到,算是硬件路线的独有优势。
注意:柔性传感器有疲劳寿命,反复大角度弯折几千次之后,伸直状态的阻值会发生漂移。长期使用的设备建议每次开机重新采一次伸直基准,不要写死常量。
6. 这套结构还能往哪些方向延伸
跑通数字识别之后,我发现真正有价值的部分不是"能数出几根手指",而是那套几何量算加状态稳定的框架。把它换个映射表,就能做很多别的事。
比如把五根手指的角度当成五路连续信号,不去二值化,直接映射成五路 PWM,就是一只五通道的调光手,握拳变暗、张开变亮,中间过程全连续。这种玩法比离散数字的交互细腻得多,也更适合做氛围灯。代价是用户没法精确控制某个值,只能靠视觉反馈慢慢调,所以必须配合足够的响应速度。
再比如把角度序列当成时间特征,做简单的动作识别:快速握拳两次是开关,画圈是切模式。这需要引入一个小的时间窗特征提取,把每帧的角度拼成序列,算变化速度和过零次数,逻辑不复杂,但要小心误触发,宁可要求动作幅度大一点。
最后分享一个小技巧:调试阶段不要只看最终数字,一定要把每根手指的弯曲角度实时打印出来,画成曲线或者直接叠在画面上。绝大多数识别问题都能从这几条曲线上直接看出来——是整体偏移了、还是某根手指在抖、还是阈值正好压在噪声带里。盯着最终输出猜原因,效率至少低一半。