裸视频可分析:生成工作区、设置提示、逐帧力与按来源选画笔
pass 只认 frames.csv 与 session.json,裸视频此前因此只能播放;现在为它在应用自己的目录里生成 这两份文件并复用既有链路,链路重算的力按帧存入结果并标注为「重算」,box 的空白箭头画布改按帧 来源走共享画笔。 apps/studio/tests 497 项、shared/tests 471 项(2 个已知墙钟失败)、autocal 358 项、ui_check 634 项 均 0 失败,lint_gate 与 doc_lint 无发现;真实 GL 快照见 .scratch/bare-video/snapshots。
This commit is contained in:
@@ -9,7 +9,8 @@
|
||||
一次约 4.8 分钟。Part A 把这一遍跑一次,只存回放真正要画的东西。
|
||||
|
||||
格式版本从 1 升到 2:帧记录里多了深度通道。旧结果不会被误读 —— 版本不符直接
|
||||
`AnalysisFormatError`,重跑一遍即可(结果本来就是可重算的)。
|
||||
`AnalysisFormatError`,重跑一遍即可。力的通道**没有**再升版本:它追加在记录最后,老读端按原偏移
|
||||
仍能读出同一帧,只是没有力(第 1 节末)。
|
||||
|
||||
## 1. 结果模型
|
||||
|
||||
@@ -43,9 +44,27 @@ zlib level 6。
|
||||
而且一旦有深度值落在岛外就会**静默丢掉一个测量**。
|
||||
|
||||
`provenance` 随结果存:算法版本、传感器配置及其内容哈希、标定 id、单位、参考帧 id、首个输出
|
||||
帧 id、解码偏移、逐通道的"重算/记录"标记(`flow`、`depth` 重算,`forces` 记录)、`complete`
|
||||
标志与计数。`counts` 另记 `depth_frames` / `depth_nonzero_frames` / `depth_nonzero_max` /
|
||||
`depth_pixels` 与实测的 `frame_record_bytes`,进度面板和"这一帧为什么是空的"都读它。
|
||||
帧 id、解码偏移、逐通道的"重算/记录"标记(`flow`、`depth` 重算;`forces` 见第 1.1 节)、
|
||||
`synthetic` 与 `synthetic_source`、`complete` 标志与计数。`counts` 另记深度与力通道的计数与实测
|
||||
的 `frame_record_bytes`,进度面板和"这一帧为什么是空的"都读它。
|
||||
|
||||
**帧记录可以再带一份力,追加在最后。** 裸视频没有 `forces.csv`,pass 就把链路算出的力按帧存下:
|
||||
`FRAME_FLAG_FORCE` 后接六个 float32 与两个标志位,28 B/帧。版本不变的理由见开头;读数取不到时存
|
||||
`NaN`。
|
||||
|
||||
### 1.1 裸视频的输入:生成的工作区
|
||||
|
||||
pass 只认一种输入形状(一个目录里的 `frames.csv`、`session.json` 与 `raw_*.mp4`),而形状本身是
|
||||
好的:帧身份、时间与链路都来自录制自己的元数据。裸视频因此**不新增第二条代码路径**,而是把这两个
|
||||
文件为它*生成*出来(`analysis/bare_video.py`):`frames.csv` 给容器每一帧一行
|
||||
(`t_rel_s = index / fps`),`session.json` 给出 `device`/`stats`/`files` 与一个 `synthetic` 块;
|
||||
裸视频只有容器时间,所以容器的钟**就是**信号时间(第 5 节)。
|
||||
|
||||
凡是 `recording_identity` 会哈希的字段都只由**视频与所选配置**决定,不含任何时钟:重开同一条视频
|
||||
就是同一个 key;标定进 key 的是**文件内容**的摘要。视频以 `raw_0001.mp4` 放进工作区(能硬链接就
|
||||
链接):一个文件就是一段,因此**多文件输入直接拒绝**并列出文件名。工作区写在应用自己的派生数据根
|
||||
(`paths.video_workspace_dir()`),**绝不写在操作员视频旁边**;`synthetic` 块被 pass 抄进
|
||||
`provenance.synthetic`/`synthetic_source`,重算出来的测量因此不可能被当成记录的测量读。
|
||||
|
||||
## 2. Keyframe:候选位置与有效掩码
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
@@ -30,6 +31,8 @@ from PySide6.QtWidgets import (
|
||||
from orisys_studio.ui import icons
|
||||
from orisys_studio.ui.icons import ICON_SIZE
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import pyqtgraph as pg
|
||||
except ImportError as exc: # pragma: no cover
|
||||
@@ -190,6 +193,11 @@ class ForcePlotsPanel(QFrame):
|
||||
#: True once a source is attached (either kind). ``_store is None`` was
|
||||
#: the old proxy for this and could not express a disk source.
|
||||
self._source_ready = False
|
||||
#: Non-empty when there is no trace to draw *and that is a fact about the input*:
|
||||
#: a bare video whose pass has not run, or a profile whose chain produced no force
|
||||
#: for it. The mode line states it rather than leaving an empty plot under wording
|
||||
#: that implies data is on its way.
|
||||
self._source_absent_reason = ""
|
||||
#: The live store to return to when a recording source is detached.
|
||||
self._live_store = None
|
||||
self._mode = "follow"
|
||||
@@ -473,8 +481,10 @@ class ForcePlotsPanel(QFrame):
|
||||
for i, plot in enumerate(self.plots):
|
||||
try:
|
||||
plot.getViewBox().setBackgroundColor(QColor(bg))
|
||||
except Exception:
|
||||
pass
|
||||
except Exception: # noqa: BLE001 - a view box that refuses a colour
|
||||
# Reported rather than swallowed: a plot left on the previous theme's
|
||||
# background is a visible defect, and only this line says why it happened.
|
||||
logger.debug("could not set the chart background", exc_info=True)
|
||||
for axis_name in ("left", "bottom"):
|
||||
axis = plot.getAxis(axis_name)
|
||||
axis.setPen(axis_pen)
|
||||
@@ -842,6 +852,7 @@ class ForcePlotsPanel(QFrame):
|
||||
self._live_store = store
|
||||
self._source = StoreCurveSource(store)
|
||||
self._source_ready = store is not None
|
||||
self._source_absent_reason = ""
|
||||
self._on_source_changed()
|
||||
|
||||
def set_recording_source(self, reader) -> None:
|
||||
@@ -850,6 +861,29 @@ class ForcePlotsPanel(QFrame):
|
||||
|
||||
self._source = RecordingCurveSource(reader)
|
||||
self._source_ready = True
|
||||
self._source_absent_reason = ""
|
||||
self._on_source_changed()
|
||||
|
||||
def set_curve_source(self, source, *, reason: str = "") -> None:
|
||||
"""Attach any curve source, or say why there is no trace to draw.
|
||||
|
||||
The charts used to have exactly two states: the live session store, and one
|
||||
recording's ``forces.csv``. A bare video has neither — the force there is the
|
||||
analysis pass's own recomputation, stored inside the result — and the panel has to
|
||||
be able to read it *and* say which of the two it is reading, because a recomputed
|
||||
trace and a recorded one are different claims about the same footage.
|
||||
|
||||
``source is None`` with a ``reason`` is a state, not an empty chart: the mode line
|
||||
states that reason instead of leaving "following live" over a flat plot.
|
||||
"""
|
||||
if source is None:
|
||||
self._source = StoreCurveSource(self._live_store)
|
||||
self._source_ready = False
|
||||
self._source_absent_reason = str(reason or "")
|
||||
else:
|
||||
self._source = source
|
||||
self._source_ready = True
|
||||
self._source_absent_reason = ""
|
||||
self._on_source_changed()
|
||||
|
||||
def clear_recording_source(self) -> None:
|
||||
@@ -857,8 +891,18 @@ class ForcePlotsPanel(QFrame):
|
||||
if self.is_recording_source():
|
||||
self.set_store(self._live_store)
|
||||
|
||||
def curve_kind(self) -> str:
|
||||
"""Which kind of source is attached: ``memory``, ``disk`` or ``analysis``."""
|
||||
return str(getattr(self._source, "kind", ""))
|
||||
|
||||
def is_recording_source(self) -> bool:
|
||||
return str(getattr(self._source, "kind", "")) == "disk"
|
||||
"""True when the curves are read from a file rather than the live session.
|
||||
|
||||
Both a recording's own CSV and a result's recomputed trace count: neither is the
|
||||
live store, and every caller that asks this question (the "back to latest" button,
|
||||
the redraw paths) means "not the live data" rather than "specifically disk".
|
||||
"""
|
||||
return self.curve_kind() in ("disk", "analysis")
|
||||
|
||||
def _switch_unit(self, unit: str) -> None:
|
||||
"""Move the live buffer to another display unit without misreading it.
|
||||
@@ -1274,8 +1318,22 @@ class ForcePlotsPanel(QFrame):
|
||||
continue)") did not fit next to the header's icon buttons at 1280–1440 px
|
||||
in English and was simply cut mid-word, so the parenthetical lives in the
|
||||
tooltip and the label stays a name.
|
||||
|
||||
Three sources can be attached, and two of them are *files* — a recording's own
|
||||
``forces.csv`` and an analysis result's recomputed trace. They are different
|
||||
claims about the same footage, so they get different names rather than one
|
||||
"replay" that would hide which number came from where.
|
||||
"""
|
||||
if self.is_recording_source():
|
||||
if self._source_absent_reason:
|
||||
key = "chart.mode_no_trace"
|
||||
self.mode_label.setText(t(key, reason=self._source_absent_reason))
|
||||
self.mode_label.setToolTip(t("chart.mode_no_trace_tip"))
|
||||
self.btn_latest.setEnabled(False)
|
||||
self.btn_overview.setEnabled(False)
|
||||
return
|
||||
if self.curve_kind() == "analysis":
|
||||
key = "chart.mode_recomputed"
|
||||
elif self.is_recording_source():
|
||||
key = "chart.mode_replay"
|
||||
else:
|
||||
key = "chart.mode_browse" if self._mode == "browse" else "chart.mode_follow"
|
||||
@@ -1397,6 +1455,7 @@ class ForcePlotsPanel(QFrame):
|
||||
try:
|
||||
ymin, ymax = plot.getViewBox().viewRange()[1]
|
||||
except Exception: # noqa: BLE001 - a view box mid-construction
|
||||
logger.debug("could not read the chart's view range", exc_info=True)
|
||||
continue
|
||||
zero_line.setVisible(bool(ymin <= 0.0 <= ymax))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user