bec-widgets 2.4.3__py3-none-any.whl → 2.5.0__py3-none-any.whl

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.
@@ -37,10 +37,11 @@ jobs:
37
37
  echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
38
38
  git clone --branch $OPHYD_DEVICES_BRANCH https://github.com/bec-project/ophyd_devices.git
39
39
  export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
40
- pip install -e ./ophyd_devices
41
- pip install -e ./bec/bec_lib[dev]
42
- pip install -e ./bec/bec_ipython_client
43
- pip install -e .[dev,pyside6]
40
+ pip install uv
41
+ uv pip install --system -e ./ophyd_devices
42
+ uv pip install --system -e ./bec/bec_lib[dev]
43
+ uv pip install --system -e ./bec/bec_ipython_client
44
+ uv pip install --system -e .[dev,pyside6]
44
45
 
45
46
  - name: Run Pytest
46
47
  run: |
@@ -48,10 +48,11 @@ jobs:
48
48
  echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
49
49
  git clone --branch $OPHYD_DEVICES_BRANCH https://github.com/bec-project/ophyd_devices.git
50
50
  export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
51
- pip install -e ./ophyd_devices
52
- pip install -e ./bec/bec_lib[dev]
53
- pip install -e ./bec/bec_ipython_client
54
- pip install -e .[dev,pyside6]
51
+ pip install uv
52
+ uv pip install --system -e ./ophyd_devices
53
+ uv pip install --system -e ./bec/bec_lib[dev]
54
+ uv pip install --system -e ./bec/bec_ipython_client
55
+ uv pip install --system -e .[dev,pyside6]
55
56
 
56
57
  - name: Run Pytest with Coverage
57
58
  id: coverage
CHANGELOG.md CHANGED
@@ -1,6 +1,19 @@
1
1
  # CHANGELOG
2
2
 
3
3
 
