Skip to content

FrameLab v1.1.0: Basler/backend support, audit (complexity + bug fixes + tests), dependency pinning#7

Draft
andrefecto wants to merge 54 commits into
mainfrom
dev-v1.1.0
Draft

FrameLab v1.1.0: Basler/backend support, audit (complexity + bug fixes + tests), dependency pinning#7
andrefecto wants to merge 54 commits into
mainfrom
dev-v1.1.0

Conversation

@andrefecto
Copy link
Copy Markdown
Owner

FrameLab v1.1.0 — dev-v1.1.0main

Large release: 53 commits, 71 files, +9,664 / −1,856. Two bodies of work:
(A) the v1.1.0 feature/performance/platform work, and (B) a full codebase
audit (complexity reduction + bug hardening + tests + supply-chain pinning).

Opening as draft — being smoke-tested on the customer's machine tonight before marking ready.


A. Features / performance / platform

  • Basler Ace 2 camera support (GenICam controls) via a new camera backend abstraction layer
    (camera/source.py, source_factory.py, basler_source.py) — OpenCV and Basler behind one interface.
  • Windows camera reliability: switched DirectShow → Media Foundation (MSMF) with a verified
    fallback; MJPG for the 1080p path to fix low FPS; stabilized UUID generation (no more UUID churn
    on replug/restart).
  • Performance / stability: faster Windows camera init, FPS-independent pose smoothing, thread-safety
    and resource-leak fixes, jitter/FPS fixes.
  • Tooling: GitHub Actions CI, install scripts (install.sh/install.bat), run scripts,
    configurable keyboard shortcuts, README/CHANGELOG/CONTRIBUTING.

B. Codebase audit (11 tracked items + security) — all complete

Goal: fewer places for bugs to hide, easier to navigate, no regressions.

Bug fixes (behavioral):

Structure (pure code movement, identical external API):

Security / supply-chain:

  • All dependency versions pinned exactly (==) in requirements*.txt.
  • Pose model pinned to a versioned URL and SHA-256-verified on download/cache (rejects a
    tampered or corrupt model).

Quality gates (now blocking in CI):

  • pylint enforced at 10.00/10; tests grew ~51 → 96; pre-commit hook added.
  • Added the first coverage for coordinate transforms, quadrant logic, pose-detect() thread-safety,
    zoom geometry, buffer scrubbing, and a gui import-cycle guard.

Full per-item detail in TODO.md; sharp edges documented in CLAUDE.md.


✅ Verification

  • CI green on the branch head: test + lint both pass (Python 3.10, Ubuntu, with GL libs).
  • Locally: 96 tests pass, pylint 10.00/10, pre-commit clean, import main OK (Python 3.11 venv).
  • Not yet exercised on real camera hardware — that's tonight's customer-machine test.

🔧 Setup on the customer machine (important)

  1. Recreate the venv — dependencies are now pinned, so an old venv may have mismatched versions:
    python3.11 -m venv venv        # use a 3.9–3.12 interpreter (not 3.13+)
    <venv>/pip install -r requirements.txt
    
  2. First pose-enabled run downloads the ~29 MB model (now checksum-verified) to
    ~/.cache/framelab/models/ — needs network access once.

🧪 Real-hardware smoke checklist (the behavior changes most worth confirming)

📝 Known doc follow-up (non-blocking)

CHANGELOG.md predates the audit — it still lists the removed Preferences dialog and the old
dev-v.1.1.0 branch name. Worth a sync pass before final release, but it doesn't affect runtime.

🤖 Generated with Claude Code

cu-andrefecto and others added 30 commits November 16, 2025 10:32
* Fixed issues with recording not showing wireframes
* Trying to fix a bug where cameras get initialized to a low resolution
  even though they're higher
* Trying to fix a bug where camera FPS shows higher than possible
* Moved the UI around a little to make the menu make sense
* Added a bunch of per-camera settings
* Fixed issues with the wireframe settings not taking effect
* Fixed issues where having pose enabled by default didn't actually work
* Added the ability to configure camera settings like resolution, FPS,
  video codecc
## Bug Fixes:

1. **Video Loading** - Fixed TypeError when loading videos
   - Videos with None max_display dimensions now load correctly
   - Added None check before dimension comparison (camera.py:180)

