817f830565
Phase 1 of the review asked for one directory per experiment, a force CSV, video of the pre-processing image, and metadata that ties them together. The record channel from the previous change already kept the whole session; this adds the on-disk format. What - Add record/experiment.py: ExperimentRecorder writes one directory per session containing session.json, forces.csv, frames.csv, events.csv, raw_NNNN.mp4 (segmented) and an optional lossless PNG sequence. - forces.csv uses the reviewed column order (session_id, frame_id, t_rel_s, FN_raw, FX_raw, FY_raw, FN_N, FX_N, FY_N, valid, calibration_id) and leaves a cell empty when a value is unavailable: an uncalibrated newton value or a failed readout is never written as 0. - frames.csv is the frame-index mapping: frame_id, t_rel_s, acquisition wall clock, segment, segment_frame, in_video. forces.csv and frames.csv share the packet frame_id and the same t_rel_s origin, so video and data can be aligned after dropped or segmented frames. - session.json carries format version, session id, start/end time in UTC, device and config (with a sha256 of the config file), app/shared/SDK versions, calibration reference, axis convention, filter parameters, video codec and the lossy-MP4 caveat, plus the event log and end-of-run statistics. It is written once at start so a crash still leaves identifiable metadata. - Add a record control to the toolbar: mode (video+data / data only) and a start/stop toggle that stays enabled while recording. - Log zeroing, calibration application and session end as timestamped events. Why - The recorder is fed from the worker sink list on the acquisition thread and only appends to a bounded queue, so disk I/O can never stall acquisition. - On_packet copies the raw frame instead of holding a reference: the packet frame may alias a buffer the SDK reuses between frames. - A queue overflow is counted and alarmed (rate limited), and a write or encode failure is counted, reported and after 5 consecutive errors stops the recording with failed=true rather than silently losing data. Verification - shared/tests/test_experiment_recorder.py: 12 cases covering the directory layout, the CSV contract, frame-id/time-base agreement, segment rolling, data-only and lossless modes, queue overflow accounting, encoder failure, metadata fields, crash-before-stop, and that on_packet never blocks. - tools/ui_check.py gains an "experiment record" section (169 checks, 0 failures overall), driving a recording through the UI and asserting the layout, the empty-cell rule and the event sequence. - apps/studio/tests/test_ui_smoke.py mirrors those invariants. - docs/record-channel.md documents the format and the remaining phase-2 work. Not in this change: CSV export of a selected time range, full-session curve browsing, calibration diagnostics (outlier exclusion, fit residuals) and PyOpenGL 3D.
188 lines
7.6 KiB
JSON
188 lines
7.6 KiB
JSON
{
|
||
"app": {
|
||
"title": "Orisys Studio",
|
||
"about_title": "关于",
|
||
"about_body": "Orisys Studio 桌面应用\n版本: {app_version}",
|
||
"scaffold_hint": "脚手架界面 — 采集功能稍后加入"
|
||
},
|
||
"brand": {
|
||
"name": "ORISYS",
|
||
"tagline": "叠动科技"
|
||
},
|
||
"menu": {
|
||
"file": "文件",
|
||
"open_video": "打开视频…",
|
||
"reveal_config": "在资源管理器中显示配置…",
|
||
"view": "视图",
|
||
"theme": "主题",
|
||
"theme_dark": "深色",
|
||
"theme_light": "浅色",
|
||
"language": "语言",
|
||
"lang_zh": "简体中文",
|
||
"lang_en": "English",
|
||
"calibration_panel": "标定面板",
|
||
"finger_3d": "手指 3D 视图",
|
||
"show_fps": "显示 FPS",
|
||
"plot_settings": "绘图设置…",
|
||
"rotate_arrows": "旋转箭头图 90°",
|
||
"run": "运行 / 停止",
|
||
"stop": "取消采集",
|
||
"preview": "预览 / 停止预览",
|
||
"reset": "重置追踪器",
|
||
"help": "帮助",
|
||
"about": "关于"
|
||
},
|
||
"dialog": {
|
||
"plot_settings_title": "绘图设置",
|
||
"plot_window": "时间窗口",
|
||
"plot_window_tip": "力曲线时间窗口(秒)",
|
||
"plot_fn_max": "FN 上限",
|
||
"plot_fn_max_tip": "法向力纵轴上限(牛顿模式)",
|
||
"plot_shear_max": "剪切上限",
|
||
"plot_shear_max_tip": "剪切力 FX/FY 纵轴 ±上限(牛顿模式)",
|
||
"plot_fn_raw_max": "FN 原始上限",
|
||
"plot_fn_raw_max_tip": "未标定(原始)模式下的法向力纵轴上限。0 = 按数据自动量程;默认值按实测峰值设定,使静止时的抖动在图中不可见",
|
||
"plot_shear_raw_max": "原始剪切上限",
|
||
"plot_shear_raw_max_tip": "未标定(原始)模式下的剪切力 FX/FY 纵轴 ±上限。0 = 自动量程",
|
||
"reset_defaults": "恢复默认",
|
||
"ok": "确定",
|
||
"cancel": "取消"
|
||
},
|
||
"toolbar": {
|
||
"source": "来源",
|
||
"source_camera": "摄像机",
|
||
"source_video": "视频文件",
|
||
"search_cameras": "搜索",
|
||
"cam_id": "摄像机",
|
||
"browse_video": "浏览…",
|
||
"video_placeholder": "未选择视频",
|
||
"preview": "预览",
|
||
"preview_stop": "停止预览",
|
||
"run": "运行",
|
||
"stop": "停止",
|
||
"run_label": "▶ 运行",
|
||
"stop_label": "■ 停止",
|
||
"cancel_label": "■ 取消连接",
|
||
"cancel_tip": "中断连接(连接卡住时随时可用,快捷键 Esc)",
|
||
"run_unavailable": "当前不可运行",
|
||
"run_disabled_video": "请先选择一个视频文件",
|
||
"run_disabled_sensor": "此传感器类型暂不支持运行",
|
||
"reset": "重置",
|
||
"units": "单位",
|
||
"units_newton": "牛顿",
|
||
"units_raw": "原始"
|
||
},
|
||
"units": {
|
||
"newton_short": "N",
|
||
"raw_short": "raw"
|
||
},
|
||
"sensor": {
|
||
"type": "产品",
|
||
"config": "配置",
|
||
"thumb_placeholder": "产品型号"
|
||
},
|
||
"panels": {
|
||
"arrow": "形变箭头",
|
||
"view_arrows_pure": "箭头",
|
||
"view_arrows_stitched": "箭头/采集图像",
|
||
"view_depth": "深度图",
|
||
"view_mode_tip": "运行时的图像显示模式",
|
||
"arrow_empty": "尚未采集图像\n请选择视频或摄像机后点击「运行」",
|
||
"arrow_empty_tip": "运行或预览后在此显示形变箭头图",
|
||
"depth_unavailable": "本帧没有深度数据\n(不是深度为零,也未回退显示普通形变图)",
|
||
"depth_scale": "深度色标 {low} – {high}",
|
||
"depth_scale_tip": "本会话固定色标:色标范围锁定在首个有效深度帧,便于跨帧比较;灰色区域表示无深度数据",
|
||
"plots": "力曲线图",
|
||
"cal": "标定",
|
||
"mesh3d": "三维网格",
|
||
"mesh3d_empty": "手指三维视图占位 —\nOpen3D 网格稍后加入",
|
||
"mesh3d_empty_tip": "手指传感器的三维网格预览"
|
||
},
|
||
"chart": {
|
||
"fn": "FN",
|
||
"fx": "FX",
|
||
"fy": "FY",
|
||
"no_data": "—"
|
||
},
|
||
"record": {
|
||
"label": "记录",
|
||
"mode_video_data": "视频 + 数据",
|
||
"mode_data_only": "仅数据",
|
||
"mode_tip": "视频记录的是处理前的相机输入图像(不含箭头与文字叠加)。MP4 是有损封装,不等于无损原始数据。",
|
||
"start_label": "● 开始记录",
|
||
"stop_label": "■ 停止记录",
|
||
"start_tip": "开始记录本实验:视频、力数据与元数据写入同一目录",
|
||
"stop_tip": "停止记录并写入 session.json(随时可用)",
|
||
"requires_run": "请先点击「运行」开始采集,再开始记录",
|
||
"started": "已开始记录({mode})",
|
||
"start_failed": "无法开始记录: {error}",
|
||
"finished": "记录完成 — {frames} 帧 / {segments} 段,已写入 {path}",
|
||
"finished_with_problems": "记录完成,但存在问题: {detail}",
|
||
"alarm": "记录异常: {detail}"
|
||
},
|
||
"cal": {
|
||
"weight": "砝码重量 (N)",
|
||
"duration": "采集时长",
|
||
"record": "记录",
|
||
"recording_stop": "停止",
|
||
"record_disabled": "请先点击「运行」开始采集,再记录标定样本",
|
||
"record_cancelled": "已取消本次采集",
|
||
"empty_hint": "当前通道尚无样本:运行后选择通道并点击「记录」。",
|
||
"delete_row": "删除行",
|
||
"clear_channel": "清空通道",
|
||
"clear_all": "全部清空",
|
||
"export": "导出…",
|
||
"sn": "序列号",
|
||
"sn_placeholder": "序列号(可选)",
|
||
"live_raw_label": "实时原始值",
|
||
"live_raw": "实时原始值 FN={fn} FX={fx} FY={fy}",
|
||
"col_weight": "重量 (N)",
|
||
"col_raw": "原始均值",
|
||
"col_n": "n",
|
||
"col_std": "σ",
|
||
"col_actions": "",
|
||
"recording": "正在采集标定样本…",
|
||
"recording_stop_tip": "点击可取消本次标定采集(随时可用)",
|
||
"record_ok": "已记录 weight={weight} raw={raw}",
|
||
"record_failed_few": "采集失败 — 样本过少",
|
||
"export_ok": "已导出 {path}",
|
||
"export_local": "同时写入 {path}",
|
||
"export_warnings": "跳过: {detail}",
|
||
"export_failed": "导出失败: {error}",
|
||
"apply_title": "应用标定",
|
||
"apply_confirm": "将刚导出的标定应用到当前设备?\n\n设备类型: {sensor_type}\n配置: {config}\n\n选择「否」只保留导出文件,不改动运行中的设备。",
|
||
"apply_skipped": "已导出,未应用到设备",
|
||
"apply_done": "标定已应用到 {sensor_type}",
|
||
"apply_no_target": "没有可应用的设备(请先运行一次采集)"
|
||
},
|
||
"status": {
|
||
"ready": "就绪",
|
||
"phase_idle": "空闲",
|
||
"phase_connecting": "连接中",
|
||
"phase_running": "运行中",
|
||
"phase_error": "出错",
|
||
"camera_none": "未发现摄像机",
|
||
"camera_found": "发现 {count} 个摄像机: {names}",
|
||
"preview_started": "预览已启动(摄像机 {camera_id})",
|
||
"preview_stopped": "预览已停止",
|
||
"preview_failed": "预览失败: {error}",
|
||
"connecting": "正在连接…",
|
||
"connect_slow": "连接耗时较长,可点击「取消连接」或按 Esc 中断",
|
||
"run_started": "运行中({source})",
|
||
"run_stopped": "已停止",
|
||
"run_stopping": "正在停止…(正在释放设备)",
|
||
"run_stopping_cancelled": "已取消连接(正在释放设备)",
|
||
"sample_invalid": "采样无效(第 {count} 次):{reason} — 该样本已标记无效,未计入曲线与标定",
|
||
"samples_dropped": "记录缓冲已达上限,最早的 {count} 个样本已被释放(全程记录不再完整)",
|
||
"run_failed": "启动失败: {error}",
|
||
"video_ended": "视频已结束",
|
||
"camera_disconnected": "摄像机已断开",
|
||
"reset_ok": "追踪器已重置",
|
||
"finger_run_unavailable": "此传感器类型暂不支持运行",
|
||
"video_required": "请先选择视频文件",
|
||
"config_missing": "未找到传感器配置: {type}",
|
||
"units_raw_fallback": "无标定数据 — 显示原始单位",
|
||
"reveal_failed": "无法打开配置位置: {error}"
|
||
}
|
||
}
|