From 11962cdc34124b133ff7c9dc900e504ba088f29a Mon Sep 17 00:00:00 2001 From: ykcheungab Date: Mon, 14 Sep 2026 10:53:33 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=B2=A1=E5=86=99=E7=9A=84?= =?UTF-8?q?=E6=BB=A4=E6=B3=A2=E9=94=AE=E7=AD=89=E4=BA=8E"=E5=85=B3"?= =?UTF-8?q?=EF=BC=8C=E4=B8=8D=E5=86=8D=E5=9B=9E=E8=90=BD=E5=88=B0=E4=BC=9A?= =?UTF-8?q?=E6=BB=A4=E6=B3=A2=E7=9A=84=E9=BB=98=E8=AE=A4=E5=80=BC=20-=20Wh?= =?UTF-8?q?at:=20DEFAULT=5FFORCE=5FMEDIAN=5FWINDOW=203=20->=201=E3=80=81DE?= =?UTF-8?q?FAULT=5FFLOW=5FEMA=5FALPHA=200.5=20->=201.0=EF=BC=88DEFAULT=5FF?= =?UTF-8?q?ORCE=5FLOWPASS=5FHZ=20=E6=9C=AC=E5=B0=B1=E6=98=AF=200.0?= =?UTF-8?q?=EF=BC=89=EF=BC=8C=E5=B9=B6=E6=8A=8A=E6=B3=A8=E9=87=8A=E6=94=B9?= =?UTF-8?q?=E6=88=90"=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E6=B2=A1?= =?UTF-8?q?=E5=86=99=E7=9A=84=E9=94=AE=E5=B0=B1=E6=98=AF=E5=85=B3=EF=BC=8C?= =?UTF-8?q?=E8=80=8C=E4=B8=8D=E6=98=AF=E6=88=91=E4=BB=AC=E8=A7=89=E5=BE=97?= =?UTF-8?q?=E5=90=88=E7=90=86=E7=9A=84=E9=82=A3=E4=B8=80=E6=A1=A3"?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=8E=9F=E5=9B=A0=E5=86=99=E5=9C=A8=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E9=87=8C=EF=BC=9A=E5=90=A6=E5=88=99=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E4=BC=9A=E5=8E=BB=E6=BB=A4=E4=B8=80=E4=B8=AA=E6=B2=A1=E4=BA=BA?= =?UTF-8?q?=E8=A6=81=E6=B1=82=E6=BB=A4=E7=9A=84=E6=B5=8B=E9=87=8F=EF=BC=8C?= =?UTF-8?q?=E8=80=8C=E6=96=87=E4=BB=B6=E9=87=8C=E6=B2=A1=E6=9C=89=E4=BB=BB?= =?UTF-8?q?=E4=BD=95=E7=BA=BF=E7=B4=A2=EF=BC=9B=E5=AE=9E=E6=B5=8B=E7=9A=84?= =?UTF-8?q?=E5=BB=B6=E8=BF=9F/=E9=99=8D=E5=99=AA=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E4=BF=9D=E7=95=99=E5=9C=A8=E6=B3=A8=E9=87=8A=E4=B8=AD=EF=BC=8C?= =?UTF-8?q?=E4=BD=9C=E4=B8=BA=20balanced/low=5Fnoise=20=E9=A2=84=E8=AE=BE?= =?UTF-8?q?=E7=9A=84=E4=BE=9D=E6=8D=AE=E3=80=82docs/filters.md=20=E7=AC=AC?= =?UTF-8?q?=202=20=E8=8A=82=E5=A2=9E=E5=8A=A0"=E7=BC=BA=E7=9C=81=20=3D=20?= =?UTF-8?q?=E5=85=B3"=E8=BF=99=E4=B8=80=E6=9D=A1=E5=B9=B6=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E6=B2=A1=E6=9C=89=20filters=20=E6=AE=B5=E7=9A=84?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=BB=80=E4=B9=88=E9=83=BD=E4=B8=8D=E6=BB=A4?= =?UTF-8?q?=EF=BC=8C=E9=A2=84=E8=AE=BE=E8=A1=A8=E9=87=8C=20balanced=20?= =?UTF-8?q?=E7=9A=84=E7=94=A8=E9=80=94=E7=94=B1"=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=BB=BA=E8=AE=AE"=E6=94=B9=E4=B8=BA"=E5=BB=BA=E8=AE=AE?= =?UTF-8?q?=E8=B5=B7=E6=AD=A5=E6=A1=A3"=EF=BC=88=E5=AE=83=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E6=98=AF=E9=BB=98=E8=AE=A4=E5=80=BC=EF=BC=89=E3=80=82?= =?UTF-8?q?examples/filters/README.md=20=E8=A1=A5=E4=B8=80=E5=8F=A5"?= =?UTF-8?q?=E6=95=B4=E6=AE=B5=E5=88=A0=E6=8E=89=E7=AD=89=E4=BB=B7=E4=BA=8E?= =?UTF-8?q?=20off"=E3=80=82tests/test=5Ffilter=5Fpresets.py=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20test=5Fa=5Fconfig=5Fwith=5Fno=5Ffilters=5Fblock=5Ff?= =?UTF-8?q?ilters=5Fnothing=EF=BC=9A=E6=8A=8A=20box=20bundle=20=E7=9A=84?= =?UTF-8?q?=20filters=20=E6=AE=B5=E6=95=B4=E6=AE=B5=E5=88=A0=E6=8E=89?= =?UTF-8?q?=E5=90=8E=E6=9E=84=E9=80=A0=E7=9C=9F=E5=AE=9E=20Sensor=EF=BC=8C?= =?UTF-8?q?=E6=96=AD=E8=A8=80=20filters=5Fstate()=20=E6=8A=A5=20ema=201.0?= =?UTF-8?q?=20/=20median=201=20/=20cutoff=200.0=20=E4=B8=94=20any=5Fstage?= =?UTF-8?q?=5Fapplied=20=E4=B8=BA=20False=E3=80=82=20-=20Why:=20=E4=B8=8A?= =?UTF-8?q?=E4=B8=80=E7=89=88=E5=8F=AA=E6=8A=8A"=E9=9D=9E=E6=B3=95?= =?UTF-8?q?=E5=80=BC"=E6=94=B9=E6=88=90=E5=9B=9E=E8=90=BD=E5=88=B0?= =?UTF-8?q?=E5=85=B3=EF=BC=8C=E4=BD=86"=E9=94=AE=E6=A0=B9=E6=9C=AC?= =?UTF-8?q?=E4=B8=8D=E5=86=99"=E4=BB=8D=E4=BC=9A=E8=B5=B0=20DEFAULT=5F*=20?= =?UTF-8?q?=E4=BB=8E=E8=80=8C=E6=8A=8A=20EMA=200.5=20+=20=E4=B8=AD?= =?UTF-8?q?=E5=80=BC=203=20=E6=89=93=E5=BC=80=E2=80=94=E2=80=94=E8=BF=99?= =?UTF-8?q?=E4=B8=8E"=E9=85=8D=E7=BD=AE=E8=87=AA=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E3=80=81=E6=96=87=E4=BB=B6=E8=AF=B4=E4=BB=80=E4=B9=88=E5=B0=B1?= =?UTF-8?q?=E8=B7=91=E4=BB=80=E4=B9=88=E3=80=81=E5=87=BA=E5=8E=82=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=85=A8=E5=85=B3"=E7=9F=9B=E7=9B=BE=EF=BC=8C?= =?UTF-8?q?=E4=B8=94=E7=8E=B0=E5=AE=9E=E9=87=8C=E5=AD=98=E5=9C=A8=E5=A4=A7?= =?UTF-8?q?=E9=87=8F=E6=B2=A1=E6=9C=89=20filters=20=E6=AE=B5=E7=9A=84?= =?UTF-8?q?=E6=89=8B=E5=86=99=E9=85=8D=E7=BD=AE=EF=BC=88=E4=BE=8B=E5=A6=82?= =?UTF-8?q?=20=5Fperf/cfg=5Fres=5F280x300.json=EF=BC=89=EF=BC=8C=E5=AE=83?= =?UTF-8?q?=E4=BB=AC=E4=BC=9A=E5=9C=A8=E6=97=A0=E4=BA=BA=E5=AF=9F=E8=A7=89?= =?UTF-8?q?=E7=9A=84=E6=83=85=E5=86=B5=E4=B8=8B=E7=BB=A7=E7=BB=AD=E6=BB=A4?= =?UTF-8?q?=E6=B3=A2=E3=80=82=E7=BC=BA=E7=9C=81=E5=BC=80=E6=98=AF=E8=BF=99?= =?UTF-8?q?=E6=9D=A1=E9=93=BE=E4=B8=8A=E6=9C=80=E5=90=8E=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E9=9A=90=E8=97=8F=E9=BB=98=E8=AE=A4=E5=80=BC=E3=80=82=20-=20Ve?= =?UTF-8?q?rification:=20=E5=88=A0=E6=8E=89=20filters=20=E6=AE=B5=E5=90=8E?= =?UTF-8?q?=E5=AE=9E=E6=B5=8B=20filters=5Fstate()=20=3D=20(1.0,=201,=200.0?= =?UTF-8?q?)=20=E4=B8=94=20any=5Fstage=5Fapplied=3DFalse=EF=BC=8C=E5=87=BA?= =?UTF-8?q?=E5=8E=82=20box/mini/finger=20=E4=B8=89=E4=BB=BD=E5=90=8C?= =?UTF-8?q?=E6=A0=B7=E4=B8=BA=E5=85=A8=E5=85=B3=EF=BC=9Bpytest=20tests/tes?= =?UTF-8?q?t=5Ffilter=5Fpresets.py=20+=20tests/test=5Fcalibration.py=20?= =?UTF-8?q?=E2=86=92=2033=20passed=EF=BC=9BSDK=20=E5=85=A8=E9=87=8F=20336?= =?UTF-8?q?=20passed=20/=2015=20skipped=EF=BC=88=E5=94=AF=E4=B8=80?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E4=BB=8D=E6=98=AF=E9=9C=80=E8=A6=81=201200?= =?UTF-8?q?=20=E5=B8=A7=E8=80=8C=E6=A0=B7=E4=BE=8B=E5=8F=AA=E6=9C=89=20577?= =?UTF-8?q?=20=E5=B8=A7=E7=9A=84=20longrun=20=E6=B5=8B=E8=AF=95=EF=BC=89?= =?UTF-8?q?=EF=BC=9Bapp=20=E4=BE=A7=20studio=2085=20=E9=A1=B9=E4=B8=8E=20s?= =?UTF-8?q?hared=20142=20=E9=A1=B9=E6=B5=8B=E8=AF=95=E3=80=81ui=5Fcheck=20?= =?UTF-8?q?554=20=E9=A1=B9=E6=A3=80=E6=9F=A5=E5=9C=A8=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E5=90=8E=E7=9A=84=200.5.2=20=E4=B8=8B=E5=85=A8=E7=BB=BF?= =?UTF-8?q?=E3=80=82=20-=20Not=20in=20this=20change:=20=E5=87=BA=E5=8E=82?= =?UTF-8?q?=20bundle=20=E7=9A=84=E4=B8=89=E4=B8=AA=E6=98=BE=E5=BC=8F?= =?UTF-8?q?=E5=80=BC=E6=9C=AA=E5=8A=A8=EF=BC=88=E6=9C=AC=E6=9D=A5=E5=B0=B1?= =?UTF-8?q?=E5=85=A8=E5=85=B3=EF=BC=89=EF=BC=9B=5Fperf=20=E7=9A=84?= =?UTF-8?q?=E4=B8=A4=E4=B8=AA=E6=A0=A1=E9=AA=8C=E8=84=9A=E6=9C=AC=E4=B8=8D?= =?UTF-8?q?=E5=9C=A8=E4=BB=93=E5=BA=93=E5=86=85=EF=BC=8C=E5=B7=B2=E5=B0=B1?= =?UTF-8?q?=E5=9C=B0=E8=A1=A5=E4=B8=8A=E6=98=BE=E5=BC=8F=20filters=20?= =?UTF-8?q?=E6=AE=B5=EF=BC=88balanced=EF=BC=89=E4=BB=A5=E4=BF=9D=E6=8C=81"?= =?UTF-8?q?=E5=BC=80=20vs=20=E5=85=B3"=E5=AF=B9=E7=85=A7=E4=BB=8D=E6=9C=89?= =?UTF-8?q?=E6=84=8F=E4=B9=89=EF=BC=9Bapp=20=E4=BE=A7=20wrapper/=E9=BD=BF?= =?UTF-8?q?=E8=BD=AE/CSV=2017=20=E5=88=97=E6=9C=AA=E5=8A=A8=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/filters.md | 5 ++++- examples/filters/README.md | 2 ++ src/orisys/sensor.py | 26 +++++++++++++++++--------- tests/test_filter_presets.py | 27 +++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/docs/filters.md b/docs/filters.md index 48ae3b3..f357b71 100644 --- a/docs/filters.md +++ b/docs/filters.md @@ -21,6 +21,9 @@ - **每个级由自己的值开关**:`flow_ema_alpha >= 1`、`force_median_window <= 1`、 `force_lowpass_cutoff_hz <= 0` 都是"关"。 +- **配置没写的键 = 关**:缺省不是"我们觉得合理的那档",而是"不滤波"。因此**没有 `filters` + 段的配置文件什么都不滤**,出厂 bundle 也把三个值显式写成全关 —— 想让哪一级跑起来, + 必须在文件里写出来(数值或 `preset`)。这样"文件说的"和"设备跑的"才总是一致。 - **非法值 → 关**,绝不回落到"默认开"。 - `filters.enabled` 是**废弃别名**,显式 `false` 表示"三级全关";没有任何开关能覆盖已写的值。 - `filters.preset` 可引用下面的预设;**显式键优先于预设值**(可从预设出发只改一个旋钮)。 @@ -32,7 +35,7 @@ |---|---|---|---|---|---| | `off` | 1.0 | 1 | 0.0 | 0 帧 | 原始读数;文件说什么就跑什么 | | `low_latency` | 1.0 | 3 | 0.0 | ≈1 帧(33 ms) | 延迟敏感,只去尖峰 | -| `balanced` | 0.5 | 3 | 0.0 | ≈2 帧(67 ms) | 默认建议:EMA 干掉力级滤波够不到的光流抖动 | +| `balanced` | 0.5 | 3 | 0.0 | ≈2 帧(67 ms) | 建议起步档:EMA 干掉力级滤波够不到的光流抖动 | | `low_noise` | 0.3 | 3 | 10.0 | ≈3 帧(100 ms) | 底噪优先,读数再低通一道 | 延迟换算 @60fps 减半;数字来自 `_perf/`(对照表见 `orisys-sdk-perf-plan.md`)。 diff --git a/examples/filters/README.md b/examples/filters/README.md index 64f2c96..6340b81 100644 --- a/examples/filters/README.md +++ b/examples/filters/README.md @@ -4,6 +4,8 @@ 到 `sensors//sensor.json` 的 `"filters"` 段即可。原有的 `filters` 段请整段替换, 不要只改其中一行(避免出现 `preset` 与数值混写的歧义——显式键优先于预设值,混写容易看错)。 +配置里**没写**的滤波键等于"关":整段删掉就是什么都不滤,和 `off` 那段等价。 + 背景、代价与可追溯性约定见 `docs/filters.md`;延迟/噪声实测见 `_perf/` 与 `orisys-sdk-perf-plan.md`。 diff --git a/src/orisys/sensor.py b/src/orisys/sensor.py index 452756e..7ec6b54 100644 --- a/src/orisys/sensor.py +++ b/src/orisys/sensor.py @@ -32,16 +32,24 @@ from .signal_processing import ( ) # Default signal-conditioning settings (config: top-level "filters" block). -# Measured on dd02-0703 at 280x300 (see the noise analysis in the perf notes): -# flow EMA 0.5 + median 3 costs ~2 frames at 30 fps, cuts steady-press force -# jitter by roughly half and removes ~85% of the heavy-tailed spikes, while also -# stabilising vnormal/contact (which a force-level filter cannot do). -# The force low-pass is OFF by default: on top of the EMA it added no measurable -# noise benefit at the same latency, so it is kept as an opt-in stage for sites -# with different noise characteristics. +# +# A key a config does not state is **off**, not "whatever we thought was +# sensible": the file has to name what runs, or a device can be filtering a +# measurement nobody asked it to filter and no reader of the file could tell. +# The shipped bundles state all three values explicitly, so they are unaffected +# by these defaults; a config with no "filters" block at all filters nothing. +# +# Which settings are worth asking for is measured, not guessed. On dd02-0703 at +# 280x300 (see the noise analysis in the perf notes) flow EMA 0.5 + median 3 +# costs ~2 frames at 30 fps and cuts steady-press force jitter by roughly half +# while also stabilising vnormal/contact (which a force-level filter cannot do); +# that combination is the "balanced" preset. The force low-pass stays off there +# too: on top of the EMA it added no measurable noise benefit at the same +# latency, so it is an opt-in stage for sites with different noise +# characteristics (the "low_noise" preset). DEFAULT_FORCE_LOWPASS_HZ = 0.0 -DEFAULT_FORCE_MEDIAN_WINDOW = 3 -DEFAULT_FLOW_EMA_ALPHA = 0.5 +DEFAULT_FORCE_MEDIAN_WINDOW = 1 +DEFAULT_FLOW_EMA_ALPHA = 1.0 #: Named filter presets, so a recommendation is *executable* instead of a #: comment in a JSON file (``json.load`` cannot carry comments). A config may diff --git a/tests/test_filter_presets.py b/tests/test_filter_presets.py index 3120c44..dd27fef 100644 --- a/tests/test_filter_presets.py +++ b/tests/test_filter_presets.py @@ -10,6 +10,7 @@ when a copy stops matching them. from __future__ import annotations import json +import shutil from pathlib import Path import pytest @@ -88,3 +89,29 @@ def test_shipped_bundles_report_all_off(bundle: str): assert fp["measurement"] == {"flow_ema_alpha": 1.0} assert fp["readout"]["force_median_window"] == 1 assert fp["readout"]["force_lowpass_cutoff_hz"] == 0.0 + + +def test_a_config_with_no_filters_block_filters_nothing(tmp_path: Path): + """Absence is off, not "the values we thought were sensible". + + A key that is missing has to mean the stage is not running: otherwise a + device filters a measurement nobody asked for, and nothing in the file says + so. Callers with a hand-written config (an old customer file, a bench + script) rely on this. + """ + copy = tmp_path / "box" + shutil.copytree(REPO_ROOT / "sensors" / "box", copy) + path = copy / "sensor.json" + cfg = json.loads(path.read_text(encoding="utf-8")) + cfg.pop("filters", None) + path.write_text(json.dumps(cfg, indent=2), encoding="utf-8") + + sensor = Sensor("external", config_name=str(path), verbose=False, backend="cpu") + try: + state = sensor.filters_state() + finally: + sensor.disconnect() + assert state["measurement"]["flow_ema_alpha"] == 1.0 + assert state["readout"]["force_median_window"] == 1 + assert state["readout"]["force_lowpass_cutoff_hz"] == 0.0 + assert state["any_stage_applied"] is False