lazylabel-gui 1.3.6__tar.gz → 1.3.8__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.
Files changed (63) hide show
  1. lazylabel_gui-1.3.8/PKG-INFO +227 -0
  2. lazylabel_gui-1.3.8/README.md +172 -0
  3. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/pyproject.toml +1 -1
  4. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/main_window.py +82 -44
  5. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/modes/multi_view_mode.py +38 -10
  6. lazylabel_gui-1.3.8/src/lazylabel_gui.egg-info/PKG-INFO +227 -0
  7. lazylabel_gui-1.3.6/PKG-INFO +0 -197
  8. lazylabel_gui-1.3.6/README.md +0 -142
  9. lazylabel_gui-1.3.6/src/lazylabel_gui.egg-info/PKG-INFO +0 -197
  10. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/LICENSE +0 -0
  11. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/setup.cfg +0 -0
  12. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/__init__.py +0 -0
  13. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/__main__.py +0 -0
  14. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/config/__init__.py +0 -0
  15. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/config/hotkeys.py +0 -0
  16. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/config/paths.py +0 -0
  17. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/config/settings.py +0 -0
  18. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/core/__init__.py +0 -0
  19. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/core/file_manager.py +0 -0
  20. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/core/model_manager.py +0 -0
  21. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/core/segment_manager.py +0 -0
  22. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/main.py +0 -0
  23. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/models/__init__.py +0 -0
  24. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/models/sam2_model.py +0 -0
  25. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/models/sam_model.py +0 -0
  26. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/__init__.py +0 -0
  27. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/control_panel.py +0 -0
  28. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/editable_vertex.py +0 -0
  29. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/hotkey_dialog.py +0 -0
  30. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/hoverable_pixelmap_item.py +0 -0
  31. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/hoverable_polygon_item.py +0 -0
  32. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/modes/__init__.py +0 -0
  33. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/modes/base_mode.py +0 -0
  34. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/modes/single_view_mode.py +0 -0
  35. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/numeric_table_widget_item.py +0 -0
  36. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/photo_viewer.py +0 -0
  37. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/reorderable_class_table.py +0 -0
  38. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/right_panel.py +0 -0
  39. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/widgets/__init__.py +0 -0
  40. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/widgets/adjustments_widget.py +0 -0
  41. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/widgets/border_crop_widget.py +0 -0
  42. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/widgets/channel_threshold_widget.py +0 -0
  43. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/widgets/fft_threshold_widget.py +0 -0
  44. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/widgets/fragment_threshold_widget.py +0 -0
  45. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/widgets/model_selection_widget.py +0 -0
  46. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/widgets/settings_widget.py +0 -0
  47. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/widgets/status_bar.py +0 -0
  48. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/workers/__init__.py +0 -0
  49. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/workers/image_discovery_worker.py +0 -0
  50. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/workers/multi_view_sam_init_worker.py +0 -0
  51. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/workers/multi_view_sam_update_worker.py +0 -0
  52. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/workers/sam_update_worker.py +0 -0
  53. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/ui/workers/single_view_sam_init_worker.py +0 -0
  54. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/utils/__init__.py +0 -0
  55. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/utils/custom_file_system_model.py +0 -0
  56. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/utils/fast_file_manager.py +0 -0
  57. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/utils/logger.py +0 -0
  58. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel/utils/utils.py +0 -0
  59. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel_gui.egg-info/SOURCES.txt +0 -0
  60. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel_gui.egg-info/dependency_links.txt +0 -0
  61. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel_gui.egg-info/entry_points.txt +0 -0
  62. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel_gui.egg-info/requires.txt +0 -0
  63. {lazylabel_gui-1.3.6 → lazylabel_gui-1.3.8}/src/lazylabel_gui.egg-info/top_level.txt +0 -0
