162 lines
6.0 KiB
GDScript
162 lines
6.0 KiB
GDScript
extends TestCase
|
||
|
||
## T-WIN-004:239 字节帧反序列化 —— 契约 contracts/websocket-keypoints.md §2
|
||
##
|
||
## 测试向量与 opendance-web 的 TypeScript 参考实现
|
||
## (web-server/src/preprocess/keypoint-codec.ts)保持一致,
|
||
## 保证两端对同一字节流的解释完全相同。
|
||
|
||
const KeypointFrame = preload("res://src/net/keypoint_frame.gd")
|
||
|
||
|
||
func _sample_points() -> PackedVector3Array:
|
||
var points := PackedVector3Array()
|
||
points.resize(33)
|
||
for i in 33:
|
||
points[i] = Vector3(
|
||
float(i % 33) / 32.0,
|
||
1.0 - float(i % 33) / 32.0,
|
||
float((i % 7) - 3) / 10.0
|
||
)
|
||
return points
|
||
|
||
|
||
func _sample_confidence() -> PackedFloat32Array:
|
||
var conf := PackedFloat32Array()
|
||
conf.resize(33)
|
||
for i in 33:
|
||
conf[i] = 1.0 if i % 2 == 0 else 0.5
|
||
return conf
|
||
|
||
|
||
func test_constants_match_contract() -> void:
|
||
eq(KeypointFrame.BINARY_SIZE, 239, "帧长度常量为 239")
|
||
eq(KeypointFrame.KEYPOINT_COUNT, 33, "关键点数量为 33")
|
||
eq(KeypointFrame.HEADER_SIZE + KeypointFrame.KEYPOINT_COUNT * KeypointFrame.POINT_BYTES, 239, "8 + 33×7 = 239")
|
||
eq(KeypointFrame.MAGIC, 0x4F44, "magic 为 'OD'(0x4F44)")
|
||
eq(KeypointFrame.PROTOCOL_VERSION, 1, "协议版本为 1")
|
||
|
||
|
||
func test_encode_produces_239_bytes_with_correct_header() -> void:
|
||
var buf := KeypointFrame.encode(42, _sample_points(), _sample_confidence(), 1)
|
||
eq(buf.size(), 239, "编码结果恰好 239 字节")
|
||
eq(buf.decode_u16(0), 0x4F44, "magic 正确")
|
||
eq(buf.decode_u8(2), 1, "version 正确")
|
||
eq(buf.decode_u8(3), 1, "flags 正确")
|
||
eq(buf.decode_u32(4), 42, "frame_id 正确")
|
||
|
||
|
||
func test_roundtrip_within_fixed_point_precision() -> void:
|
||
var points := _sample_points()
|
||
var conf := _sample_confidence()
|
||
var decoded := KeypointFrame.decode(KeypointFrame.encode(7, points, conf, 0))
|
||
|
||
not_null(decoded, "解码成功")
|
||
eq(decoded.frame_id, 7, "frame_id 往返正确")
|
||
eq(decoded.points.size(), 33, "解码出 33 个点")
|
||
|
||
var max_delta := 0.0
|
||
for i in 33:
|
||
max_delta = maxf(max_delta, absf(decoded.points[i].x - points[i].x))
|
||
max_delta = maxf(max_delta, absf(decoded.points[i].y - points[i].y))
|
||
max_delta = maxf(max_delta, absf(decoded.points[i].z - points[i].z))
|
||
# 定点精度:1/32767 ≈ 3.05e-5,留一倍余量
|
||
ok(max_delta < 1e-4, "坐标往返误差小于定点精度", "最大误差 %.8f" % max_delta)
|
||
|
||
var max_conf_delta := 0.0
|
||
for i in 33:
|
||
max_conf_delta = maxf(max_conf_delta, absf(decoded.confidence[i] - conf[i]))
|
||
ok(max_conf_delta < 1.0 / 255.0 + 1e-6, "置信度往返误差在一个量化步长内", "最大误差 %.8f" % max_conf_delta)
|
||
|
||
|
||
func test_rejects_wrong_length() -> void:
|
||
var short := PackedByteArray()
|
||
short.resize(238)
|
||
ne(KeypointFrame.validate(short), "", "238 字节被拒绝")
|
||
contains(KeypointFrame.validate(short), "239", "错误信息说明期望长度")
|
||
|
||
var long := PackedByteArray()
|
||
long.resize(240)
|
||
ne(KeypointFrame.validate(long), "", "240 字节被拒绝")
|
||
|
||
is_null(KeypointFrame.decode(short), "解码失败返回 null")
|
||
|
||
|
||
func test_rejects_bad_magic() -> void:
|
||
var buf := PackedByteArray()
|
||
buf.resize(239)
|
||
ne(KeypointFrame.validate(buf), "", "全零帧因 magic 错误被拒绝")
|
||
contains(KeypointFrame.validate(buf), "magic", "错误信息指出 magic")
|
||
|
||
|
||
func test_rejects_unsupported_version() -> void:
|
||
var buf := KeypointFrame.encode(0, _sample_points(), _sample_confidence())
|
||
buf.encode_u8(2, 2)
|
||
contains(KeypointFrame.validate(buf), "版本", "版本不符被拒绝")
|
||
|
||
|
||
func test_extreme_values_do_not_overflow() -> void:
|
||
var points := _sample_points()
|
||
points[0] = Vector3(1.0, 1.0, -1.0)
|
||
points[1] = Vector3(0.0, 0.0, 1.0)
|
||
var conf := _sample_confidence()
|
||
conf[0] = 1.0
|
||
conf[1] = 0.0
|
||
|
||
var decoded := KeypointFrame.decode(KeypointFrame.encode(0, points, conf))
|
||
almost(decoded.points[0].x, 1.0, 1e-4, "x = 1.0 不溢出 int16")
|
||
almost(decoded.points[0].z, -1.0, 1e-4, "z = -1.0 不溢出 int16")
|
||
almost(decoded.points[1].z, 1.0, 1e-4, "z = 1.0 不溢出 int16")
|
||
almost(decoded.confidence[1], 0.0, 1e-6, "置信度 0 往返正确")
|
||
|
||
|
||
func test_flags_helpers() -> void:
|
||
var frame := KeypointFrame.decode(
|
||
KeypointFrame.encode(0, _sample_points(), _sample_confidence(), KeypointFrame.FLAG_MIRRORED | KeypointFrame.FLAG_UPPER_BODY_ONLY)
|
||
)
|
||
is_true(frame.is_mirrored(), "镜像标志解析正确")
|
||
is_true(frame.is_upper_body_only(), "上半身模式标志解析正确")
|
||
is_false(frame.has_low_confidence(), "未设置低置信度标志")
|
||
|
||
|
||
func test_mirrored_frame_x_is_flipped() -> void:
|
||
var points := _sample_points()
|
||
var frame := KeypointFrame.decode(
|
||
KeypointFrame.encode(0, points, _sample_confidence(), KeypointFrame.FLAG_MIRRORED)
|
||
)
|
||
var flipped := frame.unmirrored_points()
|
||
almost(flipped[5].x, 1.0 - points[5].x, 1e-4, "镜像帧翻转 x")
|
||
# y 未经翻转,但仍经过 int16 定点往返,容差取定点精度
|
||
almost(flipped[5].y, points[5].y, 1e-4, "翻转不影响 y")
|
||
|
||
var normal := KeypointFrame.decode(KeypointFrame.encode(0, points, _sample_confidence(), 0))
|
||
almost(normal.unmirrored_points()[5].x, points[5].x, 1e-4, "非镜像帧不翻转 x")
|
||
|
||
|
||
func test_confidence_threshold_marks_unreliable() -> void:
|
||
var conf := _sample_confidence()
|
||
conf[11] = 0.49
|
||
conf[12] = 0.5
|
||
var frame := KeypointFrame.decode(KeypointFrame.encode(0, _sample_points(), conf))
|
||
is_false(frame.is_reliable(11), "0.49 判为不可信")
|
||
is_true(frame.is_reliable(12), "0.5 判为可信(阈值含端点)")
|
||
is_true(KeypointFrame.compute_low_confidence(_sample_points(), conf), "存在低置信度点时置位")
|
||
|
||
|
||
func test_json_debug_frame() -> void:
|
||
var raw_points: Array = []
|
||
for i in 33:
|
||
raw_points.append([0.25, 0.75, -0.1, 0.9])
|
||
var frame := KeypointFrame.from_json_dict({
|
||
"frameId": 9,
|
||
"flags": {"mirrored": true, "upperBodyOnly": false},
|
||
"points": raw_points,
|
||
})
|
||
not_null(frame, "JSON 调试帧解析成功")
|
||
eq(frame.frame_id, 9, "frameId 解析正确")
|
||
is_true(frame.is_mirrored(), "mirrored 标志解析正确")
|
||
almost(frame.points[0].x, 0.25, 1e-6, "坐标解析正确")
|
||
|
||
var bad := KeypointFrame.from_json_dict({"frameId": 1, "points": []})
|
||
is_null(bad, "点数不足的 JSON 帧被拒绝")
|