Files
OpenGait/.sisyphus/notepads/demo-visualizer/issues.md
T
crosstyan 15523bb84c docs(sisyphus): record demo fixes and preprocess research
Capture validated debugging outcomes and ScoNet preprocessing findings in persistent notes so future sessions can resume with verified context instead of redoing the same investigation.
2026-02-28 21:52:07 +08:00

301 lines
12 KiB
Markdown

## Task 1 Fix (Retry)
- Issue: Passing visualize=args.visualize to ScoliosisPipeline caused TypeError
- Fix: Use inspect.signature to conditionally pass visualize only if supported
- Added: `import inspect` and runtime signature check
- Result: No more TypeError; module runs and fails only on missing files (expected)
## Task 1 Fix: Duplication Cleanup
- Issue: File had duplicated CLI block (lines 95-180 were duplicate of 1-93)
- Fix: Removed duplicate block, kept inspect.signature version
- Result: Single clean module with conditional visualize kwarg
## Task 2 Retry: Type Fix Resolution
### Issue Resolution
Fixed 10 basedpyright errors related to MatLike/NDArray type incompatibility.
### Root Cause
OpenCV Python bindings return MatLike (cv2.Mat) which is structurally different from numpy NDArray
even though they are runtime-compatible. Strict type checking flagged these as errors.
### Fix Strategy
Used explicit cast() rather than widening types to preserve type safety at boundaries:
- cast(ImageArray, cv2.cvtColor(...)) # for color conversions
- cast(ImageArray, cv2.resize(...)) # for resizing operations
This approach:
1. Maintains type safety within the module
2. Documents where external library types enter the system
3. Allows LSP to track types correctly through the call chain
### Residual Risks
None. Runtime behavior unchanged; only type annotations modified.
## Task 3 Issues
### LSP Type Warnings (Acceptable)
The following basedpyright warnings exist but are acceptable:
- `viz_payload.get()` shows as error because LSP sees `object` type without `.get()` method
- `self._visualizer.update()` shows as error because LSP sees `object` type without `.update()` method
These are false positives - the code works correctly at runtime. The warnings occur because:
1. `_visualizer` is typed as `object | None` (intentional to avoid circular imports)
2. `viz_payload` is typed as `dict[str, object] | None` (return type of `process_frame`)
Runtime behavior is correct due to Python's dynamic typing.
### No Critical Issues
All verification commands pass. The implementation is complete and functional.
## Task 3 Regression Fix - Issues Resolved
### Original Issue
LSP reportAttributeAccessIssue errors when calling `.get()` and `.update()` on `object` typed values.
### Root Cause
- `_visualizer: object | None` - LSP doesn't know this has `.update()` and `.close()` methods
- `viz_payload: dict[str, object] | None` - LSP sees return type as `dict[str, object]` but after assignment it becomes `object | None`
### Fix Applied
Used explicit `cast()` calls and `getattr()` pattern:
```python
viz_dict = cast(dict[str, object], viz_payload)
mask_raw = viz_dict.get("mask_raw") # Now LSP knows this is valid
visualizer = cast(object, self._visualizer)
update_fn = getattr(visualizer, "update", None)
if callable(update_fn):
keep_running = update_fn(...)
```
### Residual Warnings (Acceptable)
- `reportUnnecessaryCast` - cast is actually needed for LSP, even if technically unnecessary at runtime
- `reportAny` - pyarrow types are dynamically imported
- `reportUnknownMemberType` - list.append with Unknown type
## Task 4: CLI Behavior Regression Fix
### Issue
Tests were failing due to missing validation/exit behavior in argparse-based __main__.py:
- invalid source path expected return code 2 but got 1
- invalid checkpoint path expected return code 2 but got 1
- preprocess-only without silhouette export path expected return code 2 but got 0
### Root Cause
The argparse rewrite (Task 1) was missing the validation logic that existed in the click-based main() function in pipeline.py:
1. No validation that --preprocess-only requires --silhouette-export-path
2. No call to validate_runtime_inputs() before pipeline construction
3. No exception handling to map ValueError -> exit code 2, RuntimeError -> exit code 1
### Fix Applied
Updated opengait/demo/__main__.py to restore behavior parity:
1. Added validation: if args.preprocess_only and not args.silhouette_export_path -> exit code 2
2. Added call to validate_runtime_inputs() before pipeline construction
3. Wrapped pipeline construction and run in try/except:
- ValueError -> print to stderr, exit code 2
- RuntimeError -> print to stderr, exit code 1
4. Added logging.basicConfig() setup (was missing)
### Files Modified
- opengait/demo/__main__.py only
### Verification
- `uv run pytest tests/demo/test_pipeline.py -q` -> 12 passed, 1 skipped
- `uv run python -m opengait.demo --help` -> --visualize flag present
- lsp_diagnostics -> only acceptable warnings (reportAny, unused call results)
## Fixed: NameError for _FrameMetadata in input.py
**Status:** RESOLVED
**Error:**
```
NameError: name '_FrameMetadata' is not defined
File: opengait/demo/input.py
Location: cvmmap_source() nested async generator return annotation
```
**Resolution:**
Moved `_FrameMetadata` and `_CvMmapClient` Protocol classes from inside `if TYPE_CHECKING:` block to module level in `opengait/demo/input.py`.
**Verification:**
- Import test passes
- All 74 demo tests pass (3 skipped)
- Runtime test confirms `cvmmap_source()` returns generator without NameError
## Fixed: Window Freeze on No-Detection Frames
**Status:** RESOLVED
**Issue:** When running demo with `--visualize` flag on video streams with intermittent or no person detections, the OpenCV window would freeze because `viz_payload` was `None` when no detection occurred, and the visualizer update was skipped.
**Root Cause:** In `ScoliosisPipeline.run()`, the visualizer update was conditional on both `self._visualizer is not None` AND `viz_payload is not None`. When YOLO returned no detections, `process_frame()` returned `None`, skipping the visualizer update entirely.
**Fix:** Modified the condition to update visualizer whenever it's enabled, passing `None` values for missing detection data. The `OpenCVVisualizer.update()` method already handles `None` values gracefully by displaying placeholder views.
**Files Changed:**
- `opengait/demo/pipeline.py`: Lines 344-393
- `tests/demo/test_pipeline.py`: Added `MockVisualizer` class and `test_pipeline_visualizer_updates_on_no_detection()`
**Testing:**
- New regression test: `test_pipeline_visualizer_updates_on_no_detection()` - PASSED
- All existing pipeline tests: 13 passed, 1 skipped
- LSP diagnostics: No new errors
## Task 1: Stabilize overlapped duplicate code blocks - RESOLVED
### Issues Fixed
1. **Tuple unpacking mismatch in _select_silhouette**:
- Issue: `select_person()` returns 4-tuple `(mask, bbox_mask, bbox_frame, track_id)`, but code was unpacking as 3-tuple
- Resolution: Updated unpacking to handle all 4 values and use correct bbox for mask_to_silhouette vs return value
2. **Window freeze on no-detection frames**:
- Issue: Visualizer only updated when `viz_payload is not None`, causing window freeze when no person detected
- Resolution: Update visualizer every frame when enabled, using cached payload for no-detection frames
3. **Cached payload mutation risk**:
- Issue: Storing direct reference to viz_payload could allow mutation of cached data
- Resolution: Create copies of mutable values (numpy arrays) when caching using `v.copy() if hasattr(v, 'copy')`
### Pre-existing Issues (Not Fixed)
- LSP type errors with `DemoResult` vs `dict[str, object]` in `_result_buffer` (pre-existing)
- PyArrow type warnings (pre-existing, acceptable)
### Tools Used
- sed for targeted line replacements
- Python script for complex multi-line block replacement
- Native Edit tool for simple single-line changes
- pytest for verification
## Task: BBoxXYXY Type Alias Introduction - COMPLETED
### Scope
Type-semantics cleanup only. No new features, no behavior changes.
### Changes Summary
- **opengait/demo/pipeline.py**: 3 occurrences of `tuple[int, int, int, int]` converted to `BBoxXYXY`
- Import statement updated to include BBoxXYXY from .preprocess
- Function return type annotation in `_select_silhouette()`
- Cast expression type annotation in `run()` method
### No Issues Encountered
- All other scoped files (window.py, visualizer.py, preprocess.py) already used BBoxXYXY correctly
- Tests pass without modification
- No semantic regressions detected
### Pre-existing Conditions Preserved
- LSP warnings in pipeline.py (DemoResult type mismatch, PyArrow Any types) - unchanged
- LSP warnings in window.py (torch/numpy Any types) - unchanged
- LSP warnings in visualizer.py (cv2.Any types) - unchanged
All warnings existed before this task and are unrelated to bbox type annotations.
## 2026-02-28: TypedDict Contract Application
### Pre-existing Issues Encountered
1. **Visualization payload type error** (line 353): Dictionary comprehension with `hasattr(v, 'copy')` check caused basedpyright error because type checker cannot verify attribute existence through runtime introspection
- Fixed by extracting to explicit for-loop with `Callable[[], object]` cast pattern
2. **Type narrowing in comprehensions**: Type narrowing (`is not None` checks) doesn't propagate into dictionary comprehensions, requiring explicit cast before the comprehension
### Remaining Warnings (Pre-existing, Not Related to Task)
- pyarrow types show as `Any` (dynamic import)
- Some `list.append()` methods show as partially unknown
- These are in export code paths unrelated to DemoResult typing
## Fixed: BBox Scale Bug in Fallback Path (2026-02-28)
**Status:** RESOLVED
**Issue:** In `_select_silhouette()` fallback path, bbox returned from `frame_to_person_mask()` was in mask-space coordinates, causing visualization overlay to appear at wrong scale.
**Resolution:**
Added coordinate space conversion using `result.orig_shape` to scale bbox from mask-space to frame-space:
- Extract frame dimensions from `result.orig_shape` with safe fallback
- Calculate scale factors between mask and frame dimensions
- Apply scaling to bbox coordinates for frame-space positioning
- Graceful fallback to mask-space bbox if conversion not possible
**Files Changed:**
- `opengait/demo/pipeline.py`: Lines 206-226 in `_select_silhouette()` method
**Verification:**
- `uv run pytest tests/demo/test_pipeline.py -q` - PASSED (14 passed, 1 skipped)
- LSP diagnostics: No new errors
## Fixed: Both-Mode Raw Mask Empty Display (2026-02-28)
**Status:** RESOLVED
**Issue:** In `_prepare_both_view()`, raw mask pane appeared empty because primary path returns float32 [0,1] mask while display expects uint8 [0,255].
**Resolution:**
Added dtype normalization to ensure mask is always uint8 before display:
- Check if mask is float32/float64 dtype
- If so, scale by 255 and convert to uint8
- Handles both primary path (float) and fallback path (uint8) consistently
**Files Changed:**
- `opengait/demo/visualizer.py`: Lines 330-332 in `_prepare_both_view()` method
**Verification:**
- `uv run pytest tests/demo/test_pipeline.py -q` - PASSED (14 passed, 1 skipped)
- LSP diagnostics: No new errors
## 2026-02-28: Tuple-to-TypedDict Migration Issues
### Regressions Encountered
1. **Duplicate code blocks in pipeline.py**
- Root cause: Incremental edits left stale tuple unpacking alongside new dict access
- Fix: Used Python script for atomic replacement instead of multiple small edits
2. **Stale tuple assertions in test_window.py**
- Root cause: Tests had both dict assertions AND tuple unpacking/assertions
- Fix: Systematically removed all `assert mask...`, `assert bbox...`, `assert tid...` lines
3. **Mock return value overwrite in test_pipeline.py**
- Root cause: Dict mock was immediately overwritten by tuple mock
- Fix: Removed the second tuple mock assignment
### Lessons Learned
- Use Python scripts for complex multi-line replacements to avoid file corruption
- Always grep for ALL tuple unpacking patterns (`a, b = result`, `a, b, c = result`, etc.)
- Tests may have BOTH dict and tuple assertions - must remove both
- Mock return values may be defined twice - check for overwrites
### Pre-existing Issues (not introduced)
- `BBoxXYXY` import unused in window.py (pre-existing)
- Various pyright warnings about Any types (pre-existing)
- Unused imports in pipeline.py (pre-existing)
## 2026-02-28: bbox_frame Documentation (Minor)
### Issue
User requested clarification of `bbox_frame` semantics in docstrings.
### Resolution
Updated `select_person` docstring in `window.py` to explicitly document:
- `bbox_mask`: mask-space XYXY (downsampled, for processing)
- `bbox_frame`: frame-space XYXY (full resolution, for visualization)
### Files Changed
- `opengait/demo/window.py` - Docstring only
### Note
The git checkout restored the tuple return; updated to dict return to match TypedDict contract.