pyclickimage 2.2.1__tar.gz → 2.4.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.
Files changed (43) hide show
  1. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/.bumpver.toml +1 -1
  2. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/PKG-INFO +1 -1
  3. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage/__main__.py +1 -1
  4. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage/__version__.py +1 -1
  5. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage/click_image_app.py +481 -261
  6. pyclickimage-2.4.0/pyclickimage/resources/app.png +0 -0
  7. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage.egg-info/PKG-INFO +1 -1
  8. pyclickimage-2.2.1/pyclickimage/resources/app.png +0 -0
  9. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/.github/workflows/publish.yml +0 -0
  10. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/.github/workflows/sphinx.yml +0 -0
  11. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/.gitignore +0 -0
  12. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/.gitlab-ci.yml +0 -0
  13. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/COPYING +0 -0
  14. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/LICENSE +0 -0
  15. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/Makefile +0 -0
  16. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/README.md +0 -0
  17. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/api.rst +0 -0
  18. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/api_doc/click_image_app.rst +0 -0
  19. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/api_doc/click_manager.rst +0 -0
  20. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/api_doc/image_viewer.rst +0 -0
  21. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/api_doc/run.rst +0 -0
  22. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/conf.py +0 -0
  23. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/index.rst +0 -0
  24. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/installation.rst +0 -0
  25. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/usage.rst +0 -0
  26. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/usage_doc/extracting_the_clicks.rst +0 -0
  27. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/usage_doc/running_the_GUI.rst +0 -0
  28. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/docs/source/usage_doc/using_the_GUI.rst +0 -0
  29. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/examples/clicks.csv +0 -0
  30. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/examples/example.png +0 -0
  31. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/examples/example.py +0 -0
  32. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage/__init__.py +0 -0
  33. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage/click_manager.py +0 -0
  34. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage/image_viewer.py +0 -0
  35. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage/resources/__init__.py +0 -0
  36. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage/run.py +0 -0
  37. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage.egg-info/SOURCES.txt +0 -0
  38. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage.egg-info/dependency_links.txt +0 -0
  39. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage.egg-info/entry_points.txt +0 -0
  40. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage.egg-info/requires.txt +0 -0
  41. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyclickimage.egg-info/top_level.txt +0 -0
  42. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/pyproject.toml +0 -0
  43. {pyclickimage-2.2.1 → pyclickimage-2.4.0}/setup.cfg +0 -0
@@ -1,5 +1,5 @@
1
1
  [bumpver]
2
- current_version = "2.2.1"
2
+ current_version = "2.4.0"
3
3
  version_pattern = "MAJOR.MINOR.PATCH"
4
4
  default_level = "patch"
5
5
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyclickimage
3
- Version: 2.2.1
3
+ Version: 2.4.0
4
4
  Summary: Python library to select points on a image [pyqt5 GUI]
5
5
  Author-email: Artezaru <artezaru.github@proton.me>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -64,7 +64,7 @@ def __main_gui__() -> None:
64
64
  args = parser.parse_args()
65
65
 
66
66
  if args.image is not None:
67
- image = cv2.imread(args.image)
67
+ image = cv2.imread(args.image, cv2.IMREAD_UNCHANGED)
68
68
  else:
69
69
  image = None
70
70
 
