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
|
- log
|
||||||
- rot
|
- rot
|
||||||
- transpose
|
- transpose
|
||||||
|
- autorange_mode
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
@ -767,6 +768,15 @@ class BECImageItem(RPCBase):
|
|||||||
autorange(bool): Whether to autorange the color bar.
|
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
|
@rpc_call
|
||||||
def set_color_map(self, cmap: "str" = "magma"):
|
def set_color_map(self, cmap: "str" = "magma"):
|
||||||
"""
|
"""
|
||||||
@ -796,7 +806,11 @@ class BECImageItem(RPCBase):
|
|||||||
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def set_vrange(
|
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.
|
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.
|
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
|
@rpc_call
|
||||||
def set_monitor(self, monitor: "str", name: "str" = None):
|
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.utils import EntryValidator
|
||||||
from bec_widgets.widgets.figure.plots.image.image_item import BECImageItem, ImageItemConfig
|
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
|
from bec_widgets.widgets.figure.plots.plot_base import BECPlotBase, SubplotConfig
|
||||||
|
|
||||||
|
|
||||||
@ -35,6 +39,7 @@ class BECImageShow(BECPlotBase):
|
|||||||
"set_vrange",
|
"set_vrange",
|
||||||
"set_color_map",
|
"set_color_map",
|
||||||
"set_autorange",
|
"set_autorange",
|
||||||
|
"set_autorange_mode",
|
||||||
"set_monitor",
|
"set_monitor",
|
||||||
"set_processing",
|
"set_processing",
|
||||||
"set_image_properties",
|
"set_image_properties",
|
||||||
@ -86,6 +91,7 @@ class BECImageShow(BECPlotBase):
|
|||||||
# Connect signals and slots
|
# Connect signals and slots
|
||||||
thread.started.connect(lambda: worker.process_image(device, image))
|
thread.started.connect(lambda: worker.process_image(device, image))
|
||||||
worker.processed.connect(self.update_image)
|
worker.processed.connect(self.update_image)
|
||||||
|
worker.stats.connect(self.update_vrange)
|
||||||
worker.finished.connect(thread.quit)
|
worker.finished.connect(thread.quit)
|
||||||
worker.finished.connect(thread.wait)
|
worker.finished.connect(thread.wait)
|
||||||
worker.finished.connect(worker.deleteLater)
|
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)
|
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):
|
def set_monitor(self, monitor: str, name: str = None):
|
||||||
"""
|
"""
|
||||||
Set the monitor of the image.
|
Set the monitor of the image.
|
||||||
@ -461,6 +478,7 @@ class BECImageShow(BECPlotBase):
|
|||||||
else:
|
else:
|
||||||
data = self.processor.process_image(data)
|
data = self.processor.process_image(data)
|
||||||
self.update_image(device, data)
|
self.update_image(device, data)
|
||||||
|
self.update_vrange(device, self.processor.config.stats)
|
||||||
|
|
||||||
@pyqtSlot(str, np.ndarray)
|
@pyqtSlot(str, np.ndarray)
|
||||||
def update_image(self, device: str, data: 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 = self._images["device_monitor"][device]
|
||||||
image_to_update.updateImage(data, autoLevels=image_to_update.config.autorange)
|
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):
|
def _connect_device_monitor(self, monitor: str):
|
||||||
"""
|
"""
|
||||||
Connect to the device monitor.
|
Connect to the device monitor.
|
||||||
|
@ -7,7 +7,7 @@ import pyqtgraph as pg
|
|||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
from bec_widgets.utils import BECConnector, ConnectionConfig
|
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:
|
if TYPE_CHECKING:
|
||||||
from bec_widgets.widgets.figure.plots.image.image import BECImageShow
|
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.")
|
color_map: Optional[str] = Field("magma", description="The color map of the image.")
|
||||||
downsample: Optional[bool] = Field(True, description="Whether to downsample 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.")
|
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."
|
None, description="The range of the color bar. If None, the range is automatically set."
|
||||||
)
|
)
|
||||||
color_bar: Optional[Literal["simple", "full"]] = Field(
|
color_bar: Optional[Literal["simple", "full"]] = Field(
|
||||||
"simple", description="The type of the color bar."
|
"simple", description="The type of the color bar."
|
||||||
)
|
)
|
||||||
autorange: Optional[bool] = Field(True, description="Whether to autorange 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(
|
processing: ProcessingConfig = Field(
|
||||||
default_factory=ProcessingConfig, description="The post processing of the image."
|
default_factory=ProcessingConfig, description="The post processing of the image."
|
||||||
)
|
)
|
||||||
@ -43,6 +46,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
|||||||
"set_transpose",
|
"set_transpose",
|
||||||
"set_opacity",
|
"set_opacity",
|
||||||
"set_autorange",
|
"set_autorange",
|
||||||
|
"set_autorange_mode",
|
||||||
"set_color_map",
|
"set_color_map",
|
||||||
"set_auto_downsample",
|
"set_auto_downsample",
|
||||||
"set_monitor",
|
"set_monitor",
|
||||||
@ -101,6 +105,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
|||||||
- log
|
- log
|
||||||
- rot
|
- rot
|
||||||
- transpose
|
- transpose
|
||||||
|
- autorange_mode
|
||||||
"""
|
"""
|
||||||
method_map = {
|
method_map = {
|
||||||
"downsample": self.set_auto_downsample,
|
"downsample": self.set_auto_downsample,
|
||||||
@ -112,6 +117,7 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
|||||||
"log": self.set_log,
|
"log": self.set_log,
|
||||||
"rot": self.set_rotation,
|
"rot": self.set_rotation,
|
||||||
"transpose": self.set_transpose,
|
"transpose": self.set_transpose,
|
||||||
|
"autorange_mode": self.set_autorange_mode,
|
||||||
}
|
}
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
if key in method_map:
|
if key in method_map:
|
||||||
@ -175,9 +181,18 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
|||||||
autorange(bool): Whether to autorange the color bar.
|
autorange(bool): Whether to autorange the color bar.
|
||||||
"""
|
"""
|
||||||
self.config.autorange = autorange
|
self.config.autorange = autorange
|
||||||
if self.color_bar is not None:
|
if self.color_bar and autorange:
|
||||||
self.color_bar.autoHistogramRange()
|
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"):
|
def set_color_map(self, cmap: str = "magma"):
|
||||||
"""
|
"""
|
||||||
Set the color map of the image.
|
Set the color map of the image.
|
||||||
@ -212,7 +227,29 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
|||||||
"""
|
"""
|
||||||
self.config.monitor = monitor
|
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.
|
Set the range of the color bar.
|
||||||
|
|
||||||
@ -224,11 +261,13 @@ class BECImageItem(BECConnector, pg.ImageItem):
|
|||||||
vmin, vmax = vrange
|
vmin, vmax = vrange
|
||||||
self.setLevels([vmin, vmax])
|
self.setLevels([vmin, vmax])
|
||||||
self.config.vrange = (vmin, vmax)
|
self.config.vrange = (vmin, vmax)
|
||||||
self.config.autorange = False
|
if change_autorange:
|
||||||
|
self.config.autorange = False
|
||||||
if self.color_bar is not None:
|
if self.color_bar is not None:
|
||||||
if self.config.color_bar == "simple":
|
if self.config.color_bar == "simple":
|
||||||
self.color_bar.setLevels(low=vmin, high=vmax)
|
self.color_bar.setLevels(low=vmin, high=vmax)
|
||||||
elif self.config.color_bar == "full":
|
elif self.config.color_bar == "full":
|
||||||
|
# pylint: disable=unexpected-keyword-arg
|
||||||
self.color_bar.setLevels(min=vmin, max=vmax)
|
self.color_bar.setLevels(min=vmin, max=vmax)
|
||||||
self.color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
|
self.color_bar.setHistogramRange(vmin - 0.1 * vmin, vmax + 0.1 * vmax)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -7,6 +8,16 @@ from pydantic import BaseModel, Field
|
|||||||
from qtpy.QtCore import QObject, Signal, Slot
|
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):
|
class ProcessingConfig(BaseModel):
|
||||||
fft: Optional[bool] = Field(False, description="Whether to perform FFT on the monitor data.")
|
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.")
|
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."
|
None, description="The rotation angle of the monitor data before displaying."
|
||||||
)
|
)
|
||||||
model_config: dict = {"validate_assignment": True}
|
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:
|
class ImageProcessor:
|
||||||
@ -97,6 +112,18 @@ class ImageProcessor:
|
|||||||
# def center_of_mass(self, data: np.ndarray) -> tuple: # TODO check functionality
|
# def center_of_mass(self, data: np.ndarray) -> tuple: # TODO check functionality
|
||||||
# return np.unravel_index(np.argmax(data), data.shape)
|
# 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:
|
def process_image(self, data: np.ndarray) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Process the data according to the configuration.
|
Process the data according to the configuration.
|
||||||
@ -115,6 +142,7 @@ class ImageProcessor:
|
|||||||
data = self.transpose(data)
|
data = self.transpose(data)
|
||||||
if self.config.log:
|
if self.config.log:
|
||||||
data = self.log(data)
|
data = self.log(data)
|
||||||
|
self.update_image_stats(data)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@ -124,6 +152,7 @@ class ProcessorWorker(QObject):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
processed = Signal(str, np.ndarray)
|
processed = Signal(str, np.ndarray)
|
||||||
|
stats = Signal(str, ImageStats)
|
||||||
stopRequested = Signal()
|
stopRequested = Signal()
|
||||||
finished = Signal()
|
finished = Signal()
|
||||||
|
|
||||||
@ -147,6 +176,7 @@ class ProcessorWorker(QObject):
|
|||||||
self._isRunning = False
|
self._isRunning = False
|
||||||
if not self._isRunning:
|
if not self._isRunning:
|
||||||
self.processed.emit(device, processed_image)
|
self.processed.emit(device, processed_image)
|
||||||
|
self.stats.emit(self.processor.config.stats)
|
||||||
self.finished.emit()
|
self.finished.emit()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
Reference in New Issue
Block a user