0
0
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:
2024-06-24 17:57:51 +02:00
committed by wyzula_j
parent d62da494c8
commit df812eaad5
4 changed files with 131 additions and 7 deletions

View File

@ -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):
"""

View File

@ -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.

View File

@ -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)

View File

@ -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):