@@ -16,4 +16,4 @@ You should have received a copy of the GNU General Public License
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  """
18
18
 
19
- __version__ = "2.2.1"
19
+ __version__ = "2.4.0"
@@ -58,12 +58,27 @@ class ClickImageApp(QtWidgets.QMainWindow):
58
58
  self._colormap_has_changed = True
59
59
  self._is_empty_image = True
60
60
 
61
+ # --------------------------
62
+ # States
63
+ # --------------------------
64
+ self.alpha = 1.0
65
+ self.beta_pc = 0
66
+ self.display_min_pc = 0
67
+ self.display_max_pc = 100
68
+ self.show_clicks = True
69
+ self.marker_color = QtGui.QColor(255, 0, 0)
70
+ self.marker_size = 8
71
+
61
72
  # -------------------------
62
73
  # Core components
63
74
  # -------------------------
64
75
  self.click_manager = ClickManager(precision_mode="float")
65
76
  self.viewer = ImageViewer(half_shift=True)
66
77
 
78
+ self.viewer.auto_marker = False # Don't draw directly
79
+ self.viewer.left_click_signal.connect(self._process_left_click)
80
+ self.viewer.right_click_signal.connect(self._process_right_click)
81
+
67
82
  # -------------------------
68
83
  # Logging
69
84
  # -------------------------
@@ -80,24 +95,32 @@ class ClickImageApp(QtWidgets.QMainWindow):
80
95
  # -------------------------
81
96
  self.set_image(image)
82
97
 
83
- # -------------------------
84
- # Markers
85
- # -------------------------
86
- self.marker_color = QtGui.QColor(255, 0, 0)
87
- self.marker_size = 8
88
-
89
98
  # -------------------------
90
99
  # UI
91
100
  # -------------------------
92
- self._init_ui()
101
+ self.central = QtWidgets.QWidget()
102
+ self.setCentralWidget(self.central)
103
+
104
+ self.layout = QtWidgets.QHBoxLayout()
105
+ self.central.setLayout(self.layout)
106
+
107
+ quit_action = QtWidgets.QAction("Quit", self)
108
+ quit_action.setShortcut("Ctrl+Q")
109
+ quit_action.triggered.connect(self.close)
110
+
111
+ self.addAction(quit_action)
112
+ self.layout.addWidget(self.viewer)
113
+ self.side = QtWidgets.QVBoxLayout()
114
+ self.side_widget = QtWidgets.QWidget()
115
+ self.side_widget.setFixedWidth(340)
116
+ self.side_widget.setLayout(self.side)
117
+
118
+ self._init_left_panel()
119
+ self._init_toolbar()
93
120
 
94
121
  # -------------------------
95
122
  # Signals
96
123
  # -------------------------
97
- self.viewer.auto_marker = False # Don't draw directly
98
- self.viewer.left_click_signal.connect(self._process_left_click)
99
- self.viewer.right_click_signal.connect(self._process_right_click)
100
-
101
124
  self.initialization_done = True
102
125
  self._append_log("Application ready.")
103
126
  if self.output_path is not None:
@@ -108,72 +131,29 @@ class ClickImageApp(QtWidgets.QMainWindow):
108
131
  # ============================================================
109
132
  # UI
110
133
  # ============================================================
111
-
112
- def _init_ui(self):
113
- r"""
114
- Build full interface layout.
115
- """
116
-
117
- central = QtWidgets.QWidget()
118
- self.setCentralWidget(central)
119
-
120
- layout = QtWidgets.QHBoxLayout()
121
- central.setLayout(layout)
122
-
123
- quit_action = QtWidgets.QAction("Quit", self)
124
- quit_action.setShortcut("Ctrl+Q")
125
- quit_action.triggered.connect(self.close)
126
-
127
- self.addAction(quit_action)
128
-
129
- # -------------------------
130
- # Viewer
131
- # -------------------------
132
- layout.addWidget(self.viewer)
133
-
134
- # -------------------------
135
- # Side panel
136
- # -------------------------
137
- side = QtWidgets.QVBoxLayout()
138
- side_widget = QtWidgets.QWidget()
139
- side_widget.setFixedWidth(340)
140
- side_widget.setLayout(side)
141
-
134
+ def _init_left_panel(self):
135
+ r"""Build the left interface"""
136
+ # ============================================================
137
+ # Scroll Area
138
+ # ============================================================
142
139
  scroll = QtWidgets.QScrollArea()
143
140
  scroll.setWidgetResizable(True)
144
141
  scroll.setFixedWidth(360)
145
- scroll.setWidget(side_widget)
142
+ scroll.setWidget(self.side_widget)
146
143
 
147
- layout.addWidget(scroll)
144
+ self.layout.addWidget(scroll)
148
145
 
149
146
  # ============================================================
150
147
  # Load Image
151
148
  # ============================================================
152
- self.load_btn = QtWidgets.QPushButton("Load Image (Ctrl+O)")
153
- self.load_btn.clicked.connect(self.on_load_image)
154
- self.load_btn.setShortcut("Ctrl+O")
155
- side.addWidget(self.load_btn)
156
-
157
- row = QtWidgets.QHBoxLayout()
158
-
159
- self.colormap_selector = QtWidgets.QComboBox()
160
- self.colormap_selector.addItem("Default")
161
- self.colormap_selector.addItem("Gray")
162
- self.colormap_selector.addItem("Hot")
163
- self.colormap_selector.addItem("Jet")
164
- self.colormap_selector.addItem("Rainbow")
165
- self.colormap_selector.addItem("Cool")
166
- self.colormap_selector.addItem("Spring")
167
- self.colormap_selector.setCurrentIndex(0) # Default to "Default"
168
- self.colormap_selector.currentIndexChanged.connect(self.on_colormap_changed)
169
- row.addWidget(QtWidgets.QLabel("Image Colormap:"))
170
- row.addWidget(self.colormap_selector)
149
+ self.load_image_btn = QtWidgets.QPushButton("Load Image (Ctrl+O)")
150
+ self.load_image_btn.clicked.connect(self.on_load_image)
151
+ self.load_image_btn.setShortcut("Ctrl+O")
152
+ self.side.addWidget(self.load_image_btn)
171
153
 
172
- side.addLayout(row)
173
-
174
- self.loadc_btn = QtWidgets.QPushButton("Load Clicks")
175
- self.loadc_btn.clicked.connect(self.on_load_clicks)
176
- side.addWidget(self.loadc_btn)
154
+ self.load_click_btn = QtWidgets.QPushButton("Load Clicks")
155
+ self.load_click_btn.clicked.connect(self.on_load_clicks)
156
+ self.side.addWidget(self.load_click_btn)
177
157
 
178
158
  # ============================================================
179
159
  # Precision mode
@@ -182,21 +162,20 @@ class ClickImageApp(QtWidgets.QMainWindow):
182
162
  self.precision_checkbox.setChecked(False)
183
163
  self.precision_checkbox.stateChanged.connect(self.on_precision_changed)
184
164
  self.precision_checkbox.setShortcut("Ctrl+I")
185
- side.addWidget(self.precision_checkbox)
186
-
165
+ self.side.addWidget(self.precision_checkbox)
187
166
  self.half_shift_checkbox = QtWidgets.QCheckBox(
188
167
  "Half-Shift: (0,0) on the pixel center."
189
168
  )
190
169
  self.half_shift_checkbox.setChecked(True)
191
170
  self.half_shift_checkbox.stateChanged.connect(self.on_half_shift_changed)
192
- side.addWidget(self.half_shift_checkbox)
171
+ self.side.addWidget(self.half_shift_checkbox)
193
172
 
194
173
  self.display_clicks_checkbox = QtWidgets.QCheckBox("Display clicks")
195
174
  self.display_clicks_checkbox.setChecked(True)
196
175
  self.display_clicks_checkbox.stateChanged.connect(
197
176
  self.on_display_clicks_changed
198
177
  )
199
- side.addWidget(self.display_clicks_checkbox)
178
+ self.side.addWidget(self.display_clicks_checkbox)
200
179
 
201
180
  # ============================================================
202
181
  # Group selector
@@ -211,7 +190,7 @@ class ClickImageApp(QtWidgets.QMainWindow):
211
190
 
212
191
  row.addWidget(self.group_selector)
213
192
 
214
- side.addLayout(row)
193
+ self.side.addLayout(row)
215
194
 
216
195
  # -------------------------
217
196
  # Group management buttons
@@ -237,7 +216,7 @@ class ClickImageApp(QtWidgets.QMainWindow):
237
216
  row.addWidget(self.rename_group_btn)
238
217
  row.addWidget(self.delete_group_btn)
239
218
 
240
- side.addLayout(row)
219
+ self.side.addLayout(row)
241
220
 
242
221
  # --------------------------
243
222
  # Click Management
@@ -255,7 +234,7 @@ class ClickImageApp(QtWidgets.QMainWindow):
255
234
  row.addWidget(self.undo_btn)
256
235
  row.addWidget(self.clear_btn)
257
236
 
258
- side.addLayout(row)
237
+ self.side.addLayout(row)
259
238
 
260
239
  # ============================================================
261
240
  # Table
@@ -263,7 +242,7 @@ class ClickImageApp(QtWidgets.QMainWindow):
263
242
  self.table = QtWidgets.QTableWidget(0, 3)
264
243
  self.table.setHorizontalHeaderLabels(["Index", "X", "Y"])
265
244
  self.table.setMinimumHeight(180)
266
- side.addWidget(self.table)
245
+ self.side.addWidget(self.table)
267
246
 
268
247
  # ============================================================
269
248
  # Save button
@@ -280,81 +259,216 @@ class ClickImageApp(QtWidgets.QMainWindow):
280
259
  self.save_as_btn.setShortcut("Ctrl+Shift+S")
281
260
  row.addWidget(self.save_as_btn)
282
261
 
283
- side.addLayout(row)
262
+ self.side.addLayout(row)
284
263
 
285
264
  # ============================================================
286
265
  # Log
287
266
  # ============================================================
288
- side.addWidget(self.log_text_edit)
267
+ self.side.addWidget(self.log_text_edit)
289
268
 
269
+ def _init_toolbar(self):
270
+ r"""Build toolbar"""
290
271
  # ============================================================
291
272
  # Toolbar
292
273
  # ============================================================
274
+
293
275
  toolbar = QtWidgets.QToolBar()
294
276
  self.addToolBar(toolbar)
295
277
 
278
+ # ============================================================
279
+ # RESET
280
+ # ============================================================
281
+
296
282
  toolbar.addAction("Reset View", self.viewer.reset_view)
297
283
 
298
284
  toolbar.addSeparator()
299
285
 
300
286
  # ============================================================
301
- # Crosser color (button + preview)
287
+ # IMAGE
302
288
  # ============================================================
303
- crosshair_color_widget = QtWidgets.QWidget()
304
- crosshair_color_layout = QtWidgets.QHBoxLayout()
305
- crosshair_color_layout.setContentsMargins(0, 0, 0, 0)
306
289
 
307
- self.crosshair_color_btn = QtWidgets.QPushButton("Color")
308
- self.crosshair_color_btn.clicked.connect(self.crosshair_choose_color)
290
+ image_btn = QtWidgets.QToolButton()
291
+ image_btn.setText("⚙ Image")
292
+ image_btn.setPopupMode(QtWidgets.QToolButton.InstantPopup)
309
293
 
310
- self.crosshair_color_preview = QtWidgets.QLabel(" ")
311
- self.crosshair_color_preview.setStyleSheet(
312
- "background-color: rgb(255,0,0); border: 1px solid black;"
294
+ image_menu = QtWidgets.QMenu(self)
295
+
296
+ image_panel = QtWidgets.QWidget()
297
+ image_layout = QtWidgets.QFormLayout(image_panel)
298
+
299
+ # -------------------------
300
+ # Colormap
301
+ # -------------------------
302
+
303
+ self.colormap_selector = QtWidgets.QComboBox()
304
+ self.colormap_selector.addItems(
305
+ [
306
+ "Default",
307
+ "Gray",
308
+ "Hot",
309
+ "Jet",
310
+ "Rainbow",
311
+ "Cool",
312
+ "Spring",
313
+ ]
313
314
  )
314
315
 
315
- crosshair_color_layout.addWidget(QtWidgets.QLabel("Crosshair"))
316
- crosshair_color_layout.addWidget(self.crosshair_color_btn)
317
- crosshair_color_layout.addWidget(self.crosshair_color_preview)
316
+ self.colormap_selector.currentIndexChanged.connect(self.on_colormap_changed)
318
317
 
319
- crosshair_color_widget.setLayout(crosshair_color_layout)
318
+ image_layout.addRow("Colormap", self.colormap_selector)
320
319
 
321
- toolbar.addWidget(crosshair_color_widget)
320
+ # -------------------------
321
+ # Alpha
322
+ # -------------------------
322
323
 
323
- toolbar.addSeparator()
324
+ self.alpha_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
325
+ self.alpha_slider.setRange(1, 500)
326
+ self.alpha_slider.setValue(100)
327
+
328
+ self.alpha_value_label = QtWidgets.QLabel("1.00")
329
+
330
+ self.alpha_slider.valueChanged.connect(self.on_contrast_changed)
331
+
332
+ alpha_widget = QtWidgets.QWidget()
333
+ alpha_layout = QtWidgets.QHBoxLayout(alpha_widget)
334
+ alpha_layout.setContentsMargins(0, 0, 0, 0)
335
+
336
+ alpha_layout.addWidget(self.alpha_slider)
337
+ alpha_layout.addWidget(self.alpha_value_label)
338
+
339
+ image_layout.addRow("Alpha", alpha_widget)
340
+
341
+ # -------------------------
342
+ # Beta
343
+ # -------------------------
344
+
345
+ self.beta_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
346
+ self.beta_slider.setRange(-100, 100)
347
+ self.beta_slider.setValue(0)
348
+
349
+ self.beta_value_label = QtWidgets.QLabel("0%")
350
+
351
+ self.beta_slider.valueChanged.connect(self.on_contrast_changed)
352
+
353
+ beta_widget = QtWidgets.QWidget()
354
+ beta_layout = QtWidgets.QHBoxLayout(beta_widget)
355
+ beta_layout.setContentsMargins(0, 0, 0, 0)
356
+
357
+ beta_layout.addWidget(self.beta_slider)
358
+ beta_layout.addWidget(self.beta_value_label)
359
+
360
+ image_layout.addRow("Beta", beta_widget)
361
+
362
+ # -------------------------
363
+ # Min
364
+ # -------------------------
365
+
366
+ self.min_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
367
+ self.min_slider.setRange(0, 100)
368
+ self.min_slider.setValue(0)
369
+
370
+ self.min_value_label = QtWidgets.QLabel("0%")
371
+
372
+ self.min_slider.valueChanged.connect(self.on_contrast_changed)
373
+
374
+ min_widget = QtWidgets.QWidget()
375
+ min_layout = QtWidgets.QHBoxLayout(min_widget)
376
+ min_layout.setContentsMargins(0, 0, 0, 0)
377
+
378
+ min_layout.addWidget(self.min_slider)
379
+ min_layout.addWidget(self.min_value_label)
380
+
381
+ image_layout.addRow("Min", min_widget)
382
+
383
+ # -------------------------
384
+ # Max
385
+ # -------------------------
386
+
387
+ self.max_slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
388
+ self.max_slider.setRange(0, 100)
389
+ self.max_slider.setValue(100)
390
+
391
+ self.max_value_label = QtWidgets.QLabel("100%")
392
+
393
+ self.max_slider.valueChanged.connect(self.on_contrast_changed)
394
+
395
+ max_widget = QtWidgets.QWidget()
396
+ max_layout = QtWidgets.QHBoxLayout(max_widget)
397
+ max_layout.setContentsMargins(0, 0, 0, 0)
398
+
399
+ max_layout.addWidget(self.max_slider)
400
+ max_layout.addWidget(self.max_value_label)
401
+
402
+ image_layout.addRow("Max", max_widget)
403
+
404
+ # -------------------------
405
+ # Reset contrast
406
+ # -------------------------
407
+
408
+ self.reset_contrast_btn = QtWidgets.QPushButton("Reset")
409
+
410
+ self.reset_contrast_btn.clicked.connect(self.on_reset_contrast)
411
+
412
+ image_layout.addRow(self.reset_contrast_btn)
413
+
414
+ image_action = QtWidgets.QWidgetAction(image_menu)
415
+ image_action.setDefaultWidget(image_panel)
416
+
417
+ image_menu.addAction(image_action)
418
+
419
+ image_btn.setMenu(image_menu)
420
+
421
+ toolbar.addWidget(image_btn)
324
422
 
325
423
  # ============================================================
326
- # Marker color (button + preview)
424
+ # CLICKS
327
425
  # ============================================================
328
- color_widget = QtWidgets.QWidget()
329
- color_layout = QtWidgets.QHBoxLayout()
330
- color_layout.setContentsMargins(0, 0, 0, 0)
426
+
427
+ toolbar.addSeparator()
428
+
429
+ clicks_btn = QtWidgets.QToolButton()
430
+ clicks_btn.setText("⚙ Clicks")
431
+ clicks_btn.setPopupMode(QtWidgets.QToolButton.InstantPopup)
432
+
433
+ clicks_menu = QtWidgets.QMenu(self)
434
+
435
+ clicks_panel = QtWidgets.QWidget()
436
+ clicks_layout = QtWidgets.QFormLayout(clicks_panel)
437
+
438
+ self.display_clicks_checkbox = QtWidgets.QCheckBox("Display clicks")
439
+
440
+ self.display_clicks_checkbox.setChecked(True)
441
+
442
+ self.display_clicks_checkbox.stateChanged.connect(
443
+ self.on_display_clicks_changed
444
+ )
445
+
446
+ clicks_layout.addRow(self.display_clicks_checkbox)
447
+
448
+ # Marker color
449
+
450
+ marker_widget = QtWidgets.QWidget()
451
+ marker_layout = QtWidgets.QHBoxLayout(marker_widget)
452
+ marker_layout.setContentsMargins(0, 0, 0, 0)
331
453
 
332
454
  self.color_btn = QtWidgets.QPushButton("Color")
333
455
  self.color_btn.clicked.connect(self.choose_color)
334
456
 
335
457
  self.color_preview = QtWidgets.QLabel(" ")
458
+
336
459
  self.color_preview.setStyleSheet(
337
460
  "background-color: rgb(255,0,0); border: 1px solid black;"
338
461
  )
339
462
 
340
- color_layout.addWidget(QtWidgets.QLabel("Marker"))
341
- color_layout.addWidget(self.color_btn)
342
- color_layout.addWidget(self.color_preview)
343
-
344
- color_widget.setLayout(color_layout)
463
+ marker_layout.addWidget(self.color_btn)
464
+ marker_layout.addWidget(self.color_preview)
345
465
 
346
- toolbar.addWidget(color_widget)
466
+ clicks_layout.addRow("Marker", marker_widget)
347
467
 
348
- toolbar.addSeparator()
349
-
350
- # ============================================================
351
- # Marker size selector
352
- # ============================================================
353
- size_widget = QtWidgets.QWidget()
354
- size_layout = QtWidgets.QHBoxLayout()
355
- size_layout.setContentsMargins(0, 0, 0, 0)
468
+ # Marker size
356
469
 
357
470
  self.size_selector = QtWidgets.QComboBox()
471
+
358
472
  self.size_selector.addItems(
359
473
  [
360
474
  "0.2",
@@ -374,77 +488,198 @@ class ClickImageApp(QtWidgets.QMainWindow):
374
488
  "100",
375
489
  ]
376
490
  )
491
+
377
492
  self.size_selector.setCurrentText("8")
493
+
378
494
  self.size_selector.currentTextChanged.connect(self.on_marker_size_changed)
379
495
 
380
- size_layout.addWidget(QtWidgets.QLabel("Size"))
381
- size_layout.addWidget(self.size_selector)
496
+ clicks_layout.addRow(
497
+ "Size",
498
+ self.size_selector,
499
+ )
500
+
501
+ clicks_action = QtWidgets.QWidgetAction(clicks_menu)
502
+ clicks_action.setDefaultWidget(clicks_panel)
503
+
504
+ clicks_menu.addAction(clicks_action)
382
505
 
383
- size_widget.setLayout(size_layout)
506
+ clicks_btn.setMenu(clicks_menu)
384
507
 
385
- toolbar.addWidget(size_widget)
508
+ toolbar.addWidget(clicks_btn)
509
+
510
+ # ============================================================
511
+ # CROSSHAIR
512
+ # ============================================================
386
513
 
387
514
  toolbar.addSeparator()
388
515
 
389
- toolbar.addAction("Clear Logs", self.clear_logs)
516
+ crosshair_btn = QtWidgets.QToolButton()
517
+ crosshair_btn.setText("⚙ Crosshair")
518
+ crosshair_btn.setPopupMode(QtWidgets.QToolButton.InstantPopup)
519
+
520
+ crosshair_menu = QtWidgets.QMenu(self)
521
+
522
+ crosshair_panel = QtWidgets.QWidget()
523
+ crosshair_layout = QtWidgets.QFormLayout(crosshair_panel)
524
+
525
+ crosshair_widget = QtWidgets.QWidget()
526
+ crosshair_widget_layout = QtWidgets.QHBoxLayout(crosshair_widget)
527
+
528
+ crosshair_widget_layout.setContentsMargins(
529
+ 0,
530
+ 0,
531
+ 0,
532
+ 0,
533
+ )
390
534
 
535
+ self.crosshair_color_btn = QtWidgets.QPushButton("Color")
536
+
537
+ self.crosshair_color_btn.clicked.connect(self.crosshair_choose_color)
538
+
539
+ self.crosshair_color_preview = QtWidgets.QLabel(" ")
540
+
541
+ self.crosshair_color_preview.setStyleSheet(
542
+ "background-color: rgb(255,0,0); border: 1px solid black;"
543
+ )
544
+
545
+ crosshair_widget_layout.addWidget(self.crosshair_color_btn)
546
+
547
+ crosshair_widget_layout.addWidget(self.crosshair_color_preview)
548
+
549
+ crosshair_layout.addRow(
550
+ "Color",
551
+ crosshair_widget,
552
+ )
553
+
554
+ crosshair_action = QtWidgets.QWidgetAction(crosshair_menu)
555
+
556
+ crosshair_action.setDefaultWidget(crosshair_panel)
557
+
558
+ crosshair_menu.addAction(crosshair_action)
559
+
560
+ crosshair_btn.setMenu(crosshair_menu)
561
+
562
+ toolbar.addWidget(crosshair_btn)
563
+
564
+ # ============================================================
565
+ # LOGS
566
+ # ============================================================
391
567
  toolbar.addSeparator()
392
568
 
569
+ clear_log_btn = QtWidgets.QToolButton()
570
+ clear_log_btn.setText("Clear logs")
571
+ clear_log_btn.clicked.connect(self.clear_logs)
572
+
573
+ toolbar.addWidget(clear_log_btn)
574
+
393
575
  # ============================================================
394
- # Precision mode
576
+ # Update pipeline
395
577
  # ============================================================
396
578
 
397
- def on_precision_changed(self, state):
579
+ def update(self):
398
580
  r"""