2. **Camera UUID Consistency** - Fixed per-camera settings not applying
   - FPS display now uses camera_uuid instead of camera_id (camera.py:514)
   - Pose estimation settings use camera_uuid (camera.py:874-884)
   - Per-camera body parts, angles, and side view now persist correctly

3. **High-Resolution Camera Support** - Fixed 1080p/4K cameras on Windows
   - Windows default 640x480 resolution now overridden
   - Requests 4K (3840x2160) on init, falls back to camera max
   - 1080p and 4K cameras now use native resolution

4. **Video Memory Optimization** - Fixed potential memory issues
   - 4K video loading now limits display texture to 1920x1080
   - Prevents oversized DearPyGUI textures
   - Improves UI responsiveness for high-res videos

## Documentation:

- Created CLAUDE.md - comprehensive codebase guide for AI assistance
- Added section markers to major files (camera.py, pose/, gui/, utils/)
- Enhanced module docstrings with architecture notes
- Documented commenting conventions for easier code navigation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
## Performance Enhancements:

1. **60 FPS Camera Support** - Removed artificial FPS throttling
   - Live cameras now run at native hardware FPS
   - Eliminates motion blur from frame rate limiting
   - Significantly improved smoothness for high-FPS cameras
   - Better GPU/CPU utilization (removed sleep delays)

2. **Enhanced Pose Estimation** - Upgraded MediaPipe model
   - Increased model_complexity from 1 to 2 (Heavy model)
   - Better accuracy at cost of more GPU usage
   - Disabled segmentation for better performance
   - GPU automatically optimized by TensorFlow Lite delegate

3. **Pose Smoothing** - Reduced wireframe jitter
   - Added exponential moving average (EMA) smoothing
   - Smoothing factor 0.3 balances responsiveness with stability
   - Increased tracking confidence from 0.5 to 0.7
   - Angle measurements much easier to read

## Bug Fixes:

4. **Pose Estimation Disable Crash** - Fixed MediaPipe shutdown error
   - Fixed "packet timestamp mismatch" race condition
   - Proper shutdown: flag → wait 100ms → release resources
   - Added try-catch wrapper for graceful error handling
   - Clears smoothed landmarks cache on release

5. **Pixel Format Filtering** - Fixed invalid formats in dropdown
   - Improved FOURCC to string conversion for non-printable chars
   - Filters out formats with "?" or "Unknown"
   - Selected formats now persist correctly
   - Prevents restart failures from invalid codes

## UI Improvements:

6. **Adjustable Angle Arc Radius** - Configurable visualization size
   - Added angle_arc_radius setting (default: 20px, down from 30px)
   - New slider in Wireframe → Appearance (range: 10-80px)
   - Smaller arcs reduce clutter, easier to read angles
   - Persists in settings.json

All changes extensively tested with 1080p/4K cameras at 60 FPS.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Significantly improved startup time on Windows systems by optimizing
camera detection and initialization:

- Use DirectShow backend (CAP_DSHOW) on Windows for 2-3x faster camera detection
- Early exit after 2 consecutive failed camera indices (instead of checking all 10)
- Reduced max camera indices to check from 10 to 8
- Reduced camera adjustment delays (0.1s → 0.05s)
- Reduced frame read retry attempts (3 → 2) and delays (0.05s → 0.03s)

Expected improvement: 70-80% reduction in startup time (from 15-20s to 3-5s)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add recording_lock to prevent race condition between capture thread and stop_recording()
- Release pose estimator resources on exception to prevent MediaPipe leaks
- Release VideoCapture on init failure to prevent handle leaks
- Guard against empty frame buffer in toggle_live_pause() to prevent IndexError
- Wrap frame buffer resize with frame_lock for thread safety
- Add retry limit (300) for consecutive cap.read() failures to prevent infinite loop
- Guard seek_to_frame against total_frames=0
- Use try/finally to ensure video_writer.release() on encoding failure
- Extract _get_quadrant_sizes() helper replacing 4 duplicate calculations in layout.py
- Extract _create_mouse_handlers() and _create_quadrant_content() eliminating ~170 duplicate lines
- Remove dead create_dividers() function
- Fix bare except clauses to use except Exception
- Clean up handler registries on layout rebuild to prevent memory leak
- Restore divider positions from settings on startup
- Fix broken settings tests to use UUID-based API
- Remove unused Path imports, fix misleading comment, simplify redundant conditional

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce CameraSource ABC so FrameLab can support industrial cameras
(Basler GigE/USB3 Vision) alongside standard USB webcams. Detection now
returns descriptors instead of bare ints, and the factory pattern selects
the right backend at runtime. pypylon is an optional dependency — existing
webcam-only setups are unaffected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use time-constant-based EMA instead of fixed alpha so landmark smoothing
is consistent regardless of camera frame rate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix zoom label updates in live camera controls using configure_item
  instead of set_value (buttons ignore set_value)
