mirror of
https://github.com/bec-project/bec_widgets.git
synced 2025-07-14 11:41:49 +02:00
feat: add new default scaling of image_item
This commit is contained in:
@ -711,6 +711,7 @@ class BECImageItem(RPCBase):
|
||||
- log
|
||||
- rot
|
||||
- transpose
|
||||
- autorange_mode
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
@ -767,6 +768,15 @@ class BECImageItem(RPCBase):
|
||||
autorange(bool): Whether to autorange the color bar.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_autorange_mode(self, mode: "Literal['max', 'mean']" = "mean"):
|
||||
"""
|
||||
Set the autorange mode to scale the vrange of the color bar. Choose between min/max or mean +/- std.
|
||||
|
||||
Args:
|
||||
mode(Literal["max","mean"]): Max for min/max or mean for mean +/- std.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_color_map(self, cmap: "str" = "magma"):
|
||||
"""
|
||||
@ -796,7 +806,11 @@ class BECImageItem(RPCBase):
|
||||
|
||||
@rpc_call
|
||||
def set_vrange(
|
||||
self, vmin: "float" = None, vmax: "float" = None, vrange: "tuple[int, int]" = None
|
||||
self,
|
||||
vmin: "float" = None,
|
||||
vmax: "float" = None,
|
||||
vrange: "tuple[float, float]" = None,
|
||||
change_autorange: "bool" = True,
|
||||
):
|
||||
"""
|
||||
Set the range of the color bar.
|
||||
@ -931,6 +945,17 @@ class BECImageShow(RPCBase):
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_autorange_mode(self, mode: "Literal['max', 'mean']", name: "str" = None):
|
||||
"""
|
||||
Set the autoscale mode of the image, that decides how the vrange of the color bar is scaled.
|
||||
Choose betwen 'max' -> min/max of the data, 'mean' -> mean +/- fudge_factor*std of the data (fudge_factor~2).
|
||||
|
||||
Args:
|
||||
mode(str): The autoscale mode of the image.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
|
||||
@rpc_call
|
||||
def set_monitor(self, monitor: "str", name: "str" = None):
|
||||
"""
|
||||
|
@ -12,7 +12,11 @@ from qtpy.QtWidgets import QWidget
|
||||
|
||||
from bec_widgets.utils import EntryValidator
|
||||
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem, ImageItemConfig
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import ImageProcessor, ProcessorWorker
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import (
|
||||
ImageProcessor,
|
||||
ImageStats,
|
||||
ProcessorWorker,
|
||||
)
|
||||
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||
|
||||
|
||||
@ -35,6 +39,7 @@ class BECImageShow(BECPlotBase):
|
||||
"set_vrange",
|
||||
"set_color_map",
|
||||
"set_autorange",
|
||||
"set_autorange_mode",
|
||||
"set_monitor",
|
||||
"set_processing",
|
||||
"set_image_properties",
|
||||
@ -86,6 +91,7 @@ class BECImageShow(BECPlotBase):
|
||||
# Connect signals and slots
|
||||
thread.started.connect(lambda: worker.process_image(device, image))
|
||||
worker.processed.connect(self.update_image)
|
||||
worker.stats.connect(self.update_vrange)
|
||||
worker.finished.connect(thread.quit)
|
||||
worker.finished.connect(thread.wait)
|
||||
worker.finished.connect(worker.deleteLater)
|
||||
@ -341,6 +347,17 @@ class BECImageShow(BECPlotBase):
|
||||
"""
|
||||
self.apply_setting_to_images("set_autorange", args=[enable], kwargs={}, image_id=name)
|
||||
|
||||
def set_autorange_mode(self, mode: Literal["max", "mean"], name: str = None):
|
||||
"""
|
||||
Set the autoscale mode of the image, that decides how the vrange of the color bar is scaled.
|
||||
Choose betwen 'max' -> min/max of the data, 'mean' -> mean +/- fudge_factor*std of the data (fudge_factor~2).
|
||||
|
||||
Args:
|
||||
mode(str): The autoscale mode of the image.
|
||||
name(str): The name of the image. If None, apply to all images.
|
||||
"""
|
||||
self.apply_setting_to_images("set_autorange_mode", args=[mode], kwargs={}, image_id=name)
|
||||
|
||||
def set_monitor(self, monitor: str, name: str = None):
|
||||
"""
|
||||
Set the monitor of the image.
|
||||
@ -461,6 +478,7 @@ class BECImageShow(BECPlotBase):
|
||||
else:
|
||||
data = self.processor.process_image(data)
|
||||
self.update_image(device, data)
|
||||
self.update_vrange(device, self.processor.config.stats)
|
||||
|
||||
@pyqtSlot(str, np.ndarray)
|
||||
def update_image(self, device: str, data: np.ndarray):
|
||||
@ -474,6 +492,18 @@ class BECImageShow(BECPlotBase):
|
||||
image_to_update = self._images["device_monitor"][device]
|
||||
image_to_update.updateImage(data, autoLevels=image_to_update.config.autorange)
|
||||
|
||||
@pyqtSlot(str, ImageStats)
|
||||
def update_vrange(self, device: str, stats: ImageStats):
|
||||
"""
|
||||
Update the scaling of the image.
|
||||
|
||||
Args:
|
||||
stats(ImageStats): The statistics of the image.
|
||||
"""
|
||||
image_to_update = self._images["device_monitor"][device]
|
||||
if image_to_update.config.autorange:
|
||||
image_to_update.auto_update_vrange(stats)
|
||||
|
||||
def _connect_device_monitor(self, monitor: str):
|
||||
"""
|
||||
Connect to the device monitor.
|
||||
|
@ -7,7 +7,7 @@ import pyqtgraph as pg
|
||||
from pydantic import Field
|
||||
|
||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import ProcessingConfig
|
||||
from bec_widgets.widgets.figure.plots.image.image_processor import ImageStats, ProcessingConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_widgets.widgets.figure.plots.image.image import BECImageShow
|
||||
@ -20,13 +20,16 @@ class ImageItemConfig(ConnectionConfig):
|
||||
color_map: Optional[str] = Field("magma", description="The color map of the image.")
|
||||
downsample: Optional[bool] = Field(True, description="Whether to downsample the image.")
|
||||
opacity: Optional[float] = Field(1.0, description="The opacity of the image.")
|
||||
vrange: Optional[tuple[int, int]] = Field(
|
||||
vrange: Optional[tuple[float, float]] = Field(
|
||||
None, description="The range of the color bar. If None, the range is automatically set."
|
||||
)
|
||||
color_bar: Optional[Literal["simple", "full"]] = Field(
|
||||
"simple", description="The type of the color bar."
|
||||
)
|
||||
autorange: Optional[bool] = Field(True, description="Whether to autorange the color bar.")
|
||||
autorange_mode: Optional[Literal["max", "mean"]] = Field(
|
||||
"mean", description="Whether to use the mean of the image for autoscaling."
|
||||
)
|
||||
processing: ProcessingConfig = Field(
|
||||
default_factory=ProcessingConfig, description="The post processing of the image."
|
||||
)
|
||||
@ -43,6 +46,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
"set_transpose",
|
||||
"set_opacity",
|
||||
"set_autorange",
|
||||
"set_autorange_mode",
|
||||
"set_color_map",
|
||||
"set_auto_downsample",
|
||||
"set_monitor",
|
||||
@ -101,6 +105,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
- log
|
||||
- rot
|
||||
- transpose
|
||||
- autorange_mode
|
||||
"""
|
||||
method_map = {
|
||||
"downsample": self.set_auto_downsample,
|
||||
@ -112,6 +117,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
"log": self.set_log,
|
||||
"rot": self.set_rotation,
|
||||
"transpose": self.set_transpose,
|
||||
"autorange_mode": self.set_autorange_mode,
|
||||
}
|
||||
for key, value in kwargs.items():
|
||||
if key in method_map:
|
||||
@ -175,9 +181,18 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
autorange(bool): Whether to autorange the color bar.
|
||||
"""
|
||||
self.config.autorange = autorange
|
||||
if self.color_bar is not None:
|
||||
if self.color_bar and autorange:
|
||||
self.color_bar.autoHistogramRange()
|
||||
|
||||
def set_autorange_mode(self, mode: Literal["max", "mean"] = "mean"):
|
||||
"""
|
||||
Set the autorange mode to scale the vrange of the color bar. Choose between min/max or mean +/- std.
|
||||
|
||||
Args:
|
||||
mode(Literal["max","mean"]): Max for min/max or mean for mean +/- std.
|
||||
"""
|
||||
self.config.autorange_mode = mode
|
||||
|
||||
def set_color_map(self, cmap: str = "magma"):
|
||||
"""
|
||||
Set the color map of the image.
|
||||
@ -212,7 +227,29 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
"""
|
||||
self.config.monitor = monitor
|
||||
|
||||
def set_vrange(self, vmin: float = None, vmax: float = None, vrange: tuple[int, int] = None):
|
||||
def auto_update_vrange(self, stats: ImageStats) -> None:
|
||||
"""Auto update of the vrange base on the stats of the image.
|
||||
|
||||
Args:
|
||||
stats(ImageStats): The stats of the image.
|
||||
"""
|
||||
fumble_factor = 2
|
||||
if self.config.autorange_mode == "mean":
|
||||
vmin = max(stats.mean - fumble_factor * stats.std, 0)
|
||||
vmax = stats.mean + fumble_factor * stats.std
|
||||
self.set_vrange(vmin, vmax, change_autorange=False)
|
||||
return
|
||||
if self.config.autorange_mode == "max":
|
||||
self.set_vrange(max(stats.minimum, 0), stats.maximum, change_autorange=False)
|
||||
return
|
||||
|
||||
def set_vrange(
|
||||
self,
|
||||
vmin: float = None,
|
||||
vmax: float = None,
|
||||
vrange: tuple[float, float] = None,
|
||||
change_autorange: bool = True,
|
||||
):
|
||||
"""
|
||||
Set the range of the color bar.
|
||||
|
||||
@ -224,11 +261,13 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
||||
vmin, vmax = vrange
|
||||
self.setLevels([vmin, vmax])
|
||||
self.config.vrange = (vmin, vmax)
|
||||
if change_autorange:
|
||||
self.config.autorange = False
|
||||
if self.color_bar is not None:
|
||||
if self.config.color_bar == "simple":
|
||||
self.color_bar.setLevels(low=vmin, high=vmax)
|
||||
elif self.config.color_bar == "full":
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
self.color_bar.setLevels(min=vmin, max=vmax)
|
||||
self.color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
@ -7,6 +8,16 @@ from pydantic import BaseModel, Field
|
||||
from qtpy.QtCore import QObject, Signal, Slot
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImageStats:
|
||||
"""Container to store stats of an image."""
|
||||
|
||||
maximum: float
|
||||
minimum: float
|
||||
mean: float
|
||||
std: float
|
||||
|
||||
|
||||
class ProcessingConfig(BaseModel):
|
||||
fft: Optional[bool] = Field(False, description="Whether to perform FFT on the monitor data.")
|
||||
log: Optional[bool] = Field(False, description="Whether to perform log on the monitor data.")
|
||||
@ -20,6 +31,10 @@ class ProcessingConfig(BaseModel):
|
||||
None, description="The rotation angle of the monitor data before displaying."
|
||||
)
|
||||
model_config: dict = {"validate_assignment": True}
|
||||
stats: ImageStats = Field(
|
||||
ImageStats(maximum=0, minimum=0, mean=0, std=0),
|
||||
description="The statistics of the image data.",
|
||||
)
|
||||
|
||||
|
||||
class ImageProcessor:
|
||||
@ -97,6 +112,18 @@ class ImageProcessor:
|
||||
# def center_of_mass(self, data: np.ndarray) -> tuple: # TODO check functionality
|
||||
# return np.unravel_index(np.argmax(data), data.shape)
|
||||
|
||||
def update_image_stats(self, data: np.ndarray) -> None:
|
||||
"""Get the statistics of the image data.
|
||||
|
||||
Args:
|
||||
data(np.ndarray): The image data.
|
||||
|
||||
"""
|
||||
self.config.stats.maximum = np.max(data)
|
||||
self.config.stats.minimum = np.min(data)
|
||||
self.config.stats.mean = np.mean(data)
|
||||
self.config.stats.std = np.std(data)
|
||||
|
||||
def process_image(self, data: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Process the data according to the configuration.
|
||||
@ -115,6 +142,7 @@ class ImageProcessor:
|
||||
data = self.transpose(data)
|
||||
if self.config.log:
|
||||
data = self.log(data)
|
||||
self.update_image_stats(data)
|
||||
return data
|
||||
|
||||
|
||||
@ -124,6 +152,7 @@ class ProcessorWorker(QObject):
|
||||
"""
|
||||
|
||||
processed = Signal(str, np.ndarray)
|
||||
stats = Signal(str, ImageStats)
|
||||
stopRequested = Signal()
|
||||
finished = Signal()
|
||||
|
||||
@ -147,6 +176,7 @@ class ProcessorWorker(QObject):
|
||||
self._isRunning = False
|
||||
if not self._isRunning:
|
||||
self.processed.emit(device, processed_image)
|
||||
self.stats.emit(self.processor.config.stats)
|
||||
self.finished.emit()
|
||||
|
||||
def stop(self):
|
||||
|
Reference in New Issue
Block a user