4
+ ## v2.5.0 (2025-05-20)
5
+
6
+ ### Continuous Integration
7
+
8
+ - Try uv for test env setup
9
+ ([`6ee0f50`](https://github.com/bec-project/bec_widgets/commit/6ee0f5004d6c840a31a9394f5d1610f635e9b83b))
10
+
11
+ ### Features
12
+
13
+ - **image_rois**: Image rois with RPC can be added to Image widget
14
+ ([`1d018e8`](https://github.com/bec-project/bec_widgets/commit/1d018e863ca0cdb3274002cf35d69a6961aaf07d))
15
+
16
+
4
17
  ## v2.4.3 (2025-05-19)
5
18
 
6
19
  ### Bug Fixes
PKG-INFO CHANGED
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_widgets
3
- Version: 2.4.3
3
+ Version: 2.5.0
4
4
  Summary: BEC Widgets
5
5
  Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec_widgets/issues
6
6
  Project-URL: Homepage, https://gitlab.psi.ch/bec/bec_widgets
bec_widgets/cli/client.py CHANGED
@@ -504,6 +504,204 @@ class BECStatusBox(RPCBase):
504
504
  """
505
505
 
506
506
 
507
+ class BaseROI(RPCBase):
508
+ """Base class for all Region of Interest (ROI) implementations."""
509
+
510
+ @property
511
+ @rpc_call
512
+ def label(self) -> "str":
513
+ """
514
+ Gets the display name of this ROI.
515
+
516
+ Returns:
517
+ str: The current name of the ROI.
518
+ """
519
+
520
+ @label.setter
521
+ @rpc_call
522
+ def label(self) -> "str":
523
+ """
524
+ Gets the display name of this ROI.
525
+
526
+ Returns:
527
+ str: The current name of the ROI.
528
+ """
529
+
530
+ @property
531
+ @rpc_call
532
+ def line_color(self) -> "str":
533
+ """
534
+ Gets the current line color of the ROI.
535
+
536
+ Returns:
537
+ str: The current line color as a string (e.g., hex color code).
538
+ """
539
+
540
+ @line_color.setter
541
+ @rpc_call
542
+ def line_color(self) -> "str":
543
+ """
544
+ Gets the current line color of the ROI.
545
+
546
+ Returns:
547
+ str: The current line color as a string (e.g., hex color code).
548
+ """
549
+
550
+ @property
551
+ @rpc_call
552
+ def line_width(self) -> "int":
553
+ """
554
+ Gets the current line width of the ROI.
555
+
556
+ Returns:
557
+ int: The current line width in pixels.
558
+ """
559
+
560
+ @line_width.setter
561
+ @rpc_call
562
+ def line_width(self) -> "int":
563
+ """
564
+ Gets the current line width of the ROI.
565
+
566
+ Returns:
567
+ int: The current line width in pixels.
568
+ """
569
+
570
+ @rpc_call
571
+ def get_coordinates(self):
572
+ """
573
+ Gets the coordinates that define this ROI's position and shape.
574
+
575
+ This is an abstract method that must be implemented by subclasses.
576
+ Implementations should return either a dictionary with descriptive keys
577
+ or a tuple of coordinates, depending on the value of self.description.
578
+
579
+ Returns:
580
+ dict or tuple: The coordinates defining the ROI's position and shape.
581
+
582
+ Raises:
583
+ NotImplementedError: This method must be implemented by subclasses.
584
+ """
585
+
586
+ @rpc_call
587
+ def get_data_from_image(
588
+ self, image_item: "pg.ImageItem | None" = None, returnMappedCoords: "bool" = False, **kwargs
589
+ ):
590
+ """
591
+ Wrapper around `pyqtgraph.ROI.getArrayRegion`.
592
+
593
+ Args:
594
+ image_item (pg.ImageItem or None): The ImageItem to sample. If None, auto-detects
595
+ the first `ImageItem` in the same GraphicsScene as this ROI.
596
+ returnMappedCoords (bool): If True, also returns the coordinate array generated by
597
+ *getArrayRegion*.
598
+ **kwargs: Additional keyword arguments passed to *getArrayRegion* or *affineSlice*,
599
+ such as `axes`, `order`, `shape`, etc.
600
+
601
+ Returns:
602
+ ndarray: Pixel data inside the ROI, or (data, coords) if *returnMappedCoords* is True.
603
+ """
604
+
605
+
606
+ class CircularROI(RPCBase):
607
+ """Circular Region of Interest with center/diameter tracking and auto-labeling."""
608
+
609
+ @property
610
+ @rpc_call
611
+ def label(self) -> "str":
612
+ """
613
+ Gets the display name of this ROI.
614
+
615
+ Returns:
616
+ str: The current name of the ROI.
617
+ """
618
+
619
+ @label.setter
620
+ @rpc_call
621
+ def label(self) -> "str":
622
+ """
623
+ Gets the display name of this ROI.
624
+
625
+ Returns:
626
+ str: The current name of the ROI.
627
+ """
628
+
629
+ @property
630
+ @rpc_call
631
+ def line_color(self) -> "str":
632
+ """
633
+ Gets the current line color of the ROI.
634
+
635
+ Returns:
636
+ str: The current line color as a string (e.g., hex color code).
637
+ """
638
+
639
+ @line_color.setter
640
+ @rpc_call
641
+ def line_color(self) -> "str":
642
+ """
643
+ Gets the current line color of the ROI.
644
+
645
+ Returns:
646
+ str: The current line color as a string (e.g., hex color code).
647
+ """
648
+
649
+ @property
650
+ @rpc_call
651
+ def line_width(self) -> "int":
652
+ """
653
+ Gets the current line width of the ROI.
654
+
655
+ Returns:
656
+ int: The current line width in pixels.
657
+ """
658
+
659
+ @line_width.setter
660
+ @rpc_call
661
+ def line_width(self) -> "int":
662
+ """
663
+ Gets the current line width of the ROI.
664
+
665
+ Returns:
666
+ int: The current line width in pixels.
667
+ """
668
+
669
+ @rpc_call
670
+ def get_coordinates(self, typed: "bool | None" = None) -> "dict | tuple":
671
+ """
672
+ Calculates and returns the coordinates and size of an object, either as a
673
+ typed dictionary or as a tuple.
674
+
675
+ Args:
676
+ typed (bool | None): If True, returns coordinates as a dictionary. Defaults
677
+ to None, which utilizes the object's description value.
678
+
679
+ Returns:
680
+ dict: A dictionary with keys 'center_x', 'center_y', 'diameter', and 'radius'
681
+ if `typed` is True.
682
+ tuple: A tuple containing (center_x, center_y, diameter, radius) if `typed` is False.
683
+ """
684
+
685
+ @rpc_call
686
+ def get_data_from_image(
687
+ self, image_item: "pg.ImageItem | None" = None, returnMappedCoords: "bool" = False, **kwargs
688
+ ):
689
+ """
690
+ Wrapper around `pyqtgraph.ROI.getArrayRegion`.
691
+
692
+ Args:
693
+ image_item (pg.ImageItem or None): The ImageItem to sample. If None, auto-detects
694
+ the first `ImageItem` in the same GraphicsScene as this ROI.
695
+ returnMappedCoords (bool): If True, also returns the coordinate array generated by
696
+ *getArrayRegion*.
697
+ **kwargs: Additional keyword arguments passed to *getArrayRegion* or *affineSlice*,
698
+ such as `axes`, `order`, `shape`, etc.
699
+
700
+ Returns:
701
+ ndarray: Pixel data inside the ROI, or (data, coords) if *returnMappedCoords* is True.
702
+ """
703
+
704
+
507
705
  class Curve(RPCBase):
508
706
  @rpc_call
509
707
  def remove(self):
@@ -1215,6 +1413,44 @@ class Image(RPCBase):
1215
1413
  Access the main image item.
1216
1414
  """
1217
1415
 
1416
+ @rpc_call
1417
+ def add_roi(
1418
+ self,
1419
+ kind: "Literal['rect', 'circle']" = "rect",
1420
+ name: "str | None" = None,
1421
+ line_width: "int | None" = 10,
1422
+ pos: "tuple[float, float] | None" = (10, 10),
1423
+ size: "tuple[float, float] | None" = (50, 50),
1424
+ **pg_kwargs,
1425
+ ) -> "RectangularROI | CircularROI":
1426
+ """
1427
+ Add a ROI to the image.
1428
+
1429
+ Args:
1430
+ kind(str): The type of ROI to add. Options are "rect" or "circle".
1431
+ name(str): The name of the ROI.
1432
+ line_width(int): The line width of the ROI.
1433
+ pos(tuple): The position of the ROI.
1434
+ size(tuple): The size of the ROI.
1435
+ **pg_kwargs: Additional arguments for the ROI.
1436
+
1437
+ Returns:
1438
+ RectangularROI | CircularROI: The created ROI object.
1439
+ """
1440
+
1441
+ @rpc_call
1442
+ def remove_roi(self, roi: "int | str"):
1443
+ """
1444
+ Remove an ROI by index or label via the ROIController.
1445
+ """
1446
+
1447
+ @property
1448
+ @rpc_call
1449
+ def rois(self) -> "list[BaseROI]":
1450
+ """
1451
+ Get the list of ROIs.
1452
+ """
1453
+
1218
1454
 
1219
1455
  class ImageItem(RPCBase):
1220
1456
  @property
@@ -2318,6 +2554,105 @@ class PositionerGroup(RPCBase):
2318
2554
  """
2319
2555
 
2320
2556
 
2557
+ class RectangularROI(RPCBase):
2558
+ """Defines a rectangular Region of Interest (ROI) with additional functionality."""
2559
+
2560
+ @property
2561
+ @rpc_call
2562
+ def label(self) -> "str":
2563
+ """
2564
+ Gets the display name of this ROI.
2565
+
2566
+ Returns:
2567
+ str: The current name of the ROI.
2568
+ """
2569
+
2570
+ @label.setter
2571
+ @rpc_call
2572
+ def label(self) -> "str":
2573
+ """
2574
+ Gets the display name of this ROI.
2575
+
2576
+ Returns:
2577
+ str: The current name of the ROI.
2578
+ """
2579
+
2580
+ @property
2581
+ @rpc_call
2582
+ def line_color(self) -> "str":
2583
+ """
2584
+ Gets the current line color of the ROI.
2585
+
2586
+ Returns:
2587
+ str: The current line color as a string (e.g., hex color code).
2588
+ """
2589
+
2590
+ @line_color.setter
2591
+ @rpc_call
2592
+ def line_color(self) -> "str":
2593
+ """
2594
+ Gets the current line color of the ROI.
2595
+
2596
+ Returns:
2597
+ str: The current line color as a string (e.g., hex color code).
2598
+ """
2599
+
2600
+ @property
2601
+ @rpc_call
2602
+ def line_width(self) -> "int":
2603
+ """
2604
+ Gets the current line width of the ROI.
2605
+
2606
+ Returns:
2607
+ int: The current line width in pixels.
2608
+ """
2609
+
2610
+ @line_width.setter
2611
+ @rpc_call
2612
+ def line_width(self) -> "int":
2613
+ """
2614
+ Gets the current line width of the ROI.
2615
+
2616
+ Returns:
2617
+ int: The current line width in pixels.
2618
+ """
2619
+
2620
+ @rpc_call
2621
+ def get_coordinates(self, typed: "bool | None" = None) -> "dict | tuple":
2622
+ """
2623
+ Returns the coordinates of a rectangle's corners. Supports returning them
2624
+ as either a dictionary with descriptive keys or a tuple of coordinates.
2625
+
2626
+ Args:
2627
+ typed (bool | None): If True, returns coordinates as a dictionary with
2628
+ descriptive keys. If False, returns them as a tuple. Defaults to
2629
+ the value of `self.description`.
2630
+
2631
+ Returns:
2632
+ dict | tuple: The rectangle's corner coordinates, where the format
2633
+ depends on the `typed` parameter.
2634
+ """
2635
+
2636
+ @rpc_call
2637
+ def get_data_from_image(
2638
+ self, image_item: "pg.ImageItem | None" = None, returnMappedCoords: "bool" = False, **kwargs
2639
+ ):
2640
+ """
2641
+ Wrapper around `pyqtgraph.ROI.getArrayRegion`.
2642
+
2643
+ Args:
2644
+ image_item (pg.ImageItem or None): The ImageItem to sample. If None, auto-detects
2645
+ the first `ImageItem` in the same GraphicsScene as this ROI.
2646
+ returnMappedCoords (bool): If True, also returns the coordinate array generated by
2647
+ *getArrayRegion*.
2648
+ **kwargs: Additional keyword arguments passed to *getArrayRegion* or *affineSlice*,
2649
+ such as `axes`, `order`, `shape`, etc.
2650
+
2651
+ Returns:
2652
+ ndarray: Pixel data inside the ROI, or (data, coords) if *returnMappedCoords* is True.
2653
+ """
2654
+
2655
+
2321
2656
  class ResetButton(RPCBase):
2322
2657
  """A button that resets the scan queue."""
2323
2658
 
@@ -43,7 +43,7 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
43
43
  "pg": pg,
44
44
  "wh": wh,
45
45
  "dock": self.dock,
46
- # "im": self.im,
46
+ "im": self.im,
47
47
  # "mi": self.mi,
48
48
  # "mm": self.mm,
49
49
  # "lm": self.lm,
@@ -112,13 +112,13 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
112
112
  # tab_widget.addTab(fifth_tab, "Waveform Next Gen")
113
113
  # tab_widget.setCurrentIndex(4)
114
114
  #
115
- # sixth_tab = QWidget()
116
- # sixth_tab_layout = QVBoxLayout(sixth_tab)
117
- # self.im = Image()
118
- # self.mi = self.im.main_image
119
- # sixth_tab_layout.addWidget(self.im)
120
- # tab_widget.addTab(sixth_tab, "Image Next Gen")
121
- # tab_widget.setCurrentIndex(5)
115
+ sixth_tab = QWidget()
116
+ sixth_tab_layout = QVBoxLayout(sixth_tab)
117
+ self.im = Image(popups=False)
118
+ self.mi = self.im.main_image
119
+ sixth_tab_layout.addWidget(self.im)
120
+ tab_widget.addTab(sixth_tab, "Image Next Gen")
121
+ tab_widget.setCurrentIndex(1)
122
122
  #
123
123
  # seventh_tab = QWidget()
124
124
  # seventh_tab_layout = QVBoxLayout(seventh_tab)
@@ -20,6 +20,12 @@ from bec_widgets.widgets.plots.image.toolbar_bundles.image_selection import (
20
20
  )
21
21
  from bec_widgets.widgets.plots.image.toolbar_bundles.processing import ImageProcessingToolbarBundle
22
22
  from bec_widgets.widgets.plots.plot_base import PlotBase
23
+ from bec_widgets.widgets.plots.roi.image_roi import (
24
+ BaseROI,
25
+ CircularROI,
26
+ RectangularROI,
27
+ ROIController,
28
+ )
23
29
 
24
30
  logger = bec_logger.logger
25
31
 
@@ -111,6 +117,9 @@ class Image(PlotBase):
111
117
  "transpose.setter",
112
118
  "image",
113
119
  "main_image",
120
+ "add_roi",
121
+ "remove_roi",
122
+ "rois",
114
123
  ]
115
124
  sync_colorbar_with_autorange = Signal()
116
125
 
@@ -128,6 +137,7 @@ class Image(PlotBase):
128
137
  self.gui_id = config.gui_id
129
138
  self._color_bar = None
130
139
  self._main_image = ImageItem()
140
+ self.roi_controller = ROIController(colormap="viridis")
131
141
  super().__init__(
132
142
  parent=parent, config=config, client=client, gui_id=gui_id, popups=popups, **kwargs
133
143
  )
@@ -139,6 +149,9 @@ class Image(PlotBase):
139
149
  # Default Color map to plasma
140
150
  self.color_map = "plasma"
141
151
 
152
+ # Headless controller keeps the canonical list.
153
+ self._roi_manager_dialog = None
154
+
142
155
  ################################################################################
143
156
  # Widget Specific GUI interactions
144
157
  ################################################################################
@@ -304,9 +317,81 @@ class Image(PlotBase):
304
317
  if vrange: # should be at the end to disable the autorange if defined
305
318
  self.v_range = vrange
306
319
 
320
+ ################################################################################
321
+ # Static rois with roi manager
322
+
323
+ def add_roi(
324
+ self,
325
+ kind: Literal["rect", "circle"] = "rect",
326
+ name: str | None = None,
327
+ line_width: int | None = 10,
328
+ pos: tuple[float, float] | None = (10, 10),
329
+ size: tuple[float, float] | None = (50, 50),
330
+ **pg_kwargs,
331
+ ) -> RectangularROI | CircularROI:
332
+ """
333
+ Add a ROI to the image.
334
+
335
+ Args:
336
+ kind(str): The type of ROI to add. Options are "rect" or "circle".
337
+ name(str): The name of the ROI.
338
+ line_width(int): The line width of the ROI.
339
+ pos(tuple): The position of the ROI.
340
+ size(tuple): The size of the ROI.
341
+ **pg_kwargs: Additional arguments for the ROI.
342
+
343
+ Returns:
344
+ RectangularROI | CircularROI: The created ROI object.
345
+ """
346
+ if name is None:
347
+ name = f"ROI_{len(self.roi_controller.rois) + 1}"
348
+ if kind == "rect":
349
+ roi = RectangularROI(
350
+ pos=pos,
351
+ size=size,
352
+ parent_image=self,
353
+ line_width=line_width,
354
+ label=name,
355
+ **pg_kwargs,
356
+ )
357
+ elif kind == "circle":
358
+ roi = CircularROI(
359
+ pos=pos,
360
+ size=size,
361
+ parent_image=self,
362
+ line_width=line_width,
363
+ label=name,
364
+ **pg_kwargs,
365
+ )
366
+ else:
367
+ raise ValueError("kind must be 'rect' or 'circle'")
368
+
369
+ # Add to plot and controller (controller assigns color)
370
+ self.plot_item.addItem(roi)
371
+ self.roi_controller.add_roi(roi)
372
+ return roi
373
+
374
+ def remove_roi(self, roi: int | str):
375
+ """Remove an ROI by index or label via the ROIController."""
376
+ if isinstance(roi, int):
377
+ self.roi_controller.remove_roi_by_index(roi)
378
+ elif isinstance(roi, str):
379
+ self.roi_controller.remove_roi_by_name(roi)
380
+ else:
381
+ raise ValueError("roi must be an int index or str name")
382
+
307
383
  ################################################################################
308
384
  # Widget Specific Properties
309
385
  ################################################################################
386
+ ################################################################################
387
+ # Rois
388
+
389
+ @property
390
+ def rois(self) -> list[BaseROI]:
391
+ """
392
+ Get the list of ROIs.
393
+ """
394
+ return self.roi_controller.rois
310
395
 
311
396
  ################################################################################
312
397
  # Colorbar toggle
@@ -925,6 +1010,11 @@ class Image(PlotBase):
925
1010
  """
926
1011
  Disconnect the image update signals and clean up the image.
927
1012
  """
1013
+ # Remove all ROIs
1014
+ rois = self.rois
1015
+ for roi in rois:
1016
+ roi.remove()
1017
+
928
1018
  # Main Image cleanup
929
1019
  if self._main_image.config.monitor is not None:
930
1020
  self.disconnect_monitor(self._main_image.config.monitor)
File without changes