- Add missing tag to video pause button so slider-drag updates it
- Fix off-by-one in video slider seek (was seeking past last frame)
- Guard texture updates with does_item_exist during layout rebuilds
- Protect frame buffer access with frame_lock to prevent race conditions
- Use list(state.cameras) snapshots to prevent iteration errors
- Replace O(n*m) nested loops with O(1) dict lookups in all handlers
- Clamp live buffer slider values to [0.0, 1.0]
- Fix get_position() denominator so video slider reaches 1.0 at end

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pylint setup:
- Add .pylintrc with sensible defaults for DearPyGUI/OpenCV/MediaPipe
- Fix import ordering (stdlib before third-party) across all modules
- Remove unused imports and variables
- Add explicit encoding to file open() calls
- Remove redundant reimports (time module)
- Score: 9.57/10

AI-friendliness improvements:
- Replace magic tuples with LandmarkAdjustment NamedTuple in pose estimator
- Extract DEFAULT_CAMERA_WIDTH/HEIGHT/FPS constants from magic numbers
- Remove dead code (unused frozen_landmarks attribute)
- Add type hints to public API methods (estimator, renderer, camera, settings)
- Rename update_texture() to process_and_render_frame() (with alias)
- Rename "None" string to "Unassigned" in quadrant combo UI
- Add clarifying comment for Settings getter/setter asymmetry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update menu paths, keyboard shortcuts, settings format, angle list,
Python version, project structure, and UI labels to match current code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lower default camera resolution from 4K to 1080p to prevent macOS
AVFoundation from falling back to 1552x1552 square format. Show
full UI with empty quadrants when no cameras are detected so users
can still load videos. Add auto-dependency-update to launcher scripts
and cache-clearing scripts for troubleshooting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cameras now use their native OS/driver defaults instead of forcing
resolution/FPS/pixel format. This fixes initialization failures on
Windows where the release/reopen cycle and DirectShow format
negotiation caused cameras to fail on restart after settings changes.

UUID fingerprint changed from resolution-dependent (unstable) to
backend+index (stable across restarts). Old assignments are
automatically migrated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
DirectShow (CAP_DSHOW) defaults to 640x480 and requires explicit
resolution requests. Media Foundation (CAP_MSMF) auto-negotiates the
camera's best native resolution, matching AVFoundation behavior on
macOS. Added a fallback for legacy backends that still default low.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…fication

On Windows, try Media Foundation first (auto-negotiates best resolution),
fall back to DirectShow if unavailable. Store which backend worked in
the camera descriptor so the same backend is used when creating the
camera source. When the 1080p fallback is triggered, verify the stream
actually works by reading a test frame — revert if it breaks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Uncompressed YUY2 at 1080p saturates USB bandwidth, capping cameras
like the Razer Kiyo Pro at ~18 FPS. Setting MJPG (compressed) format
before the resolution request allows full frame rate (30-60 FPS).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Make Basler cameras the primary detection target with OpenCV webcams as
fallback. Add native GenICam controls (exposure, gain, auto modes, FPS),
USB bandwidth limiting for multi-camera setups, Basler-specific settings
persistence, and a dedicated Camera Controls dialog in the UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix workflow push trigger (dev-v.1.1.0 -> dev-v1.1.0) so the dev branch
  actually runs CI; rename workflow to "CI"
- Add a blocking pylint job (parallel to tests); both jobs cache pip
- Add requirements-dev.txt (pinned pylint + pre-commit, over requirements.txt)
- Add .pre-commit-config.yaml with a local pylint hook
- Bring codebase to pylint 10.00/10: commented .pylintrc disables for
  framework conventions (DPG callback args, too-many-* family, state.py
  module globals) plus behavior-preserving code fixes

