Source code for sigima.objects.image.creation

# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
Image creation utilities
========================

This module provides functions and parameter classes for creating new images.

The module includes:

- `create_image`: Factory function for creating ImageObj instances
- `ImageDatatypes`: Enumeration of supported image data types
- `ImageTypes`: Enumeration of supported image generation types
- `NewImageParam` and subclasses: Parameter classes for image generation
- Factory functions and registration utilities

These utilities support creating images from various sources:
- Raw NumPy arrays
- Synthetic data (zeros, random distributions, analytical functions)
- Parameterized image generation
"""

# pylint: disable=invalid-name  # Allows short reference names like x, y, ...
# pylint: disable=duplicate-code

from __future__ import annotations

from typing import Type

import guidata.dataset as gds
import numpy as np

from sigima.config import _
from sigima.objects import base
from sigima.objects.image.object import ImageObj
from sigima.tools.image import scale_data_to_min_max


[docs] def create_image( title: str, data: np.ndarray | None = None, metadata: dict | None = None, units: tuple | None = None, labels: tuple | None = None, ) -> ImageObj: """Create a new Image object Args: title: image title data: image data metadata: image metadata units: X, Y, Z units (tuple of strings) labels: X, Y, Z labels (tuple of strings) Returns: Image object """ assert isinstance(title, str) assert data is None or isinstance(data, np.ndarray) image = ImageObj(title=title) image.title = title image.data = data if units is not None: image.xunit, image.yunit, image.zunit = units if labels is not None: image.xlabel, image.ylabel, image.zlabel = labels if metadata is not None: image.metadata.update(metadata) return image
[docs] class ImageDatatypes(gds.LabeledEnum): """Image data types"""
[docs] @classmethod def from_numpy_dtype(cls: type[ImageDatatypes], dtype: np.dtype) -> ImageDatatypes: """Return ImageDatatypes member from NumPy dtype Args: dtype: NumPy dtype object Returns: Corresponding ImageDatatypes member """ dtype_str = str(dtype) for member in cls: if member.value == dtype_str: return member return cls.UINT8 # Default fallback
[docs] def to_numpy_dtype(self) -> np.dtype: """Return the corresponding NumPy dtype object. This is the symmetrical counterpart to from_numpy_dtype(). Returns: NumPy dtype object that can be used directly with numpy functions. """ return np.dtype(self.value)
[docs] @classmethod def check(cls: type[ImageDatatypes]) -> None: """Check if data types are valid""" for member in cls: assert hasattr(np, member.value)
#: Unsigned integer number stored with 8 bits UINT8 = "uint8" #: Unsigned integer number stored with 16 bits UINT16 = "uint16" #: Signed integer number stored with 16 bits INT16 = "int16" #: Float number stored with 32 bits FLOAT32 = "float32" #: Float number stored with 64 bits FLOAT64 = "float64"
ImageDatatypes.check()
[docs] class ImageTypes(gds.LabeledEnum): """Image types.""" #: Image filled with zero ZEROS = "zero", _("Zero") #: Image filled with random data (normal distribution) NORMAL_DISTRIBUTION = "normal_distribution", _("Normal distribution") #: Image filled with random data (Poisson distribution) POISSON_DISTRIBUTION = "poisson_distribution", _("Poisson distribution") #: Image filled with random data (uniform distribution) UNIFORM_DISTRIBUTION = "uniform_distribution", _("Uniform distribution") #: 2D Gaussian image GAUSS = "gauss", _("Gaussian") #: Bilinear form image RAMP = "ramp", _("2D ramp") #: Checkerboard pattern CHECKERBOARD = "checkerboard", _("Checkerboard") #: Sinusoidal grating pattern SINUSOIDAL_GRATING = "sinusoidal_grating", _("Sinusoidal grating") #: Ring/circular pattern RING = "ring", _("Ring pattern") #: Siemens star pattern SIEMENS_STAR = "siemens_star", _("Siemens star") #: 2D sinc function SINC = "sinc", _("2D sinc")
DEFAULT_TITLE = _("Untitled image")
[docs] class NewImageParam(gds.DataSet): """New image dataset. Subclasses can optionally implement a ``generate_title()`` method to provide automatic title generation based on their parameters. This method should return a string containing the generated title, or an empty string if no title can be generated. Example:: def generate_title(self) -> str: '''Generate a title based on current parameters.''' return f"MyImage(param1={self.param1},param2={self.param2})" """ hide_height = False hide_width = False hide_dtype = False hide_type = False title = gds.StringItem(_("Title"), default=DEFAULT_TITLE) height = gds.IntItem( _("Height"), default=1024, help=_("Image height: number of rows"), min=1 ).set_prop("display", hide=gds.GetAttrProp("hide_height")) width = gds.IntItem( _("Width"), default=1024, help=_("Image width: number of columns"), min=1 ).set_prop("display", col=1, hide=gds.GetAttrProp("hide_width")) dtype = gds.ChoiceItem( _("Type"), ImageDatatypes, default=ImageDatatypes.FLOAT64, help=_("Image data type"), ).set_prop("display", hide=gds.GetAttrProp("hide_dtype")) xlabel = gds.StringItem(_("X label"), default="") xunit = gds.StringItem(_("X unit"), default="").set_prop("display", col=1) ylabel = gds.StringItem(_("Y label"), default="") yunit = gds.StringItem(_("Y unit"), default="").set_prop("display", col=1) zlabel = gds.StringItem(_("Z label"), default="") zunit = gds.StringItem(_("Z unit"), default="").set_prop("display", col=1)
[docs] def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array """ return np.zeros(shape, dtype=self.dtype.to_numpy_dtype())
IMAGE_TYPE_PARAM_CLASSES = {} def register_image_parameters_class(itype: ImageTypes, param_class) -> None: """Register a parameters class for a given image type. Args: itype: image type param_class: parameters class """ IMAGE_TYPE_PARAM_CLASSES[itype] = param_class def __get_image_parameters_class(itype: ImageTypes) -> Type[NewImageParam]: """Get parameters class for a given image type. Args: itype: image type Returns: Parameters class Raises: ValueError: if no parameters class is registered for the given image type """ try: return IMAGE_TYPE_PARAM_CLASSES[itype] except KeyError as exc: raise ValueError( f"Image type {itype} has no parameters class registered" ) from exc def check_all_image_parameters_classes() -> None: """Check all registered parameters classes.""" for itype, param_class in IMAGE_TYPE_PARAM_CLASSES.items(): assert __get_image_parameters_class(itype) is param_class
[docs] def create_image_parameters( itype: ImageTypes, title: str | None = None, height: int | None = None, width: int | None = None, idtype: ImageDatatypes | None = None, xlabel: str | None = None, ylabel: str | None = None, zlabel: str | None = None, xunit: str | None = None, yunit: str | None = None, zunit: str | None = None, **kwargs: dict, ) -> NewImageParam: """Create parameters for a given image type. Args: itype: image type title: image title height: image height (number of rows) width: image width (number of columns) idtype: image data type (`ImageDatatypes` member) xlabel: X axis label ylabel: Y axis label zlabel: Z axis label xunit: X axis unit yunit: Y axis unit zunit: Z axis unit **kwargs: additional parameters (specific to the image type) Returns: Parameters object for the given image type """ pclass = __get_image_parameters_class(itype) p = pclass.create(**kwargs) if title is not None: p.title = title if height is not None: p.height = height if width is not None: p.width = width if idtype is not None: assert isinstance(idtype, ImageDatatypes) p.dtype = idtype if xlabel is not None: p.xlabel = xlabel if ylabel is not None: p.ylabel = ylabel if zlabel is not None: p.zlabel = zlabel if xunit is not None: p.xunit = xunit if yunit is not None: p.yunit = yunit if zunit is not None: p.zunit = zunit return p
[docs] class Zero2DParam(NewImageParam, title=_("Zero")): """Image parameters for a 2D image filled with zero.""" def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array """ return np.zeros(shape, dtype=self.dtype.to_numpy_dtype())
register_image_parameters_class(ImageTypes.ZEROS, Zero2DParam)
[docs] class UniformDistribution2DParam( NewImageParam, base.UniformDistributionParam, title=_("Uniform distribution") ): """Uniform-distribution image parameters.""" def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array """ rng = np.random.default_rng(self.seed) assert self.vmin is not None assert self.vmax is not None data = scale_data_to_min_max(rng.random(shape), self.vmin, self.vmax) return data.astype(self.dtype.to_numpy_dtype())
register_image_parameters_class( ImageTypes.UNIFORM_DISTRIBUTION, UniformDistribution2DParam )
[docs] class NormalDistribution2DParam( NewImageParam, base.NormalDistributionParam, title=_("Normal distribution") ): """Normal-distribution image parameters.""" def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array. """ rng = np.random.default_rng(self.seed) assert self.mu is not None assert self.sigma is not None data: np.ndarray = rng.normal(self.mu, self.sigma, shape) return data.astype(self.dtype.to_numpy_dtype())
register_image_parameters_class( ImageTypes.NORMAL_DISTRIBUTION, NormalDistribution2DParam )
[docs] class PoissonDistribution2DParam( NewImageParam, base.PoissonDistributionParam, title=_("Poisson distribution") ): """Poisson-distribution image parameters.""" def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array. """ rng = np.random.default_rng(self.seed) assert self.lam is not None data: np.ndarray = rng.poisson(lam=self.lam, size=shape) return data.astype(self.dtype.to_numpy_dtype())
register_image_parameters_class( ImageTypes.POISSON_DISTRIBUTION, PoissonDistribution2DParam )
[docs] class Gauss2DParam( NewImageParam, title=_("Gaussian"), comment="z = A exp(-((√((x - x<sub>0</sub>)<sup>2</sup> + " "(y - y<sub>0</sub>)<sup>2</sup>) - μ)<sup>2</sup>) / (2 σ<sup>2</sup>))", ): """2D Gaussian parameters.""" a = gds.FloatItem("A", default=None, check=False) xmin = gds.FloatItem("x<sub>min</sub>", default=-10.0).set_pos(col=1) sigma = gds.FloatItem("σ", default=1.0) xmax = gds.FloatItem("x<sub>max</sub>", default=10.0).set_pos(col=1) mu = gds.FloatItem("μ", default=0.0) ymin = gds.FloatItem("y<sub>min</sub>", default=-10.0).set_pos(col=1) x0 = gds.FloatItem("x<sub>0</sub>", default=0.0) ymax = gds.FloatItem("y<sub>max</sub>", default=10.0).set_pos(col=1) y0 = gds.FloatItem("y<sub>0</sub>", default=0.0).set_pos(col=0, colspan=1) def generate_title(self) -> str: """Generate a title based on current parameters.""" return ( f"Gauss(a={self.a:g},μ={self.mu:g}," f"σ={self.sigma:g}),x0={self.x0:g},y0={self.y0:g})" ) def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array """ if self.a is None: try: self.a = np.iinfo(self.dtype.to_numpy_dtype()).max / 2.0 except ValueError: self.a = 10.0 x, y = np.meshgrid( np.linspace(self.xmin, self.xmax, shape[1]), np.linspace(self.ymin, self.ymax, shape[0]), ) data = self.a * np.exp( -((np.sqrt((x - self.x0) ** 2 + (y - self.y0) ** 2) - self.mu) ** 2) / (2.0 * self.sigma**2) ) return np.array(data, dtype=self.dtype.to_numpy_dtype())
register_image_parameters_class(ImageTypes.GAUSS, Gauss2DParam)
[docs] class Ramp2DParam( NewImageParam, title=_("2D ramp"), comment="z = A (x - x<sub>0</sub>) + B (y - y<sub>0</sub>) + C", ): """Define the parameters of a 2D ramp (planar ramp).""" _g0_begin = gds.BeginGroup(_("Coefficients")) a = gds.FloatItem("A", default=1.0).set_pos(col=0) b = gds.FloatItem("B", default=1.0).set_pos(col=1) c = gds.FloatItem("C", default=0.0).set_pos(colspan=1) x0 = gds.FloatItem("x<sub>0</sub>", default=0.0).set_pos(col=0) y0 = gds.FloatItem("y<sub>0</sub>", default=0.0).set_pos(col=1) _g0_end = gds.EndGroup("") _g1_begin = gds.BeginGroup(_("Domain")) xmin = gds.FloatItem("x<sub>min</sub>", default=-1.0).set_pos(col=0) xmax = gds.FloatItem("x<sub>max</sub>", default=1.0).set_pos(col=1) ymin = gds.FloatItem("y<sub>min</sub>", default=-1.0).set_pos(col=0) ymax = gds.FloatItem("y<sub>max</sub>", default=1.0).set_pos(col=1) _g1_end = gds.EndGroup("") def generate_title(self) -> str: """Generate a title based on current parameters.""" terms = [] # Build terms list for non-zero coefficients if self.a != 0.0: if self.x0 == 0.0: x_part = f"{self.a:g} x" else: x_part = f"{self.a:g} (x - {self.x0:g})" terms.append(x_part) if self.b != 0.0: if self.y0 == 0.0: y_part = f"{self.b:g} y" else: y_part = f"{self.b:g} (y - {self.y0:g})" terms.append(y_part) if self.c != 0.0 or not terms: # Include c if it's the only term terms.append(f"{self.c:g}") return f"z = {' + '.join(terms)}" def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array """ x = np.linspace(self.xmin, self.xmax, shape[1]) y = np.linspace(self.ymin, self.ymax, shape[0]) xx, yy = np.meshgrid(x, y) data = self.a * (xx - self.x0) + self.b * (yy - self.y0) + self.c return np.array(data, dtype=self.dtype.to_numpy_dtype())
register_image_parameters_class(ImageTypes.RAMP, Ramp2DParam)
[docs] class Checkerboard2DParam( NewImageParam, title=_("Checkerboard"), comment=_("Checkerboard pattern with alternating squares"), ): """Checkerboard pattern parameters.""" square_size = gds.IntItem( _("Square size"), default=64, min=1, help=_("Size of each square in pixels") ) x0 = gds.FloatItem("x<sub>0</sub>", default=0.0, help=_("X offset")) y0 = gds.FloatItem("y<sub>0</sub>", default=0.0, help=_("Y offset")).set_pos(col=1) vmin = gds.FloatItem( _("Minimum value"), default=0.0, help=_("Value for dark squares") ) vmax = gds.FloatItem( _("Maximum value"), default=255.0, help=_("Value for light squares") ).set_pos(col=1) def generate_title(self) -> str: """Generate a title based on current parameters.""" return f"Checkerboard(size={self.square_size})" def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array """ # Create coordinate arrays y = np.arange(shape[0]) - self.y0 x = np.arange(shape[1]) - self.x0 xx, yy = np.meshgrid(x, y) # Create checkerboard pattern using floor division pattern = ((xx // self.square_size) + (yy // self.square_size)) % 2 # Scale to desired range data = np.where(pattern == 0, self.vmin, self.vmax) return data.astype(self.dtype.to_numpy_dtype())
register_image_parameters_class(ImageTypes.CHECKERBOARD, Checkerboard2DParam)
[docs] class SinusoidalGrating2DParam( NewImageParam, title=_("Sinusoidal grating"), comment="z = A sin(2π(f<sub>x</sub>·x + f<sub>y</sub>·y) + φ) + C", ): """Sinusoidal grating parameters.""" _g0_begin = gds.BeginGroup(_("Amplitude and offset")) a = gds.FloatItem("A", default=100.0, help=_("Amplitude")).set_pos(col=0) c = gds.FloatItem("C", default=128.0, help=_("DC offset")).set_pos(col=1) _g0_end = gds.EndGroup("") _g1_begin = gds.BeginGroup(_("Frequency and phase")) fx = gds.FloatItem( "f<sub>x</sub>", default=0.1, help=_("Spatial frequency in X direction") ).set_pos(col=0) fy = gds.FloatItem( "f<sub>y</sub>", default=0.0, help=_("Spatial frequency in Y direction") ).set_pos(col=1) phase = gds.FloatItem("φ", default=0.0, help=_("Phase"), unit="rad").set_pos( colspan=1 ) _g1_end = gds.EndGroup("") _g2_begin = gds.BeginGroup(_("Domain")) xmin = gds.FloatItem("x<sub>min</sub>", default=0.0).set_pos(col=0) xmax = gds.FloatItem("x<sub>max</sub>", default=100.0).set_pos(col=1) ymin = gds.FloatItem("y<sub>min</sub>", default=0.0).set_pos(col=0) ymax = gds.FloatItem("y<sub>max</sub>", default=100.0).set_pos(col=1) _g2_end = gds.EndGroup("") def generate_title(self) -> str: """Generate a title based on current parameters.""" return f"Grating(fx={self.fx:g},fy={self.fy:g},φ={self.phase:g})" def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array """ x = np.linspace(self.xmin, self.xmax, shape[1]) y = np.linspace(self.ymin, self.ymax, shape[0]) xx, yy = np.meshgrid(x, y) data = self.a * np.sin(2 * np.pi * (self.fx * xx + self.fy * yy) + self.phase) data = data + self.c return data.astype(self.dtype.to_numpy_dtype())
register_image_parameters_class(ImageTypes.SINUSOIDAL_GRATING, SinusoidalGrating2DParam)
[docs] class Ring2DParam( NewImageParam, title=_("Ring pattern"), comment=_("Concentric ring pattern"), ): """Ring pattern parameters.""" x0 = gds.FloatItem("x<sub>0</sub>", default=0.0, help=_("Center X coordinate")) y0 = gds.FloatItem( "y<sub>0</sub>", default=0.0, help=_("Center Y coordinate") ).set_pos(col=1) _g0_begin = gds.BeginGroup(_("Ring parameters")) period = gds.FloatItem( _("Period"), default=50.0, min=0.1, help=_("Distance between ring centers") ) ring_width = gds.FloatItem( _("Ring width"), default=10.0, min=0.1, help=_("Width of each ring") ).set_pos(col=1) _g0_end = gds.EndGroup("") _g1_begin = gds.BeginGroup(_("Amplitude")) vmin = gds.FloatItem(_("Minimum value"), default=0.0) vmax = gds.FloatItem(_("Maximum value"), default=255.0).set_pos(col=1) _g1_end = gds.EndGroup("") _g2_begin = gds.BeginGroup(_("Domain")) xmin = gds.FloatItem("x<sub>min</sub>", default=-100.0).set_pos(col=0) xmax = gds.FloatItem("x<sub>max</sub>", default=100.0).set_pos(col=1) ymin = gds.FloatItem("y<sub>min</sub>", default=-100.0).set_pos(col=0) ymax = gds.FloatItem("y<sub>max</sub>", default=100.0).set_pos(col=1) _g2_end = gds.EndGroup("") def generate_title(self) -> str: """Generate a title based on current parameters.""" return f"Ring(period={self.period:g},width={self.ring_width:g})" def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array """ x = np.linspace(self.xmin, self.xmax, shape[1]) y = np.linspace(self.ymin, self.ymax, shape[0]) xx, yy = np.meshgrid(x, y) # Calculate distance from center r = np.sqrt((xx - self.x0) ** 2 + (yy - self.y0) ** 2) # Create ring pattern: modulo creates repeating pattern ring_phase = (r % self.period) / self.period # Create rings: value is high when ring_phase is within the ring width ring_width_fraction = self.ring_width / self.period rings = np.where(ring_phase < ring_width_fraction, 1.0, 0.0) # Scale to desired range data = self.vmin + rings * (self.vmax - self.vmin) return data.astype(self.dtype.to_numpy_dtype())
register_image_parameters_class(ImageTypes.RING, Ring2DParam)
[docs] class SiemensStar2DParam( NewImageParam, title=_("Siemens star"), comment=_("Siemens star pattern for resolution testing"), ): """Siemens star pattern parameters.""" x0 = gds.FloatItem("x<sub>0</sub>", default=0.0, help=_("Center X coordinate")) y0 = gds.FloatItem( "y<sub>0</sub>", default=0.0, help=_("Center Y coordinate") ).set_pos(col=1) n_spokes = gds.IntItem( _("Number of spokes"), default=36, min=2, help=_("Number of spoke pairs") ) _g0_begin = gds.BeginGroup(_("Radial limits")) inner_radius = gds.FloatItem( _("Inner radius"), default=0.0, min=0.0, help=_("Inner radius (hole in center)") ) outer_radius = gds.FloatItem( _("Outer radius"), default=100.0, min=0.1, help=_("Outer radius (edge of pattern)"), ).set_pos(col=1) _g0_end = gds.EndGroup("") _g1_begin = gds.BeginGroup(_("Amplitude")) vmin = gds.FloatItem(_("Minimum value"), default=0.0) vmax = gds.FloatItem(_("Maximum value"), default=255.0).set_pos(col=1) _g1_end = gds.EndGroup("") _g2_begin = gds.BeginGroup(_("Domain")) xmin = gds.FloatItem("x<sub>min</sub>", default=-100.0).set_pos(col=0) xmax = gds.FloatItem("x<sub>max</sub>", default=100.0).set_pos(col=1) ymin = gds.FloatItem("y<sub>min</sub>", default=-100.0).set_pos(col=0) ymax = gds.FloatItem("y<sub>max</sub>", default=100.0).set_pos(col=1) _g2_end = gds.EndGroup("") def generate_title(self) -> str: """Generate a title based on current parameters.""" return f"Siemens(n={self.n_spokes})" def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array """ x = np.linspace(self.xmin, self.xmax, shape[1]) y = np.linspace(self.ymin, self.ymax, shape[0]) xx, yy = np.meshgrid(x, y) # Calculate polar coordinates r = np.sqrt((xx - self.x0) ** 2 + (yy - self.y0) ** 2) theta = np.arctan2(yy - self.y0, xx - self.x0) # Create spoke pattern: alternating black and white spokes # Normalize angle to [0, 2π] and create pattern theta_normalized = (theta + np.pi) / (2 * np.pi) # Now in [0, 1] spoke_pattern = np.floor(theta_normalized * self.n_spokes * 2) % 2 # Apply radial mask radial_mask = (r >= self.inner_radius) & (r <= self.outer_radius) # Combine pattern with mask data = np.where(radial_mask, spoke_pattern, 0.5) # 0.5 for outside region # Scale to desired range data = self.vmin + data * (self.vmax - self.vmin) return data.astype(self.dtype.to_numpy_dtype())
register_image_parameters_class(ImageTypes.SIEMENS_STAR, SiemensStar2DParam)
[docs] class Sinc2DParam( NewImageParam, title=_("2D sinc"), comment="z = A sinc(√((x - x<sub>0</sub>)<sup>2</sup> + " "(y - y<sub>0</sub>)<sup>2</sup>) / σ) + C", ): """2D sinc function parameters.""" _g0_begin = gds.BeginGroup(_("Amplitude and offset")) a = gds.FloatItem("A", default=100.0, help=_("Amplitude")).set_pos(col=0) c = gds.FloatItem("C", default=0.0, help=_("DC offset")).set_pos(col=1) _g0_end = gds.EndGroup("") _g1_begin = gds.BeginGroup(_("Center and scale")) x0 = gds.FloatItem("x<sub>0</sub>", default=0.0, help=_("Center X coordinate")) y0 = gds.FloatItem( "y<sub>0</sub>", default=0.0, help=_("Center Y coordinate") ).set_pos(col=1) sigma = gds.FloatItem("σ", default=10.0, min=0.1, help=_("Scale factor")).set_pos( colspan=1 ) _g1_end = gds.EndGroup("") _g2_begin = gds.BeginGroup(_("Domain")) xmin = gds.FloatItem("x<sub>min</sub>", default=-50.0).set_pos(col=0) xmax = gds.FloatItem("x<sub>max</sub>", default=50.0).set_pos(col=1) ymin = gds.FloatItem("y<sub>min</sub>", default=-50.0).set_pos(col=0) ymax = gds.FloatItem("y<sub>max</sub>", default=50.0).set_pos(col=1) _g2_end = gds.EndGroup("") def generate_title(self) -> str: """Generate a title based on current parameters.""" return f"Sinc(σ={self.sigma:g},x0={self.x0:g},y0={self.y0:g})" def generate_2d_data(self, shape: tuple[int, int]) -> np.ndarray: """Generate 2D data based on current parameters. Args: shape: Tuple (height, width) for the output array. Returns: 2D data array """ x = np.linspace(self.xmin, self.xmax, shape[1]) y = np.linspace(self.ymin, self.ymax, shape[0]) xx, yy = np.meshgrid(x, y) # Calculate radial distance from center r = np.sqrt((xx - self.x0) ** 2 + (yy - self.y0) ** 2) # Calculate sinc function: sinc(x) = sin(x)/x, with special case for x=0 # Scale by sigma r_scaled = r / self.sigma # Use numpy's sinc which is defined as sin(pi*x)/(pi*x) # We want sin(x)/x, so we divide by pi data = np.where(r_scaled == 0, 1.0, np.sin(r_scaled) / r_scaled) # Apply amplitude and offset data = self.a * data + self.c return data.astype(self.dtype.to_numpy_dtype())
register_image_parameters_class(ImageTypes.SINC, Sinc2DParam) check_all_image_parameters_classes() IMG_NB = 0 def get_next_image_number(): """Get the next image number. This function is used to keep track of the number of signals created. It is typically used to generate unique titles for new signals. Returns: int: new image number """ global IMG_NB # pylint: disable=global-statement IMG_NB += 1 return IMG_NB
[docs] def create_image_from_param(param: NewImageParam) -> ImageObj: """Create a new Image object from parameters. Args: param: new image parameters Returns: Image object Raises: NotImplementedError: if the image type is not supported """ if param.height is None: param.height = 1024 if param.width is None: param.width = 1024 if param.dtype is None: param.dtype = ImageDatatypes.UINT16 # Generate data first, as some `generate_title()` methods may depend on it: shape = (param.height, param.width) data = param.generate_2d_data(shape) # Check if user has customized the title or left it as default/empty use_generated_title = not param.title or param.title == DEFAULT_TITLE if use_generated_title: # Try to generate a descriptive title gen_title = getattr(param, "generate_title", lambda: "")() if gen_title: title = gen_title else: # No generated title available, use default with number title = f"{DEFAULT_TITLE} {get_next_image_number()}" else: # User has set a custom title, use it as-is title = param.title image = create_image( title, data, units=(param.xunit, param.yunit, param.zunit), labels=(param.xlabel, param.ylabel, param.zlabel), ) return image