FrameLab v1.1.0: Basler/backend support, audit (complexity + bug fixes + tests), dependency pinning#7
Draft
andrefecto wants to merge 54 commits into
Draft
FrameLab v1.1.0: Basler/backend support, audit (complexity + bug fixes + tests), dependency pinning#7andrefecto wants to merge 54 commits into
andrefecto wants to merge 54 commits into
Conversation
* 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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
FrameLab v1.1.0 —
dev-v1.1.0→mainLarge 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
(
camera/source.py,source_factory.py,basler_source.py) — OpenCV and Basler behind one interface.fallback; MJPG for the 1080p path to fix low FPS; stabilized UUID generation (no more UUID churn
on replug/restart).
and resource-leak fixes, jitter/FPS fixes.
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):
detect()behind a lock + idempotent release —fixes the intermittent crash/garbage when recording with pose enabled.
so click/drag lines up at any zoom (no second detection).
(
Camera.create_display_texture) — fixes broken/garbled rendering for sources above 1080p.state.camera_positionskeyed by a stableposition_key(UUID for live,synthetic id for videos) instead of
id(camera)— fixes mis-mapped/blank quadrants after GC /Load Video / reinit.
Structure (pure code movement, identical external API):
gui/layout.py2200 → 200 lines — split intomenu,controls,divider,input_handlers,camera_actions,keymap, and agui/dialogs/package (Automatic bike measurements #5, FrameLab v1.1.0: Basler/backend support, audit (complexity + bug fixes + tests), dependency pinning #7).camera/camera.py1146 → 415 lines — split into capture/render/zoom/playback/recording/posemixins (#11). Same flat instance namespace and threading, so all
camera.*access is unchanged.Security / supply-chain:
==) inrequirements*.txt.tampered or corrupt model).
Quality gates (now blocking in CI):
pylintenforced at 10.00/10; tests grew ~51 → 96; pre-commit hook added.detect()thread-safety,zoom geometry, buffer scrubbing, and a gui import-cycle guard.
Full per-item detail in
TODO.md; sharp edges documented inCLAUDE.md.✅ Verification
test+lintboth pass (Python 3.10, Ubuntu, with GL libs).import mainOK (Python 3.11 venv).🔧 Setup on the customer machine (important)
~/.cache/framelab/models/— needs network access once.🧪 Real-hardware smoke checklist (the behavior changes most worth confirming)
📝 Known doc follow-up (non-blocking)
CHANGELOG.mdpredates the audit — it still lists the removed Preferences dialog and the olddev-v.1.1.0branch name. Worth a sync pass before final release, but it doesn't affect runtime.🤖 Generated with Claude Code