Bonus fixes surfaced by pylint:
- Remove ~225 lines of dead nested functions in create_menu_bar
- Fix cell-var-from-loop bug: per-camera Settings/Proc-Amp dialogs showed the
  last camera's name in their titles
- Rename Camera._apply_persisted_settings -> apply_persisted_settings
- Fix pre-existing failing test (test_pose_renderer::test_initialization
  asserted attributes PoseRenderer never defined)
- Update CLAUDE.md (CI/CD + setup) and add TODO.md tracking doc

Verified locally (py3.11): pylint 10.00/10, 51 tests pass, pre-commit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first real CI run (after the branch-typo fix) exposed a pre-existing gap:
all 8 test_pose_estimator tests error with
  OSError: libGLESv2.so.2: cannot open shared object file
because PoseEstimator() loads MediaPipe, which links libGLESv2 even on the CPU
delegate, and the bare ubuntu runner lacks it.

- Install libgl1/libegl1/libgles2 in the test job before pip install
- Bump actions/checkout@v3->v4 and setup-python@v4->v5 (Node 20 deprecation)
- Correct CLAUDE.md: pose tests need GL libs (not fully display-free)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One landmarker per camera had detect() + smoothing/adjustment state reachable
from the capture thread (recording branch) and the main render thread with no
synchronization; concurrent MediaPipe detect() during recording-with-pose is the
intermittent-crash source.

- Add a threading.Lock + _closed flag to PoseEstimator guarding process_frame,
  get_landmarks (now returns None for None/closed results), the four manual-
  adjustment setters, and release() (idempotent; waits for in-flight detect())
- Drop the time.sleep(0.1) hack in Camera.disable_pose_estimation (release() is
  now lock-safe)
- Add concurrency + use-after-release regression tests

No change to detection output, coordinates, smoothing, or recording semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rks)

Manual pose adjustment didn't line up with visible joints when zoomed. The render
loop detects on the zoomed-then-resized frame (landmarks in displayed space), but
the editor re-detected on the raw, un-zoomed frame for hit-testing, so clicks
tested against the wrong positions at zoom > 1.

- Camera publishes current_landmarks (the dict it just drew) and exposes one
  transform, display_to_frame(), used for all mouse<->landmark conversions
- PoseEditor.get_landmark_at_position reuses current_landmarks + display_to_frame
  instead of re-detecting (fixes alignment AND removes the redundant detect() -
  resolves TODO #4); update_drag uses the same transform
- layout.py mouse handler updated to the new update_drag signature
- Tests: pure display_to_frame mapping + new test_pose_editor.py hit-test

No behavior change at zoom = 1; one fewer detect() call site (complements the
thread-safety fix). Detection still runs on the cropped frame (out of scope).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Delete the no-op "Pose Estimation Quality" menu and the pose_max_width
  setting (PoseEstimator never downsampled); drop the dead camera.pose_max_width
  writes and the test assertion. Pose still runs at native resolution.
- Delete gui/preferences_dialog.py (121 lines, never imported/instantiated)
- Remove the unused update_texture alias and fix stale docstring/comment
  references to update_texture / _process_frame_for_display
- Prune CLAUDE.md's now-empty "Dead / no-op features" section

Pure removal, no behavior change. pylint 10.00/10, 57 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#5 1/3)

Move whole functions out of the 2200-line layout.py:
- gui/controls.py: create_video_controls, create_live_camera_controls
- gui/divider.py: divider drag (hit-test, ghost line, mouse down/move/release)
- gui/input_handlers.py: keyboard shortcuts, frame stepping,
  register_global_mouse_handlers, _create_mouse_handlers

Cross-gui calls use function-local imports (existing codebase pattern) to avoid
cycles. main.py imports register_global_mouse_handlers + step_frame_* from
gui.input_handlers. layout.py: 2200 -> 1235 lines. Pure movement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
andrefecto and others added 24 commits May 25, 2026 15:37
Convert gui/dialogs.py -> gui/dialogs/ package and move create_menu_bar's
~18 nested dialog closures into module-level functions:
- dialogs/video.py (moved show_load_video_dialog/load_video_file)
- dialogs/camera_settings.py, proc_amp.py, basler.py, athlete.py,
  app_settings.py, wireframe.py
- dialogs/__init__.py re-exports the public entry points

create_menu_bar now just wires the menu tree to imported callbacks.
layout.py: 1237 -> 355 lines; create_menu_bar: ~940 -> 151 lines. Pure
movement (widget multiset diff vs prior layout.py is empty); pylint 10.00/10,
57 tests pass, import smoke OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 3/3)

