云台速度在 ONVIF 里是 -1.0 到 1.0 的浮点,在国标里是一个字节。 方向也不在同一个地方:一个靠正负号,一个靠指令码。 换算必然有损,损在哪、损多少,这一节算清楚。
上一节转的是媒体,这一节转的是控制。 控制报文小得多,但对「对不对」更敏感——媒体错一点是花屏,控制错一点是往反方向跑。 两套指令集的差别不只在数值上,还在「能表达什么」上。
一、两套指令对不上的地方
ONVIF 的 PTZ 服务是一组动作:ContinuousMove 持续转、RelativeMove 相对位移、
AbsoluteMove 绝对定位、GotoHomePosition 回原位。
国标的 DeviceControl 是一串十六进制:
云台指令 PTZCmd、请求关键帧 IFrameCmd、远程重启 TeleBoot、拉框放大 DragZoom。
能对上的只有「转」这一件事,其余要大打折扣:
预置位在 ONVIF 是一等公民,在国标里靠厂商私有扩展;巡航、雨刷、辅助开关更是各家一套。
所以「控制协议转换」的真实工作量不在换算公式上,
在「哪些能力要放弃、哪些要用私有指令补」的清单上。
一个实用的做法是先做能力矩阵:把两边的动作列成两列,能对上的打勾,对不上的标「降级」。
这张表比代码更早决定项目能不能验收。
二、速度是浮点对字节
ONVIF 的速度是归一化的浮点:-1.0 是反向最快,0 是停,1.0 是正向最快,中间连续可调。 国标是一个字节:0 是停,255 是最快,只有正数;方向写在指令码里。 换算就是「取绝对值、乘 255、四舍五入」,一格是 1/255,最大误差半格。 这个精度够用:千分之二的差别,人眼看云台转动分辨不出来。 真正会出问题的是零附近:摇杆刚推的那一点点会被量化成 0,用户觉得「推了没反应」。 所以做控制的人会在映射前加个死区,把极小值直接归零。 另一端也有问题:0.999 和 1.0 都映射成 255,到了顶就没有分辨力了。
三、方向和死区
方向不在速度字节里,这是最容易写错的一处:ONVIF 用正负号,国标用指令码的第几位。 所以转换的顺序必须是「先看符号定指令码,再对绝对值取字节」,反过来写,云台会往反方向跑。 这种 bug 在测试时很容易被当成「设备装反了」。 指令码本身也有讲究:国标用「位」表示方向,一位一个方向,可以同时按两个方向(斜着走)。 这比 ONVIF 的「一个 pan 加一个 tilt」省事,但也更容易写错位。 验收时有个土办法很有效:发一条「向左」的指令,看画面往哪动。 如果往右,别怀疑设备,先查指令码。
四、动手 Demo:云台速度从浮点变成字节,丢了多少精度
把七个 ONVIF 速度值换算成国标字节,再换回来,看误差有多大、哪里会失真。 最后把一条真实的云台指令按字节摆出来,看方向在哪一位。
to_byte = lambda v: int(round(abs(v) * 255))
for v in (0.05, 0.1, 0.3, 0.5, 0.75, 0.999, 1.0):
b = to_byte(v)
print("%.3f" % v, b, "0x%02X" % b,
"%.5f" % (b / 255.0), "%+.5f" % (b / 255.0 - abs(v)))
body = [0xA5, 0x0F, 0x01, 0x02, to_byte(0.5), 0x00, 0x00]
print(" ".join("%02X" % x for x in body))
实跑输出:
--------------------------------------------------------------------
云台速度从浮点变成字节,丢了多少精度
--------------------------------------------------------------------
**ONVIF 的云台速度是 -1.0 到 1.0 的浮点数**,
**负号表示反向;国标是一个字节,0 到 255,方向写在指令码里。**
**两者对不上,得换算,换算就有精度损失。**
ONVIF 速度 国标字节 十六进制 回算 误差
-------------------------------------------------------------------------
**0.050** **13** **0x0D** **0.05098** **+0.00098**
**0.100** **26** **0x1A** **0.10196** **+0.00196**
**0.300** **76** **0x4C** **0.29804** **-0.00196**
**0.500** **128** **0x80** **0.50196** **+0.00196**
**0.750** **191** **0xBF** **0.74902** **-0.00098**
**0.999** **255** **0xFF** **1.00000** **+0.00100**
**1.000** **255** **0xFF** **1.00000** **+0.00000**
**最后两行说明上限被顶住了**:**0.999 和 1.0 都映射成 255,到了顶就没有分辨力了。**
**再往上没有空间了。**
**最大量化误差是半格**:
一格 = 1/255 = 0.00392
半格 = 0.00196
**千分之二的误差,人眼看云台转动是看不出来的**——**这个精度足够。**
**真正会出问题的是「0 附近」**:
速度 0.001 → 字节 0 → 回算 0.00000,等于「停」。
**摇杆刚推的那一点点,被量化成 0 了。**
**所以做控制的人会在映射前加个死区**,**把 0.002 以下直接归零**,免得用户觉得「推了没反应」。
**再看指令本身长什么样**:
`A5 0F 01 02 80 00 00`
**前三个字节是固定的帧头**,**第四个是方向(向左),第五个是水平速度,后两个是垂直和变倍。**
**后面还跟通道地址和校验字节,各家实现略有差异。**
**注意方向不在速度字节里**:**ONVIF 用正负号表示,国标用指令码表示。**
**转换时先看符号定指令码,再对绝对值取字节**——**顺序反了,云台会往反方向跑。**
误差那一列全部在正负 0.002 以内,最高一格是 1/255 也就是 0.0039,半格就是 0.00196。
换算公式本身没有精度问题,问题在上限和下限。
看最后两行:0.999 和 1.0 都成了 255,到了顶就没有分辨力了。
这不是 bug,是 8 位字节的物理上限。
再看 0.001 那一行:乘 255 得 0.255,四舍五入是 0,回算是 0.00000。
摇杆刚推的那一点点,被量化成「停」了。
所以要加死区,让「极小」明确变成「零」,而不是让用户猜。
那条指令 A5 0F 01 02 80 00 00 里:前三个字节是帧头,第四个是方向,第五个是水平速度。
0x80 就是 128,对应 0.5 的一半速度。
注意方向字节是 02,不是 01:国标用位表示方向,不同位组合就是不同方向。
写代码时不要用「等于几」判断方向,要用「与」运算取位。
30 秒实验
- 算一下:速度 0.25 换算成国标字节是多少?(64,即 0x40)
- 想一下:为什么要在零附近加死区?
- 想一下:0.999 和 1.0 为什么映射成同一个字节?
常见误区
- 以为「速度字节带符号」。 它只有 0 到 255,方向在指令码里。
- 以为「两套协议能一一对应」。 预置位、巡航、辅助开关都要降级或走私有扩展。
- 以为「精度损失大会让云台转错」。 千分之二看不出来,真正会错的是方向和死区。
自测三问(点开对答案)
1. ONVIF 的速度范围和国标的速度范围各是什么? —— **ONVIF 是 -1.0 到 1.0 的浮点,带符号;国标是 0 到 255 的字节,不带符号。** 2. 最大量化误差是多少? —— **半格,约 0.00196**(一格是 1/255)。 3. 为什么方向字节不能用「等于几」来判断? —— **因为它用位表示方向,可以同时置多位表示斜着走**,**必须用「与」运算取位。**下一节看媒体传输:同一路流放进四种承载,一年的账差多少。
- ☐ (本节完,下一节 10.05.07 媒体传输协议的转换)