@@ -0,0 +1,227 @@
1
+ Metadata-Version: 2.4
2
+ Name: lazylabel-gui
3
+ Version: 1.3.8
4
+ Summary: An image segmentation GUI for generating ML ready mask tensors and annotations.
5
+ Author-email: "Deniz N. Cakan" <deniz.n.cakan@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Deniz N. Cakan
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/dnzckn/lazylabel
29
+ Project-URL: Bug Tracker, https://github.com/dnzckn/lazylabel/issues
30
+ Classifier: Programming Language :: Python :: 3
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: OS Independent
33
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
34
+ Classifier: Environment :: X11 Applications :: Qt
35
+ Requires-Python: >=3.10
36
+ Description-Content-Type: text/markdown
37
+ License-File: LICENSE
38
+ Requires-Dist: PyQt6>=6.9.0
39
+ Requires-Dist: pyqtdarktheme==2.1.0
40
+ Requires-Dist: torch>=2.7.1
41
+ Requires-Dist: torchvision>=0.22.1
42
+ Requires-Dist: segment-anything==1.0
43
+ Requires-Dist: numpy>=2.1.2
44
+ Requires-Dist: opencv-python>=4.11.0.86
45
+ Requires-Dist: scipy>=1.15.3
46
+ Requires-Dist: requests>=2.32.4
47
+ Requires-Dist: tqdm>=4.67.1
48
+ Provides-Extra: dev
49
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
50
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
51
+ Requires-Dist: pytest-mock>=3.10.0; extra == "dev"
52
+ Requires-Dist: pytest-qt>=4.2.0; extra == "dev"
53
+ Requires-Dist: ruff>=0.8.0; extra == "dev"
54
+ Dynamic: license-file
55
+
56
+ # LazyLabel
57
+
58
+ <div align="center">
59
+ <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/logo2.png" alt="LazyLabel Logo" style="height:60px; vertical-align:middle;" />
60
+ <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/logo_black.png" alt="LazyLabel Cursive" style="height:60px; vertical-align:middle;" />
61
+ </div>
62
+
63
+ **AI-Assisted Image Segmentation for Machine Learning Dataset Preparation**
64
+
65
+ LazyLabel combines Meta's Segment Anything Model (SAM) with comprehensive manual annotation tools to accelerate the creation of pixel-perfect segmentation masks for computer vision applications.
66
+
67
+ <div align="center">
68
+ <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/gui.PNG" alt="LazyLabel Screenshot" width="800"/>
69
+ </div>
70
+
71
+ ---
72
+
73
+ ## Quick Start
74
+
75
+ ```bash
76
+ pip install lazylabel-gui
77
+ lazylabel-gui
78
+ ```
79
+
80
+ **From source:**
81
+ ```bash
82
+ git clone https://github.com/dnzckn/LazyLabel.git
83
+ cd LazyLabel
84
+ pip install -e .
85
+ lazylabel-gui
86
+ ```
87
+
88
+ **Requirements:** Python 3.10+, 8GB RAM, ~2.5GB disk space (for model weights)
89
+
90
+ ---
91
+
92
+ ## Core Features
93
+
94
+ ### AI-Powered Segmentation
95
+ LazyLabel leverages Meta's SAM for intelligent object detection:
96
+ - Single-click object segmentation
97
+ - Interactive refinement with positive/negative points
98
+ - Support for both SAM 1.0 and SAM 2.1 models
99
+ - GPU acceleration with automatic CPU fallback
100
+
101
+ ### Manual Annotation Tools
102
+ When precision matters:
103
+ - Polygon drawing with vertex-level editing
104
+ - Bounding box annotations for object detection
105
+ - Edit mode for adjusting existing segments
106
+ - Merge tool for combining related segments
107
+
108
+ ### Image Processing & Filtering
109
+ Advanced preprocessing capabilities:
110
+ - **FFT filtering**: Remove noise and enhance edges
111
+ - **Channel thresholding**: Isolate objects by color
112
+ - **Border cropping**: Define crop regions that set pixels outside the area to zero in saved outputs
113
+ - **View adjustments**: Brightness, contrast, gamma correction
114
+
115
+ ### Multi-View Mode
116
+ Process multiple images efficiently:
117
+ - Annotate up to 4 images simultaneously
118
+ - Synchronized zoom and pan across views
119
+ - Mirror annotations to all linked images
120
+
121
+ ---
122
+
123
+ ## Export Formats
124
+
125
+ ### NPZ Format (Semantic Segmentation)
126
+ One-hot encoded masks optimized for deep learning:
127
+
128
+ ```python
129
+ import numpy as np
130
+
131
+ data = np.load('image.npz')
132
+ mask = data['mask'] # Shape: (height, width, num_classes)
133
+
134
+ # Each channel represents one class
135
+ sky = mask[:, :, 0]
136
+ boats = mask[:, :, 1]
137
+ cats = mask[:, :, 2]
138
+ dogs = mask[:, :, 3]
139
+ ```
140
+
141
+ ### YOLO Format (Object Detection)
142
+ Normalized polygon coordinates for YOLO training:
143
+ ```
144
+ 0 0.234 0.456 0.289 0.478 0.301 0.523 ...
145
+ 1 0.567 0.123 0.598 0.145 0.612 0.189 ...
146
+ ```
147
+
148
+ ### Class Aliases (JSON)
149
+ Maintains consistent class naming across datasets:
150
+ ```json
151
+ {
152
+ "0": "background",
153
+ "1": "person",
154
+ "2": "vehicle"
155
+ }
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Typical Workflow
161
+
162
+ 1. **Open folder** containing your images
163
+ 2. **Click objects** to generate AI masks (mode 1)
164
+ 3. **Refine** with additional points or manual tools
165
+ 4. **Assign classes** and organize in the class table
166
+ 5. **Export** as NPZ or YOLO format
167
+
168
+ ### Advanced Preprocessing Workflow
169
+
170
+ For challenging images:
171
+ 1. Apply **FFT filtering** to reduce noise
172
+ 2. Use **channel thresholding** to isolate color ranges
173
+ 3. Enable **"Operate on View"** to pass filtered images to SAM
174
+ 4. Fine-tune with manual tools
175
+
176
+ ---
177
+
178
+ ## Advanced Features
179
+
180
+ ### Multi-View Mode
181
+ Access via the "Multi" tab to process multiple images:
182
+ - 2-view (side-by-side) or 4-view (grid) layouts
183
+ - Annotations mirror across linked views automatically
184
+ - Synchronized zoom maintains alignment
185
+
186
+ ### SAM 2.1 Support
187
+ LazyLabel supports both SAM 1.0 (default) and SAM 2.1 models. SAM 2.1 offers improved segmentation accuracy and better handling of complex boundaries.
188
+
189
+ To use SAM 2.1 models:
190
+ 1. Install the SAM 2 package:
191
+ ```bash
192
+ pip install git+https://github.com/facebookresearch/sam2.git
193
+ ```
194
+ 2. Download a SAM 2.1 model (e.g., `sam2.1_hiera_large.pt`) from the [SAM 2 repository](https://github.com/facebookresearch/sam2)
195
+ 3. Place the model file in LazyLabel's models folder:
196
+ - If installed via pip: `~/.local/share/lazylabel/models/` (or equivalent on your system)
197
+ - If running from source: `src/lazylabel/models/`
198
+ 4. Select the SAM 2.1 model from the dropdown in LazyLabel's settings
199
+
200
+ Note: SAM 1.0 models are automatically downloaded on first use.
201
+
202
+ ---
203
+
204
+ ## Key Shortcuts
205
+
206
+ | Action | Key | Description |
207
+ |--------|-----|-------------|
208
+ | AI Mode | `1` | SAM point-click segmentation |
209
+ | Draw Mode | `2` | Manual polygon creation |
210
+ | Edit Mode | `E` | Modify existing segments |
211
+ | Accept AI Segment | `Space` | Confirm AI segment suggestion |
212
+ | Save | `Enter` | Save annotations |
213
+ | Merge | `M` | Combine selected segments |
214
+ | Pan Mode | `Q` | Enter pan mode |
215
+ | Pan | `WASD` | Navigate image |
216
+ | Delete | `V`/`Delete` | Remove segments |
217
+ | Undo/Redo | `Ctrl+Z/Y` | Action history |
218
+
219
+ ---
220
+
221
+ ## Documentation
222
+
223
+ - [Usage Manual](src/lazylabel/USAGE_MANUAL.md) - Comprehensive feature guide
224
+ - [Architecture Guide](src/lazylabel/ARCHITECTURE.md) - Technical implementation details
225
+ - [GitHub Issues](https://github.com/dnzckn/LazyLabel/issues) - Report bugs or request features
226
+
227
+ ---
@@ -0,0 +1,172 @@
1
+ # LazyLabel
2
+
3
+ <div align="center">
4
+ <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/logo2.png" alt="LazyLabel Logo" style="height:60px; vertical-align:middle;" />
5
+ <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/logo_black.png" alt="LazyLabel Cursive" style="height:60px; vertical-align:middle;" />
6
+ </div>
7
+
8
+ **AI-Assisted Image Segmentation for Machine Learning Dataset Preparation**
9
+
10
+ LazyLabel combines Meta's Segment Anything Model (SAM) with comprehensive manual annotation tools to accelerate the creation of pixel-perfect segmentation masks for computer vision applications.
11
+
12
+ <div align="center">
13
+ <img src="https://raw.githubusercontent.com/dnzckn/LazyLabel/main/src/lazylabel/demo_pictures/gui.PNG" alt="LazyLabel Screenshot" width="800"/>
14
+ </div>
15
+
16
+ ---
17
+
18
+ ## Quick Start
19
+
20
+ ```bash
21
+ pip install lazylabel-gui
22
+ lazylabel-gui
23
+ ```
24
+
25
+ **From source:**
26
+ ```bash
27
+ git clone https://github.com/dnzckn/LazyLabel.git
28
+ cd LazyLabel
29
+ pip install -e .
30
+ lazylabel-gui
31
+ ```
32
+
33
+ **Requirements:** Python 3.10+, 8GB RAM, ~2.5GB disk space (for model weights)
34
+
35
+ ---
36
+
37
+ ## Core Features
38
+
39
+ ### AI-Powered Segmentation
40
+ LazyLabel leverages Meta's SAM for intelligent object detection:
41
+ - Single-click object segmentation
42
+ - Interactive refinement with positive/negative points
43
+ - Support for both SAM 1.0 and SAM 2.1 models
44
+ - GPU acceleration with automatic CPU fallback
45
+
46
+ ### Manual Annotation Tools
47
+ When precision matters:
48
+ - Polygon drawing with vertex-level editing
49
+ - Bounding box annotations for object detection
50
+ - Edit mode for adjusting existing segments
51
+ - Merge tool for combining related segments
52
+
53
+ ### Image Processing & Filtering
54
+ Advanced preprocessing capabilities:
55
+ - **FFT filtering**: Remove noise and enhance edges
56
+ - **Channel thresholding**: Isolate objects by color
57
+ - **Border cropping**: Define crop regions that set pixels outside the area to zero in saved outputs
58
+ - **View adjustments**: Brightness, contrast, gamma correction
59
+
60
+ ### Multi-View Mode
61
+ Process multiple images efficiently:
62
+ - Annotate up to 4 images simultaneously
63
+ - Synchronized zoom and pan across views
64
+ - Mirror annotations to all linked images
65
+
66
+ ---
67
+
68
+ ## Export Formats
69
+
70
+ ### NPZ Format (Semantic Segmentation)
71
+ One-hot encoded masks optimized for deep learning:
72
+
73
+ ```python
74
+ import numpy as np
75
+
76
+ data = np.load('image.npz')
77
+ mask = data['mask'] # Shape: (height, width, num_classes)
78
+
79
+ # Each channel represents one class
80
+ sky = mask[:, :, 0]
81
+ boats = mask[:, :, 1]
82
+ cats = mask[:, :, 2]
83
+ dogs = mask[:, :, 3]
84
+ ```
85
+
86
+ ### YOLO Format (Object Detection)
87
+ Normalized polygon coordinates for YOLO training:
88
+ ```
89
+ 0 0.234 0.456 0.289 0.478 0.301 0.523 ...
90
+ 1 0.567 0.123 0.598 0.145 0.612 0.189 ...
91
+ ```
92
+
93
+ ### Class Aliases (JSON)
94
+ Maintains consistent class naming across datasets:
95
+ ```json
96
+ {
97
+ "0": "background",
98
+ "1": "person",
99
+ "2": "vehicle"
100
+ }
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Typical Workflow
106
+
107
+ 1. **Open folder** containing your images
108
+ 2. **Click objects** to generate AI masks (mode 1)
109
+ 3. **Refine** with additional points or manual tools
110
+ 4. **Assign classes** and organize in the class table
111
+ 5. **Export** as NPZ or YOLO format
112
+
113
+ ### Advanced Preprocessing Workflow
114
+
115
+ For challenging images:
116
+ 1. Apply **FFT filtering** to reduce noise
117
+ 2. Use **channel thresholding** to isolate color ranges
118
+ 3. Enable **"Operate on View"** to pass filtered images to SAM
119
+ 4. Fine-tune with manual tools
120
+
121
+ ---
122
+
123
+ ## Advanced Features
124
+
125
+ ### Multi-View Mode
126
+ Access via the "Multi" tab to process multiple images:
127
+ - 2-view (side-by-side) or 4-view (grid) layouts
128
+ - Annotations mirror across linked views automatically
129
+ - Synchronized zoom maintains alignment
130
+
131
+ ### SAM 2.1 Support
132
+ LazyLabel supports both SAM 1.0 (default) and SAM 2.1 models. SAM 2.1 offers improved segmentation accuracy and better handling of complex boundaries.
133
+
134
+ To use SAM 2.1 models:
135
+ 1. Install the SAM 2 package:
136
+ ```bash
137
+ pip install git+https://github.com/facebookresearch/sam2.git
138
+ ```
139
+ 2. Download a SAM 2.1 model (e.g., `sam2.1_hiera_large.pt`) from the [SAM 2 repository](https://github.com/facebookresearch/sam2)
140
+ 3. Place the model file in LazyLabel's models folder:
141
+ - If installed via pip: `~/.local/share/lazylabel/models/` (or equivalent on your system)
142
+ - If running from source: `src/lazylabel/models/`
143
+ 4. Select the SAM 2.1 model from the dropdown in LazyLabel's settings
144
+
145
+ Note: SAM 1.0 models are automatically downloaded on first use.
146
+
147
+ ---
148
+
149
+ ## Key Shortcuts
150
+
151
+ | Action | Key | Description |
152
+ |--------|-----|-------------|
153
+ | AI Mode | `1` | SAM point-click segmentation |
154
+ | Draw Mode | `2` | Manual polygon creation |
155
+ | Edit Mode | `E` | Modify existing segments |
156
+ | Accept AI Segment | `Space` | Confirm AI segment suggestion |
157
+ | Save | `Enter` | Save annotations |
158
+ | Merge | `M` | Combine selected segments |
159
+ | Pan Mode | `Q` | Enter pan mode |
160
+ | Pan | `WASD` | Navigate image |
161
+ | Delete | `V`/`Delete` | Remove segments |
162
+ | Undo/Redo | `Ctrl+Z/Y` | Action history |
163
+
164
+ ---
165
+
166
+ ## Documentation
167
+
168
+ - [Usage Manual](src/lazylabel/USAGE_MANUAL.md) - Comprehensive feature guide
169
+ - [Architecture Guide](src/lazylabel/ARCHITECTURE.md) - Technical implementation details
170
+ - [GitHub Issues](https://github.com/dnzckn/LazyLabel/issues) - Report bugs or request features
171
+
172
+ ---
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lazylabel-gui"
7
- version = "1.3.6"
7
+ version = "1.3.8"
8
8
  authors = [
9
9
  { name="Deniz N. Cakan", email="deniz.n.cakan@gmail.com" },
10
10
  ]
@@ -1582,6 +1582,19 @@ class MainWindow(QMainWindow):
1582
1582
  # Reset AI mode state
1583
1583
  self.ai_click_start_pos = None
1584
1584
 
1585
+ # Reset all link states to linked when navigating to new images
1586
+ config = self._get_multi_view_config()
1587
+ num_viewers = config["num_viewers"]
1588
+ self.multi_view_linked = [True] * num_viewers
1589
+
1590
+ # Reset unlink button appearances to default linked state
1591
+ if hasattr(self, "multi_view_unlink_buttons"):
1592
+ for i, button in enumerate(self.multi_view_unlink_buttons):
1593
+ if i < num_viewers:
1594
+ button.setText("X")
1595
+ button.setToolTip("Unlink this image from mirroring")
1596
+ button.setStyleSheet("")
1597
+
1585
1598
  # Update UI lists to reflect cleared state
1586
1599
  self._update_all_lists()
1587
1600
 
@@ -2829,7 +2842,8 @@ class MainWindow(QMainWindow):
2829
2842
  for i in range(num_viewers):
2830
2843
  image_path = self.multi_view_images[i]
2831
2844
 
2832
- if not image_path:
2845
+ # Skip if no image or if this viewer is unlinked
2846
+ if not image_path or not self.multi_view_linked[i]:
2833
2847
  continue
2834
2848
 
2835
2849
  # Filter segments for this viewer
@@ -6279,22 +6293,23 @@ class MainWindow(QMainWindow):
6279
6293
  )
6280
6294
 
6281
6295
  def _start_sequential_multi_view_sam_loading(self):
6282
- """Start loading images into SAM models sequentially to avoid resource conflicts."""
6296
+ """Start loading images into SAM models in parallel for faster processing."""
6283
6297
  # Check if any loading is already in progress to prevent duplicate workers
6284
6298
  any_updating = any(self.multi_view_models_updating)
6285
6299
  if any_updating:
6286
- # Sequential loading already in progress, don't start another
6300
+ # Loading already in progress, don't start another
6287
6301
  updating_indices = [
6288
6302
  i
6289
6303
  for i, updating in enumerate(self.multi_view_models_updating)
6290
6304
  if updating
6291
6305
  ]
6292
6306
  logger.debug(
6293
- f"Sequential loading already in progress for viewers: {updating_indices}"
6307
+ f"Parallel loading already in progress for viewers: {updating_indices}"
6294
6308
  )
6295
6309
  return
6296
6310
 
6297
- # Find the next dirty model that needs updating
6311
+ # Find all dirty models that need updating and start them in parallel
6312
+ models_to_update = []
6298
6313
  for i in range(len(self.multi_view_models)):
6299
6314
  if (
6300
6315
  self.multi_view_images[i]
@@ -6302,15 +6317,24 @@ class MainWindow(QMainWindow):
6302
6317
  and self.multi_view_models_dirty[i]
6303
6318
  and not self.multi_view_models_updating[i]
6304
6319
  ):
6305
- # Start updating this model
6306
- self._ensure_multi_view_sam_updated(i)
6307
- return
6320
+ models_to_update.append(i)
6308
6321
 
6309
- # If no more models to update, we're done
6310
- if self._multi_view_loading_step >= self._multi_view_total_steps:
6311
- self._show_success_notification(
6312
- "AI models ready for prompting", duration=3000
6322
+ if models_to_update:
6323
+ logger.debug(f"Starting parallel loading for viewers: {models_to_update}")
6324
+ # Show notification about parallel loading
6325
+ self._show_notification(
6326
+ f"Loading embeddings for {len(models_to_update)} images in parallel...",
6327
+ duration=0,
6313
6328
  )
6329
+ # Start all workers in parallel
6330
+ for i in models_to_update:
6331
+ self._ensure_multi_view_sam_updated(i)
6332
+ else:
6333
+ # If no more models to update and none are running, we're done
6334
+ if not any(self.multi_view_models_updating):
6335
+ self._show_success_notification(
6336
+ "AI models ready for prompting", duration=3000
6337
+ )
6314
6338
 
6315
6339
  def _on_multi_view_init_error(self, error_message):
6316
6340
  """Handle multi-view model initialization error."""
@@ -6452,14 +6476,8 @@ class MainWindow(QMainWindow):
6452
6476
 
6453
6477
  logger.debug(f"Starting SAM image loading worker for viewer {viewer_index + 1}")
6454
6478
 
6455
- # Show enumerated progress notification
6456
- image_name = Path(image_path).name if image_path else "unknown"
6457
- config = self._get_multi_view_config()
6458
- num_viewers = config["num_viewers"]
6459
- self._show_notification(
6460
- f"Computing embeddings for image {viewer_index + 1}/{num_viewers}: {image_name}",
6461
- duration=0, # Persistent until completion
6462
- )
6479
+ # Individual notifications are now handled by the parallel loading start
6480
+ # No need for per-viewer notifications when loading in parallel
6463
6481
 
6464
6482
  # Get current modified image if operate_on_view is enabled
6465
6483
  current_image = None
@@ -6547,8 +6565,16 @@ class MainWindow(QMainWindow):
6547
6565
  if hasattr(self, "_multi_view_loading_step"):
6548
6566
  self._multi_view_loading_step += 1
6549
6567
 
6550
- # Continue sequential loading
6551
- self._start_sequential_multi_view_sam_loading()
6568
+ # Check if all models are done (either loaded, failed, or timed out)
6569
+ if not any(self.multi_view_models_updating) and not any(
6570
+ self.multi_view_models_dirty
6571
+ ):
6572
+ # All models processed
6573
+ self._show_success_notification("AI model loading complete", duration=3000)
6574
+ elif not any(self.multi_view_models_updating):
6575
+ # No models are currently updating but some may still be dirty
6576
+ # Try to load remaining models
6577
+ self._start_sequential_multi_view_sam_loading()
6552
6578
 
6553
6579
  def _on_multi_view_sam_update_finished(self, viewer_index):
6554
6580
  """Handle completion of multi-view SAM model update."""
@@ -6582,14 +6608,19 @@ class MainWindow(QMainWindow):
6582
6608
  # Update progress
6583
6609
  if hasattr(self, "_multi_view_loading_step"):
6584
6610
  self._multi_view_loading_step += 1
6585
- if self._multi_view_loading_step < self._multi_view_total_steps:
6586
- self._show_notification(
6587
- f"Loading image {self._multi_view_loading_step + 1} of {self._multi_view_total_steps}...",
6588
- duration=0,
6589
- )
6590
6611
 
6591
- # Continue sequential loading of the next SAM model
6592
- self._start_sequential_multi_view_sam_loading()
6612
+ # Check if all models are done loading
6613
+ if not any(self.multi_view_models_updating) and not any(
6614
+ self.multi_view_models_dirty
6615
+ ):
6616
+ # All models loaded successfully
6617
+ self._show_success_notification(
6618
+ "AI models ready for prompting", duration=3000
6619
+ )
6620
+ elif not any(self.multi_view_models_updating):
6621
+ # No models are currently updating but some may still be dirty
6622
+ # This can happen if there was an error, try to load remaining models
6623
+ self._start_sequential_multi_view_sam_loading()
6593
6624
 
6594
6625
  def _on_multi_view_sam_update_error(self, viewer_index, error_message):
6595
6626
  """Handle multi-view SAM model update error."""
@@ -6625,8 +6656,16 @@ class MainWindow(QMainWindow):
6625
6656
  if hasattr(self, "_multi_view_loading_step"):
6626
6657
  self._multi_view_loading_step += 1
6627
6658
 
6628
- # Continue sequential loading even if this model failed
6629
- self._start_sequential_multi_view_sam_loading()
6659
+ # Check if all models are done (either loaded or failed)
6660
+ if not any(self.multi_view_models_updating) and not any(
6661
+ self.multi_view_models_dirty
6662
+ ):
6663
+ # All models processed (some may have failed)
6664
+ self._show_success_notification("AI model loading complete", duration=3000)
6665
+ elif not any(self.multi_view_models_updating):
6666
+ # No models are currently updating but some may still be dirty
6667
+ # Try to load remaining models
6668
+ self._start_sequential_multi_view_sam_loading()
6630
6669
 
6631
6670
  def _cleanup_multi_view_models(self):
6632
6671
  """Clean up multi-view model instances."""
@@ -6802,21 +6841,20 @@ class MainWindow(QMainWindow):
6802
6841
 
6803
6842
  def _toggle_multi_view_link(self, image_index):
6804
6843
  """Toggle the link status for a specific image in multi-view."""
6805
- if 0 <= image_index < 2:
6806
- self.multi_view_linked[image_index] = not self.multi_view_linked[
6807
- image_index
6808
- ]
6844
+ # Check bounds and link status - only allow unlinking when currently linked
6845
+ if (
6846
+ 0 <= image_index < len(self.multi_view_linked)
6847
+ and self.multi_view_linked[image_index]
6848
+ ):
6849
+ # Currently linked - allow unlinking
6850
+ self.multi_view_linked[image_index] = False
6809
6851
 
6810
- # Update button appearance
6852
+ # Update button appearance to show unlinked state
6811
6853
  button = self.multi_view_unlink_buttons[image_index]
6812
- if self.multi_view_linked[image_index]:
6813
- button.setText("X")
6814
- button.setToolTip("Unlink this image from mirroring")
6815
- button.setStyleSheet("")
6816
- else:
6817
- button.setText("↪")
6818
- button.setToolTip("Link this image to mirroring")
6819
- button.setStyleSheet("background-color: #ffcccc;")
6854
+ button.setText("↪")
6855
+ button.setToolTip("This image is unlinked from mirroring")
6856
+ button.setStyleSheet("background-color: #ff4444; color: white;")
6857
+ # If already unlinked or invalid index, do nothing (prevent re-linking)
6820
6858
 
6821
6859
  def _start_background_image_discovery(self):
6822
6860
  """Start background discovery of all image files."""
@@ -369,12 +369,20 @@ class MultiViewModeHandler(BaseModeHandler):
369
369
 
370
370
  paired_segment = {"type": "Polygon", "views": {}}
371
371
 
372
- # Add view data for all viewers with same coordinates
372
+ # Add view data for current viewer and mirror to linked viewers only
373
+ paired_segment["views"][viewer_index] = view_data
374
+
375
+ # Mirror to other viewers only if they are linked
373
376
  for viewer_idx in range(num_viewers):
374
- paired_segment["views"][viewer_idx] = {
375
- "vertices": view_data["vertices"].copy(),
376
- "mask": None,
377
- }
377
+ if (
378
+ viewer_idx != viewer_index
379
+ and self.main_window.multi_view_linked[viewer_idx]
380
+ and self.main_window.multi_view_images[viewer_idx] is not None
381
+ ):
382
+ paired_segment["views"][viewer_idx] = {
383
+ "vertices": view_data["vertices"].copy(),
384
+ "mask": None,
385
+ }
378
386
 
379
387
  # Add to segment manager
380
388
  self.main_window.segment_manager.add_segment(paired_segment)
@@ -783,9 +791,13 @@ class MultiViewModeHandler(BaseModeHandler):
783
791
  # Add the current viewer's data
784
792
  paired_segment["views"][viewer_index] = view_data
785
793
 
786
- # Mirror to all other viewers with same coordinates (they should align between linked images)
794
+ # Mirror to all other viewers with same coordinates (only if they are linked)
787
795
  for other_viewer_index in range(num_viewers):
788
- if other_viewer_index != viewer_index:
796
+ if (
797
+ other_viewer_index != viewer_index
798
+ and self.main_window.multi_view_linked[other_viewer_index]
799
+ and self.main_window.multi_view_images[other_viewer_index] is not None
800
+ ):
789
801
  mirrored_view_data = {
790
802
  "vertices": view_data[
791
803
  "vertices"
@@ -807,11 +819,27 @@ class MultiViewModeHandler(BaseModeHandler):
807
819
 
808
820
  # Update UI
809
821
  self.main_window._update_all_lists()
810
- viewer_count_text = "all viewers" if num_viewers > 2 else "both viewers"
811
- self.main_window._show_notification(
812
- f"Polygon created and mirrored to {viewer_count_text}."
822
+
823
+ # Count linked viewers (excluding the source viewer)
824
+ linked_viewers_count = sum(
825
+ 1
826
+ for i in range(num_viewers)
827
+ if i != viewer_index
828
+ and self.main_window.multi_view_linked[i]
829
+ and self.main_window.multi_view_images[i] is not None
813
830
  )
814
831
 
832
+ if linked_viewers_count == 0:
833
+ viewer_count_text = "created (no linked viewers to mirror to)"
834
+ elif linked_viewers_count == 1:
835
+ viewer_count_text = "created and mirrored to 1 linked viewer"
836
+ else:
837
+ viewer_count_text = (
838
+ f"created and mirrored to {linked_viewers_count} linked viewers"
839
+ )
840
+
841
+ self.main_window._show_notification(f"Polygon {viewer_count_text}.")
842
+
815
843
  # Clear polygon state for this viewer
816
844
  self._clear_multi_view_polygon(viewer_index)
817
845