- Move create_menu_bar (menu tree) out of layout.py into gui/menu.py; drop the
  dead duplicate DIVIDER_* constants (they live in gui/divider.py)
- layout.py is now ~200 lines: _get_quadrant_sizes, resize_images,
  update_quadrant_sizes, _create_quadrant_content, rebuild_camera_layout
- Point main.py / gui.__init__ at gui.menu for create_menu_bar; rebuild uses a
  function-local import of create_menu_bar
- Add tests/test_gui_imports.py: imports every gui module + asserts key
  callables (guards against import cycles; first automated gui coverage)
- Refresh CLAUDE.md gui map

Concludes the #5 split (layout.py 2200 -> ~200). pylint 10.00/10, 59 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
load_video_file created the dynamic texture at display size
(display_width x display_height) while process_and_render_frame always
pushes a native-size array via dpg.set_value. For any source above the
1920x1080 display cap (e.g. 4K video) the array no longer matched the
texture's creation size — broken/garbage render.

Add Camera.create_display_texture(), which creates the texture at native
self.width x self.height (co-located with the consumer that writes the
same tag, so the size invariant lives in one place). It validates dims,
is idempotent on the tag, and seeds a black frame. main.py and
load_video_file both call it; main.py drops its outer texture_registry
loop (one registry per camera at startup is DPG-safe) and keeps
skip-on-invalid-dims via the bool return. numpy import dropped from both
call sites (now unused).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apply_zoom(cam, action) and toggle_live_pause_ui(cam) were copy-pasted:
the zoom+label-refresh appeared four times (video controls, live controls,
and three keyboard blocks) and the live-pause+scrub-widget toggle twice
(live control button, spacebar-release). Each copy independently restated
the zoom_label_/pause_btn_/live_slider_/step_*_btn_ tag strings and the
"{:.2f}x" label format.

New leaf module gui/camera_actions.py (imports only dpg/state/logger, no
other gui module -> top-level importable, no cycle) owns both. controls.py
callbacks become thin (cam, _ = user_data) wrappers; input_handlers.py's
three zoom loops and the live branch of the spacebar-release handler call
the helpers. Behavior preserved: the controls path previously labeled the
button via `sender`, which is the same pause_btn_{quad} tag the helper
targets; quad is derived from state.camera_positions (same source the
keyboard path already used).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
input_handlers._key_name_to_dpg_key (name -> dpg.mvKey_*) and
shortcuts_dialog._get_key_name (dpg.mvKey_* -> name) were hand-written
inverse dicts of the same ~57 keys that had to be kept in sync by hand.

New leaf module gui/keymap.py holds the canonical KEY_NAME_TO_DPG and a
derived DPG_KEY_TO_NAME = {v: k for k, v in ...}. Both functions now
delegate to the shared dicts. The mapping is bijective (verified 57<->57;
Plus/Add and Minus/Subtract are distinct constants) so the inverse loses
no key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rename-all and individual-camera-settings save callbacks each called
camera.set_friendly_name(name) and then settings.set_camera_name(uuid,
name). set_friendly_name already persists via settings.set_camera_name
(camera.py), so the second call was a redundant double-save (extra
settings.save() round-trip). Removed both; in show_rename_all_dialog the
now-unused get_settings()/settings local goes too. Both call sites are
live-camera-only and set_friendly_name no-ops for video files, so no name
is dropped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add gui.camera_actions + gui.keymap to test_gui_imports (GUI_MODULES +
EXPECTED_CALLABLES). New tests/test_keymap.py asserts KEY_NAME_TO_DPG and
DPG_KEY_TO_NAME are a bijection and round-trip both directions (guards the
derived inverse). New tests/test_camera_actions.py patches dpg + a mock
camera to verify apply_zoom dispatches the right zoom method, refreshes
zoom_label_{quad}, runs even with no quadrant, and rejects bad actions;
and that toggle_live_pause_ui syncs pause_btn/live_slider/step_* widgets.
69 tests pass; pylint 10.00/10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
state.camera_positions is keyed by id(camera), which is reused after GC
and changes on every Load Video / reinit (a fresh object) — a documented
mis-map/blank-quadrant hazard. position_key gives each camera one stable
key: the persistent camera_uuid for live cameras (so the in-memory map
mirrors the UUID-keyed settings 1:1) and a monotonic synthetic id
(caminst_N, never reused) for videos / pre-UUID. Kept as a property (not
frozen) because camera_uuid is assigned after construction. Inert until
the call sites are migrated in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every state.camera_positions access now uses camera.position_key (stable)
instead of id(camera) (reused after GC; new object on Load Video/reinit).
Sites: main.py (init + slider read), quadrants.py (add_camera_to_quadrant),
layout.py, input_handlers.py (6 reads), camera_actions._quadrant_of,
controls.py (slider + record), dialogs/video.py. The .items() delete/shift
sites in remove_*/toggle_quadrant are key-agnostic (unchanged).

save_camera_positions is now a thin mirror: it iterates state.cameras and,
for live cameras (position_key == camera_uuid), writes settings directly —
no more fragile id(cam)==obj_id match loop. controls.on_record_toggle's
key->camera loop collapses to a direct .get(position_key).

Dormant move_camera_to_position/on_position_change params renamed
camera_id->pos_key for clarity. test_camera_actions mocks now carry an
explicit string position_key (required: _quadrant_of reads it). state.py
comment updated. Purely internal; no behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
position_key semantics tests (test_camera.py): live camera -> its UUID;
video -> stable caminst_ synthetic id; two instances get distinct keys.
New test_quadrants.py: save_camera_positions persists the live camera with
its UUID and skips video files / cameras with no position. Synced CLAUDE.md
keying notes (incl. the "Quadrant position mapping" rule that wrongly said
to use id(camera)) and marked TODO #8 done. 74 tests pass; pylint 10.00/10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supply-chain hardening — remove loose >= ranges and untrusted "latest".

requirements.txt / requirements-dev.txt: pin every declared dep exactly (==)
at the current known-good version (opencv-python==4.13.0.92, mediapipe==0.10.35,
numpy==2.2.6, certifi==2026.5.20, dearpygui==2.3.1, openant==1.3.4,
pypylon==26.4.1, pre-commit==4.6.0; pylint already pinned). Reproducible
installs; intentional, reviewable bumps.

pose/estimator.py: pin the PoseLandmarker model URL to the versioned path
(float16/1, not /latest/) so the served bytes can't change silently, and
verify SHA-256 (64437af8...bc7b) on both cache hit and fresh download — a
corrupt cache or tampered/swapped download is now rejected (re-download on
stale cache, raise on bad download). Verified the pinned URL is byte-identical
to what /latest/ currently serves (md5 matches Google's x-goog-hash).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
These handlers logged many INFO lines per mouse click / keypress / joint
drag, flooding logs at the default INFO level and burying real signal.
Demoted the per-event spam to DEBUG:
- gui/divider.py on_mouse_click_handler: the whole per-click block incl.
  the two 60-char "=" banners (the error log stays).
- gui/input_handlers.py: KEY PRESS/RELEASE, Keyboard zoom, spacebar/number
  -key targeting, pause/play toggle, and the two image CLICK lines.
- pose/pose_editor.py: Started/Converted/Finished dragging landmark.
- gui/controls.py on_speed_change: callback + parsed-speed diagnostics.

Kept at INFO the genuine state-change events: recording toggle + saved,
screenshot toggle + saved, "Dividers repositioned and saved", and
"Cleared all pose adjustments". Logging level only — no logic changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gui.quadrants had real, bug-prone logic with no tests. Added
TestQuadrantManipulation: remove_quadrant's position-shift (a camera above
the removed quadrant shifts down; the dual conditions pos>q and p<q must
stay consistent), the inactive-quadrant no-op guard, move_camera_to_position
swap + the KeyError on an absent key, toggle enable-sorts / disable-removes,
and add_camera_to_quadrant eject + the "(None)"/"Load Video..." branches.

Drives the functions over the state globals with gui.layout.rebuild_camera_layout
and gui.quadrants.save_camera_positions patched out; snapshots/restores the
three globals (active_quadrants is reassigned, so restore by reassignment)
and sets them explicitly per test for order-independence. 13 tests in the
file; full suite still green (no state leak).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The zoom/pan crop math was inline in process_and_render_frame and
unobservable (output is always resized back to w x h), so its integer
truncation and edge-clamping were untested. Extracted a pure static
Camera._zoom_crop_box(w, h, zoom_level, cx, cy) -> (x1,y1,x2,y2) with the
math moved verbatim (incl. the int()/`// 2` and the right/bottom
re-adjustment); it returns the full frame for zoom <= 1.0 and the caller
keeps its `if zoom_level > 1.0` guard. No behavior change.

Added TestZoomCropBox: centered 2x box, left/right edge clamps (window
shifted back so width is preserved), the no-zoom full-frame branch, and a
non-divisible zoom (640/3 -> 213) that locks the truncation against a
future "obvious cleanup".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Buffer scrubbing (camera/camera.py): step_buffer_forward/backward clamp at
the last/first frame; seek_buffer_position maps 0..1 -> int(pct*(len-1));
all three no-op (return False, no mutation) when not paused or the buffer
is empty (the empty-buffer guard prevents a negative index).

Angles (pose/renderer.py): calculate_all_angles omits a joint with an
incomplete landmark set (asserted absent, not zeroed) and rounds present
ones to 1 decimal; calculate_angle on a zero vector must not return NaN
(guards the +1e-6 / np.clip).

Refreshed CLAUDE.md's test-coverage note (now 96 tests; lists the added
coverage) and marked TODO #10 done.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First step of splitting the 1146-line Camera god-class via mixins (pure
code movement, identical external API + threading). Moved the zoom/pan
state actions and view-geometry transforms — _zoom_crop_box (static),
zoom_in/out, reset_zoom, display_to_frame — to camera/_zoom.py; Camera now
inherits ZoomMixin. Cross-mixin calls (process_and_render_frame's
self._zoom_crop_box) resolve via MRO on the shared self.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moved video playback (play/pause/loop/speed/seek) and live frame-buffer
scrubbing (toggle_live_pause/step_buffer_*/seek_buffer_position/get_position)
to camera/_playback.py; Camera inherits PlaybackMixin. Pure movement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 3/6)

Moved start_recording/stop_recording (MP4 encode + angles JSON export) and
take_screenshot to camera/_recording.py; Camera inherits RecordingMixin.
The recording_lock drain in stop_recording and the capture-thread append
are unchanged (same instance lock/state). Dropped the now-unused os/datetime
imports from camera.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moved enable_pose_estimation/disable_pose_estimation (and the
'from pose import PoseEstimator, PoseRenderer' dependency) to
camera/_pose.py; Camera inherits PoseMixin. Detection still runs in the
render/capture paths via the shared pose_* state. Same camera->pose import
edge, just relocated — no new cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moved create_display_texture and process_and_render_frame (the main-thread
render: frame_lock read, zoom crop via self._zoom_crop_box, pose overlay,
FPS HUD, texture push) to camera/_render.py; Camera inherits RenderMixin.
Dropped the now-unused dpg/numpy imports from camera.py (cv2 stays for
initialize/_capture_loop).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moved the background capture thread (_capture_loop + start_capture) to
camera/_capture.py; Camera inherits CaptureMixin. camera.py is now 415
lines (from 1146) — the spine: __init__, initialize/settings, position_key,
naming, release. Dropped the now-unused `import time` from camera.py.

Threading is unchanged: _capture_loop (bg thread) and process_and_render_frame
(main thread) still share self.frame_lock/self.recording_lock and the same
instance state — mixins only relocate the method source.

Also: dropped the resolved C0302 (too-many-lines) .pylintrc disable — both
tracked splits (#5 layout, #11 camera) are done and the largest file is now
519 lines. Added a "Map of the camera/ package" to CLAUDE.md and marked
TODO #11 done. pylint 10.00/10, 96 tests pass, pre-commit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update the 1.1.0 entry to match what actually ships: add 'Codebase audit
& hardening' (thread-safe pose, zoom-aligned editing, native-res video
texture, stable quadrant keying) and 'Security' (pinned deps + SHA-256
model verification) sections; refresh the test/CI bullets (96 tests, the
blocking lint job). Remove now-false references to the deleted Preferences
dialog and the removed Pose Estimation Quality feature, fix the FPS-toggle
location, correct the dev-v1.1.0 branch typo, the date, and the 3.9-3.12
Python range.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants