ai-virtual-mouse-controller 1.2.0__tar.gz → 1.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/PKG-INFO +9 -9
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/README.md +8 -6
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/PKG-INFO +9 -9
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/SOURCES.txt +12 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/requires.txt +0 -2
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/config.py +71 -14
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/__init__.py +1 -1
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/camera.py +122 -25
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/cursor_controller.py +32 -16
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/gesture_detector.py +65 -40
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/hand_anchor.py +89 -5
- ai_virtual_mouse_controller-1.3.0/core/hand_gl_renderer.py +347 -0
- ai_virtual_mouse_controller-1.3.0/core/hand_mesh.py +501 -0
- ai_virtual_mouse_controller-1.3.0/core/hand_pose.py +217 -0
- ai_virtual_mouse_controller-1.3.0/core/hand_soft_renderer.py +262 -0
- ai_virtual_mouse_controller-1.3.0/core/hologram_gl_backend.py +334 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/hologram_overlay.py +53 -4
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/presentation_controller.py +3 -2
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/runtime_settings.py +24 -16
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/settings_panel.py +1 -1
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/smoothing.py +37 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/ui_overlay.py +2 -2
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/main.py +15 -1
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/pyproject.toml +6 -8
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/services/virtual_mouse_service.py +212 -127
- ai_virtual_mouse_controller-1.3.0/tests/test_camera.py +118 -0
- ai_virtual_mouse_controller-1.3.0/tests/test_cursor_feel.py +92 -0
- ai_virtual_mouse_controller-1.3.0/tests/test_gesture_detector_behavior.py +200 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_hand_anchor_bottom.py +33 -0
- ai_virtual_mouse_controller-1.3.0/tests/test_hand_mesh.py +75 -0
- ai_virtual_mouse_controller-1.3.0/tests/test_hand_pose.py +111 -0
- ai_virtual_mouse_controller-1.3.0/tests/test_hand_renderers.py +98 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_hologram_overlay.py +3 -3
- ai_virtual_mouse_controller-1.3.0/tests/test_motion_quality.py +87 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_overlay_smoke.py +4 -2
- ai_virtual_mouse_controller-1.3.0/tests/test_runtime_settings.py +50 -0
- ai_virtual_mouse_controller-1.3.0/tests/test_service_camera_loss.py +59 -0
- ai_virtual_mouse_controller-1.2.0/core/hand_mesh.py +0 -794
- ai_virtual_mouse_controller-1.2.0/core/hologram_gl_backend.py +0 -964
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/LICENSE +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/dependency_links.txt +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/entry_points.txt +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/ai_virtual_mouse_controller.egg-info/top_level.txt +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/avmc.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/click_burst.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/finger_posture.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/hand_renderer.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/hand_tracker.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/models/hand_landmarker.task +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/perf_telemetry.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/utils.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/core/windows_path.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/services/__init__.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/setup.cfg +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_click_burst.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_cursor_bottom_reach.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_cursor_click_at.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_gesture_detector.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_hand_renderer.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_hand_tracker.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_perf_telemetry.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_presentation_controller.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_settings_panel.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_smoothing.py +0 -0
- {ai_virtual_mouse_controller-1.2.0 → ai_virtual_mouse_controller-1.3.0}/tests/test_windows_path.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-virtual-mouse-controller
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Controle de cursor por gestos da mao via webcam, com holograma 3D opcional.
|
|
5
5
|
Author: Guilherme Moraes Franco
|
|
6
6
|
License: MIT
|
|
@@ -32,10 +32,8 @@ Requires-Dist: opencv-contrib-python<5.0.0,>=4.10.0
|
|
|
32
32
|
Requires-Dist: mediapipe>=0.10.18
|
|
33
33
|
Requires-Dist: pyautogui<1.0.0,>=0.9.54
|
|
34
34
|
Requires-Dist: numpy>=1.26.0
|
|
35
|
-
Requires-Dist: moderngl>=5.10.0
|
|
36
35
|
Requires-Dist: PySide6>=6.6.0
|
|
37
36
|
Provides-Extra: hologram
|
|
38
|
-
Requires-Dist: moderngl>=5.10.0; extra == "hologram"
|
|
39
37
|
Requires-Dist: PySide6>=6.6.0; extra == "hologram"
|
|
40
38
|
Provides-Extra: dev
|
|
41
39
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
@@ -89,7 +87,7 @@ O **AI Virtual Mouse Controller** é um mouse virtual com reconhecimento de gest
|
|
|
89
87
|
|
|
90
88
|
100% local. Sem nuvem, sem telemetria, sem persistir nada da webcam.
|
|
91
89
|
|
|
92
|
-
**Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy ·
|
|
90
|
+
**Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy · PySide6 (holograma 3D via OpenGL do Qt)
|
|
93
91
|
|
|
94
92
|
---
|
|
95
93
|
|
|
@@ -202,12 +200,13 @@ Toda a calibração vive em [`config.py`](config.py) — constantes nomeadas com
|
|
|
202
200
|
| Seção | O que controla |
|
|
203
201
|
|---|---|
|
|
204
202
|
| `CAMERA` | índice da câmera, resolução, FPS alvo |
|
|
203
|
+
| `PROFILE` | perfil inicial do painel: `smooth`, `precise`, `responsive`, `stable` |
|
|
205
204
|
| `CURSOR_ANCHOR` | qual ponto da mão o cursor segue (`-2` = âncora robusta, default) |
|
|
206
|
-
| `PINCH` | thresholds de detecção de clique (
|
|
205
|
+
| `PINCH` | thresholds de detecção de clique (escala adaptativa por tamanho da mão + histerese pra soltar) |
|
|
207
206
|
| `DPI` / `SMOOTHING` | sensibilidade do cursor + filtro OneEuro |
|
|
208
207
|
| `SCREEN_MARGIN_*` | margens assimétricas (top/bottom < lateral) pra alcançar cantos |
|
|
209
208
|
| `CURSOR_FOLLOWTHROUGH_*` | cinematic prediction quando a mão sai do FOV |
|
|
210
|
-
| `HOLOGRAM` | backend (
|
|
209
|
+
| `HOLOGRAM` | estilo (`quest`, `graphite`, `neon`), backend (OpenGL/QPainter), tamanho, opacidade, FPS |
|
|
211
210
|
|
|
212
211
|
Ajustes em tempo de execução também ficam disponíveis no painel (tecla `S`) — perfis prontos: *smooth*, *precise*, *responsive*, *stable*.
|
|
213
212
|
|
|
@@ -240,7 +239,7 @@ webcam → MediaPipe Hands (21 landmarks)
|
|
|
240
239
|
CursorController ← PyAutoGUI move o cursor real
|
|
241
240
|
```
|
|
242
241
|
|
|
243
|
-
Componentes auxiliares: `HologramOverlay` (
|
|
242
|
+
Componentes auxiliares: `HologramOverlay` (mão 3D estilo headset VR: `hand_pose` → `hand_mesh` → shader OpenGL do Qt, com fallback 2D QPainter), `RuntimeSettings` (perfis e sliders), `PerfTelemetry` (timing p50/p99 por estágio).
|
|
244
243
|
|
|
245
244
|
Estrutura de pastas:
|
|
246
245
|
|
|
@@ -249,7 +248,8 @@ core/ # camera, hand_tracker, gesture_detector, hand_anchor, smoothing
|
|
|
249
248
|
services/ # orquestração do loop principal
|
|
250
249
|
config.py # constantes de calibração documentadas
|
|
251
250
|
main.py # entry point
|
|
252
|
-
|
|
251
|
+
scripts/ # eval_motion.py: mede jitter/atraso/cliques com mãos sintéticas
|
|
252
|
+
tests/ # unitários + gate de qualidade de movimento
|
|
253
253
|
docs/ # site estático + assets
|
|
254
254
|
```
|
|
255
255
|
|
|
@@ -269,7 +269,7 @@ make test-gpu # só testes que precisam de display/GPU real
|
|
|
269
269
|
make test-cov # com coverage HTML
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
-
Testes marcados com `gpu` (overlay PySide6/
|
|
272
|
+
Testes marcados com `gpu` (overlay PySide6/OpenGL) ficam fora do `pytest` default — eles requerem display real e podem crashar em ambientes headless. Rode-os explicitamente com `make test-gpu` quando estiver mexendo no holograma.
|
|
273
273
|
|
|
274
274
|
---
|
|
275
275
|
|
|
@@ -42,7 +42,7 @@ O **AI Virtual Mouse Controller** é um mouse virtual com reconhecimento de gest
|
|
|
42
42
|
|
|
43
43
|
100% local. Sem nuvem, sem telemetria, sem persistir nada da webcam.
|
|
44
44
|
|
|
45
|
-
**Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy ·
|
|
45
|
+
**Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy · PySide6 (holograma 3D via OpenGL do Qt)
|
|
46
46
|
|
|
47
47
|
---
|
|
48
48
|
|
|
@@ -155,12 +155,13 @@ Toda a calibração vive em [`config.py`](config.py) — constantes nomeadas com
|
|
|
155
155
|
| Seção | O que controla |
|
|
156
156
|
|---|---|
|
|
157
157
|
| `CAMERA` | índice da câmera, resolução, FPS alvo |
|
|
158
|
+
| `PROFILE` | perfil inicial do painel: `smooth`, `precise`, `responsive`, `stable` |
|
|
158
159
|
| `CURSOR_ANCHOR` | qual ponto da mão o cursor segue (`-2` = âncora robusta, default) |
|
|
159
|
-
| `PINCH` | thresholds de detecção de clique (
|
|
160
|
+
| `PINCH` | thresholds de detecção de clique (escala adaptativa por tamanho da mão + histerese pra soltar) |
|
|
160
161
|
| `DPI` / `SMOOTHING` | sensibilidade do cursor + filtro OneEuro |
|
|
161
162
|
| `SCREEN_MARGIN_*` | margens assimétricas (top/bottom < lateral) pra alcançar cantos |
|
|
162
163
|
| `CURSOR_FOLLOWTHROUGH_*` | cinematic prediction quando a mão sai do FOV |
|
|
163
|
-
| `HOLOGRAM` | backend (
|
|
164
|
+
| `HOLOGRAM` | estilo (`quest`, `graphite`, `neon`), backend (OpenGL/QPainter), tamanho, opacidade, FPS |
|
|
164
165
|
|
|
165
166
|
Ajustes em tempo de execução também ficam disponíveis no painel (tecla `S`) — perfis prontos: *smooth*, *precise*, *responsive*, *stable*.
|
|
166
167
|
|
|
@@ -193,7 +194,7 @@ webcam → MediaPipe Hands (21 landmarks)
|
|
|
193
194
|
CursorController ← PyAutoGUI move o cursor real
|
|
194
195
|
```
|
|
195
196
|
|
|
196
|
-
Componentes auxiliares: `HologramOverlay` (
|
|
197
|
+
Componentes auxiliares: `HologramOverlay` (mão 3D estilo headset VR: `hand_pose` → `hand_mesh` → shader OpenGL do Qt, com fallback 2D QPainter), `RuntimeSettings` (perfis e sliders), `PerfTelemetry` (timing p50/p99 por estágio).
|
|
197
198
|
|
|
198
199
|
Estrutura de pastas:
|
|
199
200
|
|
|
@@ -202,7 +203,8 @@ core/ # camera, hand_tracker, gesture_detector, hand_anchor, smoothing
|
|
|
202
203
|
services/ # orquestração do loop principal
|
|
203
204
|
config.py # constantes de calibração documentadas
|
|
204
205
|
main.py # entry point
|
|
205
|
-
|
|
206
|
+
scripts/ # eval_motion.py: mede jitter/atraso/cliques com mãos sintéticas
|
|
207
|
+
tests/ # unitários + gate de qualidade de movimento
|
|
206
208
|
docs/ # site estático + assets
|
|
207
209
|
```
|
|
208
210
|
|
|
@@ -222,7 +224,7 @@ make test-gpu # só testes que precisam de display/GPU real
|
|
|
222
224
|
make test-cov # com coverage HTML
|
|
223
225
|
```
|
|
224
226
|
|
|
225
|
-
Testes marcados com `gpu` (overlay PySide6/
|
|
227
|
+
Testes marcados com `gpu` (overlay PySide6/OpenGL) ficam fora do `pytest` default — eles requerem display real e podem crashar em ambientes headless. Rode-os explicitamente com `make test-gpu` quando estiver mexendo no holograma.
|
|
226
228
|
|
|
227
229
|
---
|
|
228
230
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ai-virtual-mouse-controller
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Controle de cursor por gestos da mao via webcam, com holograma 3D opcional.
|
|
5
5
|
Author: Guilherme Moraes Franco
|
|
6
6
|
License: MIT
|
|
@@ -32,10 +32,8 @@ Requires-Dist: opencv-contrib-python<5.0.0,>=4.10.0
|
|
|
32
32
|
Requires-Dist: mediapipe>=0.10.18
|
|
33
33
|
Requires-Dist: pyautogui<1.0.0,>=0.9.54
|
|
34
34
|
Requires-Dist: numpy>=1.26.0
|
|
35
|
-
Requires-Dist: moderngl>=5.10.0
|
|
36
35
|
Requires-Dist: PySide6>=6.6.0
|
|
37
36
|
Provides-Extra: hologram
|
|
38
|
-
Requires-Dist: moderngl>=5.10.0; extra == "hologram"
|
|
39
37
|
Requires-Dist: PySide6>=6.6.0; extra == "hologram"
|
|
40
38
|
Provides-Extra: dev
|
|
41
39
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
@@ -89,7 +87,7 @@ O **AI Virtual Mouse Controller** é um mouse virtual com reconhecimento de gest
|
|
|
89
87
|
|
|
90
88
|
100% local. Sem nuvem, sem telemetria, sem persistir nada da webcam.
|
|
91
89
|
|
|
92
|
-
**Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy ·
|
|
90
|
+
**Stack:** Python 3.10+ · OpenCV · MediaPipe · PyAutoGUI · NumPy · PySide6 (holograma 3D via OpenGL do Qt)
|
|
93
91
|
|
|
94
92
|
---
|
|
95
93
|
|
|
@@ -202,12 +200,13 @@ Toda a calibração vive em [`config.py`](config.py) — constantes nomeadas com
|
|
|
202
200
|
| Seção | O que controla |
|
|
203
201
|
|---|---|
|
|
204
202
|
| `CAMERA` | índice da câmera, resolução, FPS alvo |
|
|
203
|
+
| `PROFILE` | perfil inicial do painel: `smooth`, `precise`, `responsive`, `stable` |
|
|
205
204
|
| `CURSOR_ANCHOR` | qual ponto da mão o cursor segue (`-2` = âncora robusta, default) |
|
|
206
|
-
| `PINCH` | thresholds de detecção de clique (
|
|
205
|
+
| `PINCH` | thresholds de detecção de clique (escala adaptativa por tamanho da mão + histerese pra soltar) |
|
|
207
206
|
| `DPI` / `SMOOTHING` | sensibilidade do cursor + filtro OneEuro |
|
|
208
207
|
| `SCREEN_MARGIN_*` | margens assimétricas (top/bottom < lateral) pra alcançar cantos |
|
|
209
208
|
| `CURSOR_FOLLOWTHROUGH_*` | cinematic prediction quando a mão sai do FOV |
|
|
210
|
-
| `HOLOGRAM` | backend (
|
|
209
|
+
| `HOLOGRAM` | estilo (`quest`, `graphite`, `neon`), backend (OpenGL/QPainter), tamanho, opacidade, FPS |
|
|
211
210
|
|
|
212
211
|
Ajustes em tempo de execução também ficam disponíveis no painel (tecla `S`) — perfis prontos: *smooth*, *precise*, *responsive*, *stable*.
|
|
213
212
|
|
|
@@ -240,7 +239,7 @@ webcam → MediaPipe Hands (21 landmarks)
|
|
|
240
239
|
CursorController ← PyAutoGUI move o cursor real
|
|
241
240
|
```
|
|
242
241
|
|
|
243
|
-
Componentes auxiliares: `HologramOverlay` (
|
|
242
|
+
Componentes auxiliares: `HologramOverlay` (mão 3D estilo headset VR: `hand_pose` → `hand_mesh` → shader OpenGL do Qt, com fallback 2D QPainter), `RuntimeSettings` (perfis e sliders), `PerfTelemetry` (timing p50/p99 por estágio).
|
|
244
243
|
|
|
245
244
|
Estrutura de pastas:
|
|
246
245
|
|
|
@@ -249,7 +248,8 @@ core/ # camera, hand_tracker, gesture_detector, hand_anchor, smoothing
|
|
|
249
248
|
services/ # orquestração do loop principal
|
|
250
249
|
config.py # constantes de calibração documentadas
|
|
251
250
|
main.py # entry point
|
|
252
|
-
|
|
251
|
+
scripts/ # eval_motion.py: mede jitter/atraso/cliques com mãos sintéticas
|
|
252
|
+
tests/ # unitários + gate de qualidade de movimento
|
|
253
253
|
docs/ # site estático + assets
|
|
254
254
|
```
|
|
255
255
|
|
|
@@ -269,7 +269,7 @@ make test-gpu # só testes que precisam de display/GPU real
|
|
|
269
269
|
make test-cov # com coverage HTML
|
|
270
270
|
```
|
|
271
271
|
|
|
272
|
-
Testes marcados com `gpu` (overlay PySide6/
|
|
272
|
+
Testes marcados com `gpu` (overlay PySide6/OpenGL) ficam fora do `pytest` default — eles requerem display real e podem crashar em ambientes headless. Rode-os explicitamente com `make test-gpu` quando estiver mexendo no holograma.
|
|
273
273
|
|
|
274
274
|
---
|
|
275
275
|
|
|
@@ -17,8 +17,11 @@ core/cursor_controller.py
|
|
|
17
17
|
core/finger_posture.py
|
|
18
18
|
core/gesture_detector.py
|
|
19
19
|
core/hand_anchor.py
|
|
20
|
+
core/hand_gl_renderer.py
|
|
20
21
|
core/hand_mesh.py
|
|
22
|
+
core/hand_pose.py
|
|
21
23
|
core/hand_renderer.py
|
|
24
|
+
core/hand_soft_renderer.py
|
|
22
25
|
core/hand_tracker.py
|
|
23
26
|
core/hologram_gl_backend.py
|
|
24
27
|
core/hologram_overlay.py
|
|
@@ -33,17 +36,26 @@ core/windows_path.py
|
|
|
33
36
|
core/models/hand_landmarker.task
|
|
34
37
|
services/__init__.py
|
|
35
38
|
services/virtual_mouse_service.py
|
|
39
|
+
tests/test_camera.py
|
|
36
40
|
tests/test_click_burst.py
|
|
37
41
|
tests/test_cursor_bottom_reach.py
|
|
38
42
|
tests/test_cursor_click_at.py
|
|
43
|
+
tests/test_cursor_feel.py
|
|
39
44
|
tests/test_gesture_detector.py
|
|
45
|
+
tests/test_gesture_detector_behavior.py
|
|
40
46
|
tests/test_hand_anchor_bottom.py
|
|
47
|
+
tests/test_hand_mesh.py
|
|
48
|
+
tests/test_hand_pose.py
|
|
41
49
|
tests/test_hand_renderer.py
|
|
50
|
+
tests/test_hand_renderers.py
|
|
42
51
|
tests/test_hand_tracker.py
|
|
43
52
|
tests/test_hologram_overlay.py
|
|
53
|
+
tests/test_motion_quality.py
|
|
44
54
|
tests/test_overlay_smoke.py
|
|
45
55
|
tests/test_perf_telemetry.py
|
|
46
56
|
tests/test_presentation_controller.py
|
|
57
|
+
tests/test_runtime_settings.py
|
|
58
|
+
tests/test_service_camera_loss.py
|
|
47
59
|
tests/test_settings_panel.py
|
|
48
60
|
tests/test_smoothing.py
|
|
49
61
|
tests/test_windows_path.py
|
|
@@ -146,6 +146,17 @@ DPI_MULTIPLIER_MIN: float = 0.7
|
|
|
146
146
|
DPI_MULTIPLIER_MAX: float = 1.4
|
|
147
147
|
DPI_FIXED_MULTIPLIER: float = 1.00
|
|
148
148
|
|
|
149
|
+
DPI_SIZE_SMOOTHING_S: float = 0.6
|
|
150
|
+
"""Constante de tempo (s) do tamanho da mao usado no DPI adaptativo.
|
|
151
|
+
|
|
152
|
+
O tamanho da mao no frame estima a DISTANCIA ate a camera — algo que muda
|
|
153
|
+
devagar. Frame a frame ele oscila quando o pulso inclina/gira e com o
|
|
154
|
+
ruido do tracker; como o DPI escala a posicao a partir do centro da tela,
|
|
155
|
+
isso fazia o cursor andar sozinho com a mao parada (ate ~50 px longe do
|
|
156
|
+
centro). 0.6 s filtra a inclinacao e ainda acompanha quem aproxima ou
|
|
157
|
+
afasta a mao da camera. 0 = comportamento antigo (cru).
|
|
158
|
+
"""
|
|
159
|
+
|
|
149
160
|
# ---------------------------------------------------------------------
|
|
150
161
|
# SUAVIZACAO
|
|
151
162
|
# ---------------------------------------------------------------------
|
|
@@ -155,9 +166,18 @@ SMOOTHING_STRATEGY: SmoothingStrategy = "one_euro"
|
|
|
155
166
|
|
|
156
167
|
SMOOTHING_FACTOR: float = 0.6
|
|
157
168
|
|
|
169
|
+
PROFILE: str = "smooth"
|
|
170
|
+
"""Perfil inicial do painel: 'smooth', 'precise', 'responsive' ou
|
|
171
|
+
'stable'. Define suavizacao, sensibilidade, pinca, aim assist, sticky e a
|
|
172
|
+
trava no clique (ver core/runtime_settings.PROFILES)."""
|
|
173
|
+
|
|
158
174
|
ONE_EURO_FREQ: float = 60.0
|
|
175
|
+
# min_cutoff e beta efetivos vem do PERFIL (painel, slider Suavizacao) —
|
|
176
|
+
# os dois abaixo ficam como referencia do perfil 'recommended'.
|
|
177
|
+
# beta esta na escala das coordenadas normalizadas (0-1): valores ~0.02
|
|
178
|
+
# deixavam o filtro sem adaptacao e o cursor arrastando atras da mao.
|
|
159
179
|
ONE_EURO_MIN_CUTOFF: float = 1.2
|
|
160
|
-
ONE_EURO_BETA: float =
|
|
180
|
+
ONE_EURO_BETA: float = 5.7
|
|
161
181
|
ONE_EURO_D_CUTOFF: float = 1.0
|
|
162
182
|
DEAD_ZONE_PIXELS: int = 1
|
|
163
183
|
|
|
@@ -192,9 +212,22 @@ vem do debounce de 2 frames + threshold.
|
|
|
192
212
|
|
|
193
213
|
PINCH_VELOCITY_THRESHOLD: float = 0.008
|
|
194
214
|
|
|
215
|
+
PINCH_RELEASE_RATIO: float = 1.25
|
|
216
|
+
"""Histerese da pinca (Schmitt trigger): pra SOLTAR a pinca, polegar e
|
|
217
|
+
indicador precisam abrir ate threshold * 1.25 (entrar continua em
|
|
218
|
+
threshold). Vale tambem pro clique direito.
|
|
219
|
+
|
|
220
|
+
Com limiar unico, o ruido do tracker com os dedos perto da borda fazia a
|
|
221
|
+
pinca "piscar": clique extra no encostar seguinte ou drag caindo sozinho.
|
|
222
|
+
Mesma tecnica dos detectores de pinca da Ultraleap (distancias separadas
|
|
223
|
+
de ativar/desativar). 1.0 = comportamento antigo (limiar unico).
|
|
224
|
+
Suba (1.35) se ainda notar clique duplo acidental; desca (1.15) se soltar
|
|
225
|
+
o drag ficar dificil.
|
|
226
|
+
"""
|
|
227
|
+
|
|
195
228
|
PINCH_MIN_HOLD_SECONDS: float = 0.015
|
|
196
|
-
"""
|
|
197
|
-
|
|
229
|
+
"""Sem efeito no modelo press-to-click (o clique sai no encostar).
|
|
230
|
+
Mantido so pra nao quebrar configs existentes."""
|
|
198
231
|
|
|
199
232
|
# ---------------------------------------------------------------------
|
|
200
233
|
# CLIQUE DIREITO — pinca polegar+medio
|
|
@@ -416,6 +449,13 @@ alvos pequenos errarem.
|
|
|
416
449
|
0.12s cobre o intervalo entre a deteccao do raw shape PINCH e a
|
|
417
450
|
estabilizacao da pose. Aumente pra 0.15-0.20s se ainda notar deriva.
|
|
418
451
|
Diminua pra 0.08s se sentir cursor "preso" apos clicks consecutivos.
|
|
452
|
+
|
|
453
|
+
Em runtime o valor vem do PERFIL (slider "Trava no clique" do painel);
|
|
454
|
+
este e' o valor de referencia do perfil 'smooth'/'recommended'.
|
|
455
|
+
|
|
456
|
+
Ao destravar, o cursor NAO salta pra onde a mao foi durante a trava: a
|
|
457
|
+
diferenca e' absorvida aos poucos conforme a mao se move (ver
|
|
458
|
+
CursorController._reconcile_offset). Vale tambem pro fim do drag.
|
|
419
459
|
"""
|
|
420
460
|
|
|
421
461
|
DRAG_PRECISION_FACTOR: float = 0.55
|
|
@@ -511,25 +551,42 @@ parece magica; em uso prolongado pode distrair.
|
|
|
511
551
|
HOLOGRAM_BACKEND: str = "auto"
|
|
512
552
|
"""Backend de renderizacao do holograma.
|
|
513
553
|
|
|
514
|
-
- "auto" :
|
|
515
|
-
|
|
516
|
-
- "
|
|
554
|
+
- "auto" : OpenGL 3D se houver OpenGL 3.3 (testado na inicializacao);
|
|
555
|
+
senao cai pro renderer 2D QPainter no mesmo estilo
|
|
556
|
+
- "gl" : forca OpenGL (mesh 3D + shader)
|
|
557
|
+
- "qpainter" : forca o renderer 2D (compatibilidade/debug)
|
|
558
|
+
|
|
559
|
+
O OpenGL e' o do proprio Qt (PySide6) — nao depende de moderngl/
|
|
560
|
+
glcontext, cuja DLL o Smart App Control do Windows pode bloquear.
|
|
561
|
+
Em ambientes sem GPU/driver (RDP, VM sem aceleracao) o "auto" ja cai
|
|
562
|
+
pro QPainter sozinho.
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
HOLOGRAM_STYLE: str = "quest"
|
|
566
|
+
"""Aparencia da mao.
|
|
567
|
+
|
|
568
|
+
- "quest" : mao 3D estilo headset VR — azul-acinzentado claro, luz
|
|
569
|
+
suave, borda luminosa, antebraco em fade, pontas brilham
|
|
570
|
+
na pinca (default)
|
|
571
|
+
- "graphite" : mesma mao, grafite translucido com contorno claro (fundos
|
|
572
|
+
claros)
|
|
573
|
+
- "neon" : visual antigo (silhueta neon + particulas, so 2D)
|
|
517
574
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
(RDP, VM sem aceleracao), usar "qpainter".
|
|
575
|
+
Nos estilos 3D a mao segue a pose real: aspecto correto da camera,
|
|
576
|
+
profundidade de cada articulacao e escala estavel pela palma (fechar a
|
|
577
|
+
mao nao a "infla"). Ver core/hand_pose.py.
|
|
522
578
|
"""
|
|
523
579
|
|
|
524
580
|
HOLOGRAM_TOGGLE_KEY: str = "h"
|
|
525
581
|
"""Tecla pra alternar o holograma em runtime."""
|
|
526
582
|
|
|
527
|
-
HOLOGRAM_OPACITY: float = 0.
|
|
528
|
-
"""Opacidade
|
|
529
|
-
fracoes
|
|
583
|
+
HOLOGRAM_OPACITY: float = 0.90
|
|
584
|
+
"""Opacidade da mao. No estilo "quest" o corpo fica ~72% opaco (borda
|
|
585
|
+
mais forte); no "neon" as camadas usam fracoes deste valor."""
|
|
530
586
|
|
|
531
587
|
HOLOGRAM_HAND_SIZE_PX: int = 180
|
|
532
|
-
"""Tamanho
|
|
588
|
+
"""Tamanho da mao na tela (px): comprimento da mao aberta, do pulso a
|
|
589
|
+
ponta do dedo medio. Nao muda com o gesto."""
|
|
533
590
|
|
|
534
591
|
HOLOGRAM_FPS: int = 30
|
|
535
592
|
"""Taxa de redesenho. 30 Hz e' suave + economico em CPU."""
|
|
@@ -24,7 +24,7 @@ from __future__ import annotations
|
|
|
24
24
|
import logging
|
|
25
25
|
import threading
|
|
26
26
|
import time
|
|
27
|
-
from typing import Optional
|
|
27
|
+
from typing import Optional, Tuple
|
|
28
28
|
|
|
29
29
|
import cv2
|
|
30
30
|
import numpy as np
|
|
@@ -71,41 +71,56 @@ class ThreadedCamera:
|
|
|
71
71
|
|
|
72
72
|
self._cap: Optional[cv2.VideoCapture] = None
|
|
73
73
|
self._frame: Optional[np.ndarray] = None
|
|
74
|
-
|
|
74
|
+
# Numero de sequencia do frame: deixa o consumidor processar cada
|
|
75
|
+
# frame da webcam UMA vez (sem reprocessar duplicatas).
|
|
76
|
+
self._seq: int = 0
|
|
77
|
+
self._cond = threading.Condition()
|
|
75
78
|
self._stop_event = threading.Event()
|
|
76
79
|
self._thread: Optional[threading.Thread] = None
|
|
77
80
|
self._opened = False
|
|
81
|
+
# True quando a webcam parou de entregar frames (desconectada ou
|
|
82
|
+
# tomada por outro app). O thread tenta reabrir sozinho.
|
|
83
|
+
self._stalled = False
|
|
78
84
|
|
|
79
85
|
# -----------------------------------------------------------------
|
|
80
86
|
# Lifecycle
|
|
81
87
|
# -----------------------------------------------------------------
|
|
82
88
|
|
|
83
|
-
def
|
|
84
|
-
"""
|
|
85
|
-
Abre a webcam e inicia o thread de captura.
|
|
86
|
-
|
|
87
|
-
Tenta primeiro com backend nativo do SO; se falhar, usa default.
|
|
88
|
-
"""
|
|
89
|
+
def _open_capture(self) -> Optional[cv2.VideoCapture]:
|
|
90
|
+
"""Abre o dispositivo (DirectShow primeiro, depois default)."""
|
|
89
91
|
# Tenta backend especifico do SO (mais estavel)
|
|
90
92
|
cap = cv2.VideoCapture(self.index, cv2.CAP_DSHOW)
|
|
91
93
|
if not cap.isOpened():
|
|
92
94
|
logger.debug("CAP_DSHOW falhou, tentando default")
|
|
95
|
+
cap.release()
|
|
93
96
|
cap = cv2.VideoCapture(self.index)
|
|
94
|
-
|
|
95
97
|
if not cap.isOpened():
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"Verifique se ela esta conectada e nao ocupada por outro app."
|
|
99
|
-
)
|
|
98
|
+
cap.release()
|
|
99
|
+
return None
|
|
100
100
|
|
|
101
101
|
cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.width)
|
|
102
102
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.height)
|
|
103
103
|
cap.set(cv2.CAP_PROP_FPS, self.fps)
|
|
104
104
|
# Buffer minimo = sempre frame mais recente
|
|
105
105
|
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
|
106
|
+
return cap
|
|
107
|
+
|
|
108
|
+
def open(self) -> "ThreadedCamera":
|
|
109
|
+
"""
|
|
110
|
+
Abre a webcam e inicia o thread de captura.
|
|
111
|
+
|
|
112
|
+
Tenta primeiro com backend nativo do SO; se falhar, usa default.
|
|
113
|
+
"""
|
|
114
|
+
cap = self._open_capture()
|
|
115
|
+
if cap is None:
|
|
116
|
+
raise CameraOpenError(
|
|
117
|
+
f"Nao foi possivel abrir a webcam (index={self.index}). "
|
|
118
|
+
"Verifique se ela esta conectada e nao ocupada por outro app."
|
|
119
|
+
)
|
|
106
120
|
|
|
107
121
|
self._cap = cap
|
|
108
122
|
self._opened = True
|
|
123
|
+
self._stalled = False
|
|
109
124
|
|
|
110
125
|
# Aquece a webcam (descarta primeiros frames pretos)
|
|
111
126
|
time.sleep(self.warmup_seconds)
|
|
@@ -129,6 +144,8 @@ class ThreadedCamera:
|
|
|
129
144
|
return
|
|
130
145
|
|
|
131
146
|
self._stop_event.set()
|
|
147
|
+
with self._cond:
|
|
148
|
+
self._cond.notify_all()
|
|
132
149
|
if self._thread is not None:
|
|
133
150
|
self._thread.join(timeout=2.0)
|
|
134
151
|
self._thread = None
|
|
@@ -150,31 +167,70 @@ class ThreadedCamera:
|
|
|
150
167
|
# Loop interno
|
|
151
168
|
# -----------------------------------------------------------------
|
|
152
169
|
|
|
170
|
+
_MAX_FAILURES: int = 30
|
|
171
|
+
_REOPEN_INTERVAL_S: float = 1.0
|
|
172
|
+
|
|
153
173
|
def _capture_loop(self) -> None:
|
|
154
|
-
"""Roda em thread separada lendo frames sem parar.
|
|
174
|
+
"""Roda em thread separada lendo frames sem parar.
|
|
175
|
+
|
|
176
|
+
Se a webcam parar de responder (cabo USB solto, outro app pegou o
|
|
177
|
+
dispositivo), marca `stalled`, descarta o ultimo frame (pra ninguem
|
|
178
|
+
processar imagem congelada como se fosse ao vivo) e tenta reabrir
|
|
179
|
+
a cada segundo ate voltar ou ate close().
|
|
180
|
+
"""
|
|
155
181
|
consecutive_failures = 0
|
|
156
|
-
max_failures = 30
|
|
157
182
|
|
|
158
183
|
while not self._stop_event.is_set():
|
|
159
|
-
|
|
160
|
-
|
|
184
|
+
cap = self._cap or self._try_reopen()
|
|
185
|
+
if cap is None:
|
|
186
|
+
continue
|
|
161
187
|
|
|
162
|
-
ok, frame =
|
|
188
|
+
ok, frame = cap.read()
|
|
163
189
|
if not ok or frame is None:
|
|
164
190
|
consecutive_failures += 1
|
|
165
|
-
if consecutive_failures >=
|
|
191
|
+
if consecutive_failures >= self._MAX_FAILURES:
|
|
166
192
|
logger.error(
|
|
167
|
-
"Webcam falhou %d frames consecutivos
|
|
168
|
-
|
|
193
|
+
"Webcam falhou %d frames consecutivos — tentando "
|
|
194
|
+
"reconectar", self._MAX_FAILURES,
|
|
169
195
|
)
|
|
170
|
-
|
|
196
|
+
self._mark_stalled()
|
|
197
|
+
consecutive_failures = 0
|
|
198
|
+
continue
|
|
171
199
|
time.sleep(0.01)
|
|
172
200
|
continue
|
|
173
201
|
|
|
174
202
|
consecutive_failures = 0
|
|
175
|
-
|
|
176
|
-
with self._lock:
|
|
203
|
+
with self._cond:
|
|
177
204
|
self._frame = frame
|
|
205
|
+
self._seq += 1
|
|
206
|
+
self._cond.notify_all()
|
|
207
|
+
|
|
208
|
+
def _mark_stalled(self) -> None:
|
|
209
|
+
with self._cond:
|
|
210
|
+
self._stalled = True
|
|
211
|
+
self._frame = None
|
|
212
|
+
self._cond.notify_all()
|
|
213
|
+
if self._cap is not None:
|
|
214
|
+
self._cap.release()
|
|
215
|
+
self._cap = None
|
|
216
|
+
|
|
217
|
+
def _try_reopen(self) -> Optional[cv2.VideoCapture]:
|
|
218
|
+
"""Uma tentativa de reabrir. None = esperou o intervalo e falhou."""
|
|
219
|
+
if self._stop_event.wait(self._REOPEN_INTERVAL_S):
|
|
220
|
+
return None
|
|
221
|
+
cap = self._open_capture()
|
|
222
|
+
if cap is None:
|
|
223
|
+
return None
|
|
224
|
+
if self._stop_event.is_set():
|
|
225
|
+
# close() chegou durante a abertura (DirectShow pode levar
|
|
226
|
+
# segundos): nao deixar o dispositivo aberto sem dono.
|
|
227
|
+
cap.release()
|
|
228
|
+
return None
|
|
229
|
+
self._cap = cap
|
|
230
|
+
with self._cond:
|
|
231
|
+
self._stalled = False
|
|
232
|
+
logger.info("Webcam %d reconectada", self.index)
|
|
233
|
+
return cap
|
|
178
234
|
|
|
179
235
|
# -----------------------------------------------------------------
|
|
180
236
|
# API publica
|
|
@@ -190,7 +246,7 @@ class ThreadedCamera:
|
|
|
190
246
|
Returns:
|
|
191
247
|
Frame BGR (numpy array) ou None se nada disponivel ainda.
|
|
192
248
|
"""
|
|
193
|
-
with self.
|
|
249
|
+
with self._cond:
|
|
194
250
|
if self._frame is None:
|
|
195
251
|
return None
|
|
196
252
|
frame = self._frame.copy()
|
|
@@ -199,6 +255,47 @@ class ThreadedCamera:
|
|
|
199
255
|
frame = cv2.flip(frame, 1)
|
|
200
256
|
return frame
|
|
201
257
|
|
|
258
|
+
def read_new(
|
|
259
|
+
self,
|
|
260
|
+
last_seq: int,
|
|
261
|
+
timeout: float,
|
|
262
|
+
mirror: bool = True,
|
|
263
|
+
) -> Tuple[Optional[np.ndarray], int]:
|
|
264
|
+
"""
|
|
265
|
+
Espera ate `timeout` por um frame MAIS NOVO que `last_seq`.
|
|
266
|
+
|
|
267
|
+
O loop principal costuma rodar mais rapido que a webcam (30 fps
|
|
268
|
+
e' o comum). Com read() ele reprocessava o mesmo frame varias
|
|
269
|
+
vezes: inferencia desperdicada e, pior, frames repetidos com
|
|
270
|
+
velocidade zero distorciam histerese, sticky e follow-through.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
(frame, seq) — frame None se nada novo chegou no prazo; nesse
|
|
274
|
+
caso seq volta igual a `last_seq`.
|
|
275
|
+
"""
|
|
276
|
+
with self._cond:
|
|
277
|
+
if self._seq == last_seq or self._frame is None:
|
|
278
|
+
self._cond.wait_for(
|
|
279
|
+
lambda: (
|
|
280
|
+
(self._seq != last_seq and self._frame is not None)
|
|
281
|
+
or self._stop_event.is_set()
|
|
282
|
+
),
|
|
283
|
+
timeout=timeout,
|
|
284
|
+
)
|
|
285
|
+
if self._seq == last_seq or self._frame is None:
|
|
286
|
+
return None, last_seq
|
|
287
|
+
frame = self._frame.copy()
|
|
288
|
+
seq = self._seq
|
|
289
|
+
|
|
290
|
+
if mirror:
|
|
291
|
+
frame = cv2.flip(frame, 1)
|
|
292
|
+
return frame, seq
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def is_stalled(self) -> bool:
|
|
296
|
+
"""Webcam parou de entregar frames e esta tentando reconectar."""
|
|
297
|
+
return self._stalled
|
|
298
|
+
|
|
202
299
|
@property
|
|
203
300
|
def is_opened(self) -> bool:
|
|
204
301
|
"""Camera aberta e thread ativa."""
|