399
- Toggle integer/float precision mode.
581
+ Refresh UI.
400
582
  """
401
- INT = state == QtCore.Qt.Checked
402
- self.click_manager.precision_mode = "int" if INT else "float"
403
- self._append_log(f"Precision mode: {'INT' if INT else 'FLOAT'}")
404
- self.update()
583
+ if not self.initialization_done:
584
+ return
405
585
 
406
- def on_half_shift_changed(self, state):
586
+ self.update_groups()
587
+ self.update_table()
588
+ self.update_viewer()
589
+
590
+ def _format_value(self, v):
591
+ if v is None:
592
+ return ""
593
+
594
+ if self.click_manager.use_int_precision:
595
+ return str(int(round(v)))
596
+
597
+ return f"{v:.3f}"
598
+
599
+ def update_viewer(self):
407
600
  r"""
408
- Toggle half-shift mode.
601
+ Render image + clicks.
409
602
  """
603
+ img = self.image.astype(np.float32)
604
+ imax = np.iinfo(self.image.dtype).max
605
+ dm = self.display_min_pc * imax / 100
606
+ dM = self.display_max_pc * imax / 100
607
+ b = self.beta_pc * imax / 100
410
608
 
411
- half_shift = state == QtCore.Qt.Checked
609
+ img = np.clip(img, dm, dM)
412
610
 
413
- if self.click_manager.n_clicks > 0:
611
+ img = (img - dm) / max(1, dM - dm) * imax
612
+ img = img * self.alpha + b
414
613
 
415
- msg = QtWidgets.QMessageBox(self)
416
- msg.setWindowTitle("Existing clicks detected")
417
- msg.setText(
418
- f"Do you want to shift all previous clicked points by 0.5 to match the new coordinates system ?"
614
+ img = np.clip(np.round(img), 0, imax).astype(self.image.dtype)
615
+
616
+ if self._image_has_changed or self._colormap_has_changed:
617
+ colormap = self.get_selected_colormap()
618
+
619
+ if colormap is not None:
620
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
621
+ img = cv2.applyColorMap(gray, colormap)
622
+
623
+ # conversion unique pour Qt
624
+ rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
625
+
626
+ h, w, ch = rgb.shape
627
+ bytes_per_line = ch * w
628
+
629
+ qimg = QtGui.QImage(
630
+ rgb.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888
419
631
  )
420
- msg.setInformativeText("No = keep clicks\nYes = shift clicks")
421
- msg.setStandardButtons(QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes)
422
- msg.setDefaultButton(QtWidgets.QMessageBox.Yes)
632
+ pix = QtGui.QPixmap.fromImage(qimg)
423
633
 
424
- choice = msg.exec_()
634
+ self.viewer.set_image(pix)
635
+ self._image_has_changed = False
636
+ self._colormap_has_changed = False
425
637
 
426
- if choice == QtWidgets.QMessageBox.Yes:
427
- if half_shift:
428
- self.click_manager.to_half_shift_on()
429
- else:
430
- self.click_manager.to_half_shift_off()
638
+ # redraw clicks
639
+ self.viewer.clear_markers()
431
640
 
432
- self.viewer.half_shift = half_shift
433
- self._append_log(f"Half-Shift mode: {half_shift}")
434
- self.update()
641
+ pts = self.click_manager.extract_group()
435
642
 
436
- def on_display_clicks_changed(self, state):
643
+ if self.show_clicks:
644
+ for x, y in pts:
645
+ if x is None or y is None:
646
+ continue
647
+ self.viewer.add_marker((x, y), self.marker_color, self.marker_size)
648
+
649
+ def update_table(self):
437
650
  r"""
438
- Toggle dispaly clicks.
651
+ Update table with current group points.
439
652
  """
440
- DISPLAY = state == QtCore.Qt.Checked
441
- self._append_log(f"Display clicks: {DISPLAY}")
442
- self.update()
653
+ pts = self.click_manager.extract_group()
654
+
655
+ if self.click_manager.use_float_precision:
656
+ self.table.setHorizontalHeaderLabels(["Index", "X (float)", "Y (float)"])
657
+ else:
658
+ self.table.setHorizontalHeaderLabels(["Index", "X (int)", "Y (int)"])
659
+
660
+ self.table.setRowCount(len(pts))
661
+
662
+ for i, (x, y) in enumerate(pts):
663
+ self.table.setItem(i, 0, QtWidgets.QTableWidgetItem(str(i)))
664
+ self.table.setItem(i, 1, QtWidgets.QTableWidgetItem(self._format_value(x)))
665
+ self.table.setItem(i, 2, QtWidgets.QTableWidgetItem(self._format_value(y)))
666
+
667
+ def update_groups(self):
668
+ r"""
669
+ Sync group selector.
670
+ """
671
+ self.group_selector.blockSignals(True)
672
+ self.group_selector.clear()
673
+
674
+ for g in self.click_manager.groups:
675
+ self.group_selector.addItem(g)
676
+
677
+ self.group_selector.setCurrentText(self.click_manager.current_group)
678
+ self.group_selector.blockSignals(False)
443
679
 
444
680
  # ============================================================
445
681
  # Image
446
682
  # ============================================================
447
-
448
683
  def set_image(self, image: Optional[np.ndarray]):
449
684
  r"""
450
685
  Set image to display.
@@ -529,99 +764,60 @@ class ClickImageApp(QtWidgets.QMainWindow):
529
764
 
530
765
  self.update()
531
766
 
532
- # ============================================================
533
- # Update pipeline
534
- # ============================================================
535
-
536
- def update(self):
537
- r"""
538
- Refresh UI.
767
+ def on_colormap_changed(self, index):
539
768
  """
540
- if not self.initialization_done:
541
- return
542
-
543
- self.update_groups()
544
- self.update_table()
545
- self.update_viewer()
546
-
547
- def _format_value(self, v):
548
- if v is None:
549
- return ""
550
-
551
- if self.click_manager.use_int_precision:
552
- return str(int(round(v)))
553
-
554
- return f"{v:.3f}"
555
-
556
- def update_viewer(self):
557
- r"""
558
- Render image + clicks.
769
+ Called when the user selects a different colormap.
559
770
  """
560
- if self._image_has_changed or self._colormap_has_changed:
561
- colormap = self.get_selected_colormap()
562
- img = self.image # BGR (OpenCV standard)
563
-
564
- if colormap is not None:
565
- gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
566
- img = cv2.applyColorMap(gray, colormap)
567
-
568
- # conversion unique pour Qt
569
- rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
771
+ self._colormap_has_changed = True
772
+ self._append_log(f"Colormap changed to: {self.colormap_selector.currentText()}")
773
+ self.update()
570
774
 
571
- h, w, ch = rgb.shape
572
- bytes_per_line = ch * w
775
+ def get_selected_colormap(self):
776
+ """
777
+ Get the selected colormap for displaying the image.
573
778
 
574
- qimg = QtGui.QImage(
575
- rgb.data, w, h, bytes_per_line, QtGui.QImage.Format_RGB888
576
- )
577
- pix = QtGui.QPixmap.fromImage(qimg)
779
+ Returns
780
+ -------
781
+ str
782
+ The selected colormap.
783
+ """
784
+ colormap_map = {
785
+ "Default": None,
786
+ "Gray": cv2.COLORMAP_BONE,
787
+ "Hot": cv2.COLORMAP_HOT,
788
+ "Jet": cv2.COLORMAP_JET,
789
+ "Rainbow": cv2.COLORMAP_RAINBOW,
790
+ "Cool": cv2.COLORMAP_COOL,
791
+ "Spring": cv2.COLORMAP_SPRING,
792
+ }
793
+ return colormap_map.get(self.colormap_selector.currentText(), None)
578
794
 
579
- self.viewer.set_image(pix)
580
- self._image_has_changed = False
581
- self._colormap_has_changed = False
795
+ def on_contrast_changed(self):
582
796
 
583
- # redraw clicks
584
- self.viewer.clear_markers()
797
+ self.alpha = self.alpha_slider.value() / 100
798
+ self.beta_pc = self.beta_slider.value()
799
+ self.display_min_pc = self.min_slider.value()
800
+ self.display_max_pc = self.max_slider.value()
585
801
 
586
- pts = self.click_manager.extract_group()
802
+ self.alpha_value_label.setText(f"{self.alpha:.2f}")
803
+ self.beta_value_label.setText(f"{self.beta_pc}%")
804
+ self.min_value_label.setText(f"{self.display_min_pc}%")
805
+ self.max_value_label.setText(f"{self.display_max_pc}%")
587
806
 
588
- DISPLAY = self.display_clicks_checkbox.isChecked()
589
- if DISPLAY:
590
- for x, y in pts:
591
- if x is None or y is None:
592
- continue
593
- self.viewer.add_marker((x, y), self.marker_color, self.marker_size)
807
+ self._image_has_changed = True
808
+ self.update()
594
809
 
595
- def update_table(self):
596
- r"""
597
- Update table with current group points.
810
+ def on_reset_contrast(self):
598
811
  """
599
- pts = self.click_manager.extract_group()
600
-
601
- if self.click_manager.use_float_precision:
602
- self.table.setHorizontalHeaderLabels(["Index", "X (float)", "Y (float)"])
603
- else:
604
- self.table.setHorizontalHeaderLabels(["Index", "X (int)", "Y (int)"])
605
-
606
- self.table.setRowCount(len(pts))
607
-
608
- for i, (x, y) in enumerate(pts):
609
- self.table.setItem(i, 0, QtWidgets.QTableWidgetItem(str(i)))
610
- self.table.setItem(i, 1, QtWidgets.QTableWidgetItem(self._format_value(x)))
611
- self.table.setItem(i, 2, QtWidgets.QTableWidgetItem(self._format_value(y)))
612
-
613
- def update_groups(self):
614
- r"""
615
- Sync group selector.
812
+ Reset contrast settings.
616
813
  """
617
- self.group_selector.blockSignals(True)
618
- self.group_selector.clear()
814
+ self.alpha_slider.setValue(100)
815
+ self.beta_slider.setValue(0)
619
816
 
620
- for g in self.click_manager.groups:
621
- self.group_selector.addItem(g)
817
+ self.min_slider.setValue(0)
818
+ self.max_slider.setValue(100)
622
819
 
623
- self.group_selector.setCurrentText(self.click_manager.current_group)
624
- self.group_selector.blockSignals(False)
820
+ self.on_contrast_changed()
625
821
 
626
822
  # ============================================================
627
823
  # Click handling
@@ -635,6 +831,7 @@ class ClickImageApp(QtWidgets.QMainWindow):
635
831
  return
636
832
 
637
833
  self.click_manager.add_click(x, y)
834
+ self._append_log(f"Click processed: {(x, y)}")
638
835
  self._is_saved = False
639
836
  self.update()
640
837
 
@@ -646,6 +843,7 @@ class ClickImageApp(QtWidgets.QMainWindow):
646
843
  return
647
844
 
648
845
  self.click_manager.add_click(None, None)
846
+ self._append_log(f"Click processed: {(None, None)}")
649
847
  self._is_saved = False
650
848
  self.update()
651
849
 
@@ -689,6 +887,53 @@ class ClickImageApp(QtWidgets.QMainWindow):
689
887
  self._is_saved = False
690
888
  self.update()
691
889
 
890
+ def on_precision_changed(self, state):
891
+ r"""
892
+ Toggle integer/float precision mode.
893
+ """
894
+ INT = state == QtCore.Qt.Checked
895
+ self.click_manager.precision_mode = "int" if INT else "float"
896
+ self._append_log(f"Precision mode: {'INT' if INT else 'FLOAT'}")
897
+ self.update()
898
+
899
+ def on_half_shift_changed(self, state):
900
+ r"""
901
+ Toggle half-shift mode.
902
+ """
903
+
904
+ half_shift = state == QtCore.Qt.Checked
905
+
906
+ if self.click_manager.n_clicks > 0:
907
+
908
+ msg = QtWidgets.QMessageBox(self)
909
+ msg.setWindowTitle("Existing clicks detected")
910
+ msg.setText(
911
+ f"Do you want to shift all previous clicked points by 0.5 to match the new coordinates system ?"
912
+ )
913
+ msg.setInformativeText("No = keep clicks\nYes = shift clicks")
914
+ msg.setStandardButtons(QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Yes)
915
+ msg.setDefaultButton(QtWidgets.QMessageBox.Yes)
916
+
917
+ choice = msg.exec_()
918
+
919
+ if choice == QtWidgets.QMessageBox.Yes:
920
+ if half_shift:
921
+ self.click_manager.to_half_shift_on()
922
+ else:
923
+ self.click_manager.to_half_shift_off()
924
+
925
+ self.viewer.half_shift = half_shift
926
+ self._append_log(f"Half-Shift mode: {half_shift}")
927
+ self.update()
928
+
929
+ def on_display_clicks_changed(self, state):
930
+ r"""
931
+ Toggle dispaly clicks.
932
+ """
933
+ self.show_clicks = state == QtCore.Qt.Checked
934
+ self._append_log(f"Display clicks: {self.show_clicks}")
935
+ self.update()
936
+
692
937
  # ============================================================
693
938
  # Design
694
939
  # ============================================================
@@ -703,6 +948,8 @@ class ClickImageApp(QtWidgets.QMainWindow):
703
948
  f"background-color: {color.name()}; border: 1px solid black;"
704
949
  )
705
950
 
951
+ self._append_log(f"Crosshair color set to: {color.name()}")
952
+
706
953
  def choose_color(self):
707
954
  color = QtWidgets.QColorDialog.getColor(self.marker_color, self)
708
955
 
@@ -714,6 +961,8 @@ class ClickImageApp(QtWidgets.QMainWindow):
714
961
  f"background-color: {color.name()}; border: 1px solid black;"
715
962
  )
716
963
 
964
+ self._append_log(f"Marker color set to: {color.name()}")
965
+
717
966
  def on_marker_size_changed(self, value: str):
718
967
  r"""
719
968
  Update marker size.
@@ -724,47 +973,19 @@ class ClickImageApp(QtWidgets.QMainWindow):
724
973
  except ValueError:
725
974
  self.marker_size = 8
726
975
 
727
- self._append_log(f"Marker size set to {self.marker_size}")
976
+ self._append_log(f"Marker size set to: {self.marker_size}")
728
977
 
729
978
  self.update()
730
979
 
731
- def on_colormap_changed(self, index):
732
- """
733
- Called when the user selects a different colormap.
734
- """
735
- self._append_log(f"Colormap changed to: {self.colormap_selector.currentText()}")
736
- self._colormap_has_changed = True
737
- self.update()
738
-
739
- def get_selected_colormap(self):
740
- """
741
- Get the selected colormap for displaying the image.
742
-
743
- Returns
744
- -------
745
- str
746
- The selected colormap.
747
- """
748
- colormap_map = {
749
- "Default": None,
750
- "Gray": cv2.COLORMAP_BONE,
751
- "Hot": cv2.COLORMAP_HOT,
752
- "Jet": cv2.COLORMAP_JET,
753
- "Rainbow": cv2.COLORMAP_RAINBOW,
754
- "Cool": cv2.COLORMAP_COOL,
755
- "Spring": cv2.COLORMAP_SPRING,
756
- }
757
- return colormap_map.get(self.colormap_selector.currentText(), None)
758
-
759
980
  # ============================================================
760
981
  # Groups
761
982
  # ============================================================
762
-
763
983
  def on_group_changed(self, name):
764
984
  r"""
765
985
  Switch active group.
766
986
  """
767
987
  self.click_manager.set_group(name)
988
+ self._append_log(f"Group switch to : {name}")
768
989
  self.update()
769
990
 
770
991
  def on_add_group(self):
@@ -863,7 +1084,7 @@ class ClickImageApp(QtWidgets.QMainWindow):
863
1084
  self.output_path = file_path
864
1085
 
865
1086
  if self.output_path is not None:
866
- self._append_log(f"Output CSV path setted to : {self.output_path}")
1087
+ self._append_log(f"Output CSV path setted to: {self.output_path}")
867
1088
 
868
1089
  # -------------------------------------------------
869
1090
  # Save
@@ -928,7 +1149,6 @@ class ClickImageApp(QtWidgets.QMainWindow):
928
1149
  # ============================================================
929
1150
  # Utils
930
1151
  # ============================================================
931
-
932
1152
  def _append_log(self, msg: str):
933
1153
  r"""
934
1154
  Append log message.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyclickimage
3
- Version: 2.2.1
3
+ Version: 2.4.0
4
4
  Summary: Python library to select points on a image [pyqt5 GUI]
5
5
  Author-email: Artezaru <artezaru.github@proton.me>
6
6
  License: GNU GENERAL PUBLIC LICENSE
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes