diff --git a/cuvis/Async.py b/cuvis/Async.py index 4c64570..dd2ed26 100644 --- a/cuvis/Async.py +++ b/cuvis/Async.py @@ -5,9 +5,10 @@ import asyncio as a -from typing import Tuple, Optional, Union +from typing import Optional, Union from datetime import timedelta + def _to_ms(value: Union[int, timedelta]) -> int: if isinstance(value, timedelta): return int(value / timedelta(milliseconds=1)) @@ -23,14 +24,15 @@ def __init__(self, handle): pass - def get(self, timeout_ms: Union[int, timedelta] ) -> Tuple[Optional[Measurement], AsyncResult]: + def get(self, timeout_ms: Union[int, timedelta]) -> tuple[Optional[Measurement], AsyncResult]: """ """ _ptr = cuvis_il.new_p_int() _pmesu = cuvis_il.new_p_int() cuvis_il.p_int_assign(_ptr, self._handle) - res = cuvis_il.cuvis_async_capture_get(_ptr, _to_ms(timeout_ms), _pmesu) + res = cuvis_il.cuvis_async_capture_get( + _ptr, _to_ms(timeout_ms), _pmesu) if res == cuvis_il.status_ok: return Measurement(cuvis_il.p_int_value(_pmesu)), AsyncResult.done @@ -42,14 +44,14 @@ def get(self, timeout_ms: Union[int, timedelta] ) -> Tuple[Optional[Measurement] return None, AsyncResult.timeout else: raise SDKException() - + # Python Magic Methods def __await__(self) -> Optional[Measurement]: async def _wait_for_return(): _status_ptr = cuvis_il.new_p_cuvis_status_t() while True: - if cuvis_il.status_ok != cuvis_il.cuvis_async_capture_status(self._handle,_status_ptr): + if cuvis_il.status_ok != cuvis_il.cuvis_async_capture_status(self._handle, _status_ptr): raise SDKException() status = cuvis_il.p_cuvis_status_t_value(_status_ptr) if status == cuvis_il.status_ok: @@ -57,7 +59,7 @@ async def _wait_for_return(): else: await a.sleep(10.0 / 1000) return _wait_for_return().__await__() - + def __del__(self): _ptr = cuvis_il.new_p_int() cuvis_il.p_int_assign(_ptr, self._handle) @@ -89,14 +91,13 @@ def get(self, timeout_ms: Union[int, timedelta]) -> AsyncResult: raise SDKException() pass - # Python Magic Methods def __await__(self) -> AsyncResult: async def _wait_for_return(): _status_ptr = cuvis_il.new_p_cuvis_status_t() while True: - if cuvis_il.status_ok != cuvis_il.cuvis_async_call_status(self._handle,_status_ptr): + if cuvis_il.status_ok != cuvis_il.cuvis_async_call_status(self._handle, _status_ptr): raise SDKException() status = cuvis_il.p_cuvis_status_t_value(_status_ptr) if status == cuvis_il.status_ok: @@ -104,7 +105,7 @@ async def _wait_for_return(): else: await a.sleep(10.0 / 1000) return _wait_for_return().__await__() - + def __del__(self): _ptr = cuvis_il.new_p_int() cuvis_il.p_int_assign(_ptr, self._handle) diff --git a/cuvis/Measurement.py b/cuvis/Measurement.py index acc5680..e1baddd 100644 --- a/cuvis/Measurement.py +++ b/cuvis/Measurement.py @@ -1,11 +1,14 @@ -from typing import Union, List +from typing import Union from .FileWriteSettings import SaveArgs import datetime import os +import numpy as np from ._cuvis_il import cuvis_il from .cuvis_aux import SDKException, SessionData, Capabilities, MeasurementFlags, SensorInfo, GPSData from .cuvis_types import DataFormat, ProcessingMode, ReferenceType +from .cube_utils import ImageData + import cuvis.cuvis_types as internal base_datetime = datetime.datetime(1970, 1, 1) @@ -30,6 +33,7 @@ class Measurement(object): def __init__(self, base: Union[int, str]): self._handle = None + self._session = None if isinstance(base, int): self._handle = base @@ -129,6 +133,42 @@ def set_name(self, name: str) -> None: self._refresh_metadata() pass + @property + def cube(self) -> ImageData: + """ + Retrieves or processes the 'cube' data for this Measurement. + + This property prioritizes convenience over strict design principles: + - Attempts to retrieve the 'cube' from `self.data`. + - Lazily initializes a `ProcessingContext` if a session is available but uninitialized. + - May trigger expensive processing and modify internal state during property access. + + While functional, this approach introduces side effects and tight coupling, making it less + predictable and not the cleanest solution. Suitable for specific workflows where these + trade-offs are acceptable. + + Raises + ------ + ValueError + If the 'cube' is not available and processing is not possible. + + Returns + ------- + ImageData + The 'cube' data, either retrieved from `self.data` or generated through processing. + """ + if 'cube' in self.data: + return self.data.get('cube') + if self._session is not None: + # try fallback if session is known + if self._session._pc is None: + from .ProcessingContext import ProcessingContext + self._session._pc = ProcessingContext(self._session) + self._session._pc.apply(self) + return self.data.get('cube', None) + raise ValueError( + "This Measurement does not have a cube saved. Consider reprocessing with a Processing Context.") + @property def thumbnail(self): thumb = [val for key, val in self.data.items() if "view" in key] @@ -194,48 +234,3 @@ def __del__(self): cuvis_il.cuvis_measurement_free(_ptr) self._handle = cuvis_il.p_int_value(_ptr) pass - - -class ImageData(object): - def __init__(self, img_buf=None, dformat=None): - - if img_buf is None: - - self.width = None - self.height = None - self.channels = None - self.array = None - self.wavelength = None - - elif isinstance(img_buf, cuvis_il.cuvis_imbuffer_t): - - if dformat is None: - raise TypeError("Missing format for reading image buffer") - - if img_buf.format == 1: - self.array = cuvis_il.cuvis_read_imbuf_uint8(img_buf) - elif img_buf.format == 2: - self.array = cuvis_il.cuvis_read_imbuf_uint16(img_buf) - elif img_buf.format == 3: - self.array = cuvis_il.cuvis_read_imbuf_uint32(img_buf) - elif img_buf.format == 4: - self.array = cuvis_il.cuvis_read_imbuf_float32(img_buf) - else: - raise SDKException() - - self.width = img_buf.width - self.height = img_buf.height - self.channels = img_buf.channels - - if img_buf.wavelength is not None: - self.wavelength = [ - cuvis_il.p_unsigned_int_getitem( - img_buf.wavelength, z) for z - in - range(self.channels)] - - # print("got image of size {}.".format(self.array.shape)) - - else: - raise TypeError( - "Wrong data type for image buffer: {}".format(type(img_buf))) diff --git a/cuvis/SessionFile.py b/cuvis/SessionFile.py index 07eea1a..d6f7e8f 100644 --- a/cuvis/SessionFile.py +++ b/cuvis/SessionFile.py @@ -10,9 +10,11 @@ from typing import Union, Optional + class SessionFile(object): - def __init__(self, base: Union[Path,str]): + def __init__(self, base: Union[Path, str]): self._handle = None + self._pc = None if isinstance(Path(base), Path) and os.path.exists(base): _ptr = cuvis_il.new_p_int() if cuvis_il.status_ok != cuvis_il.cuvis_session_file_load(base, @@ -23,40 +25,41 @@ def __init__(self, base: Union[Path,str]): raise SDKException( "Could not open SessionFile File! File not found!") - pass - - def get_measurement(self, frameNo: int, itemtype: SessionItemType = SessionItemType.no_gaps) -> Optional[Measurement]: + def get_measurement(self, frameNo: int = 0, itemtype: SessionItemType = SessionItemType.no_gaps) -> Optional[Measurement]: _ptr = cuvis_il.new_p_int() - ret = cuvis_il.cuvis_session_file_get_mesu(self._handle, frameNo, internal.__CuvisSessionItemType__[itemtype], - _ptr) + ret = cuvis_il.cuvis_session_file_get_mesu(self._handle, frameNo, internal.__CuvisSessionItemType__[itemtype], + _ptr) if cuvis_il.status_no_measurement == ret: return None if cuvis_il.status_ok != ret: raise SDKException() - return Measurement(cuvis_il.p_int_value(_ptr)) - - def get_reference(self, frameNo: int, reftype: ReferenceType) -> Optional[Measurement]: + mesu = Measurement(cuvis_il.p_int_value(_ptr)) + mesu._session = self + return mesu + + def get_reference(self, frameNo: int, reftype: ReferenceType) -> Optional[Measurement]: _ptr = cuvis_il.new_p_int() ret = cuvis_il.cuvis_session_file_get_reference_mesu( - self._handle, frameNo, internal.__CuvisReferenceType__[reftype], - _ptr) + self._handle, frameNo, internal.__CuvisReferenceType__[reftype], + _ptr) if cuvis_il.status_no_measurement == ret: return None if cuvis_il.status_ok != ret: raise SDKException() return Measurement(cuvis_il.p_int_value(_ptr)) - - def get_thumbnail(self) -> ImageData: + + @property + def thumbnail(self) -> ImageData: thumbnail_data = cuvis_il.cuvis_view_data_t() if cuvis_il.status_ok != cuvis_il.cuvis_session_file_get_thumbnail(self, thumbnail_data): raise SDKException() - + if thumbnail_data.data.format == CUVIS_imbuffer_format["imbuffer_format_uint8"]: return ImageData(img_buf=thumbnail_data.data, - dformat=thumbnail_data.data.format) + dformat=thumbnail_data.data.format) else: raise SDKException("Unsupported viewer bit depth!") - + def get_size(self, itemtype: SessionItemType = SessionItemType.no_gaps) -> int: val = cuvis_il.new_p_int() if cuvis_il.status_ok != cuvis_il.cuvis_session_file_get_size( @@ -79,20 +82,20 @@ def operation_mode(self) -> OperationMode: self._handle, val): raise SDKException() return internal.__OperationMode__[cuvis_il.p_cuvis_operation_mode_t_value(val)] - + @property def hash(self) -> str: return cuvis_il.cuvis_session_file_get_hash_swig(self._handle) - + # Python Magic Methods def __iter__(self): for i in range(len(self)): yield self[i] pass - + def __len__(self): return self.get_size() - + def __getitem__(self, key: int) -> Measurement: return self.get_measurement(key) diff --git a/cuvis/Viewer.py b/cuvis/Viewer.py index 0ea9f23..66d1aa6 100644 --- a/cuvis/Viewer.py +++ b/cuvis/Viewer.py @@ -5,7 +5,7 @@ from .FileWriteSettings import ViewerSettings -from typing import Union, Dict +from typing import Union class Viewer(object): @@ -25,7 +25,7 @@ def __init__(self, settings: Union[int, ViewerSettings]): type(settings))) pass - def _create_view_data(self, new_handle: int) -> Dict[str, ImageData]: + def _create_view_data(self, new_handle: int) -> Union[dict[str, ImageData], ImageData]: _ptr = cuvis_il.new_p_int() if cuvis_il.status_ok != cuvis_il.cuvis_view_get_data_count( @@ -47,11 +47,13 @@ def _create_view_data(self, new_handle: int) -> Dict[str, ImageData]: dformat=view_data.data.format) else: raise SDKException("Unsupported viewer bit depth!") - # TODO when is a good point to release the view - # cuvis_il.cuvis_view_free(_ptr) - return view_array + if len(view_array.keys()) == 1: + # if only one value is available, do not wrap in dictionary + return list(view_array.values())[0] + else: + return view_array - def apply(self, mesu: Measurement) -> Dict[str, ImageData]: + def apply(self, mesu: Measurement) -> dict[str, ImageData]: _ptr = cuvis_il.new_p_int() if cuvis_il.status_ok != cuvis_il.cuvis_viewer_apply(self._handle, mesu._handle, _ptr): diff --git a/cuvis/Worker.py b/cuvis/Worker.py index b822797..34c2f87 100644 --- a/cuvis/Worker.py +++ b/cuvis/Worker.py @@ -13,7 +13,7 @@ from .doc import copydoc from dataclasses import dataclass -from typing import Callable, Awaitable, Tuple +from typing import Callable, Awaitable @dataclass diff --git a/cuvis/__init__.py b/cuvis/__init__.py index bde3037..e20a2cb 100644 --- a/cuvis/__init__.py +++ b/cuvis/__init__.py @@ -14,6 +14,7 @@ from .Export import CubeExporter, EnviExporter, TiffExporter, ViewExporter from .Calibration import Calibration from .AcquisitionContext import AcquisitionContext +from .cube_utils import ImageData import os import platform import sys diff --git a/cuvis/cube_utils.py b/cuvis/cube_utils.py new file mode 100644 index 0000000..d0da2e4 --- /dev/null +++ b/cuvis/cube_utils.py @@ -0,0 +1,111 @@ +from typing import Union +from ._cuvis_il import cuvis_il +import numpy as np +from .cuvis_aux import SDKException + + +class ImageData(object): + def __init__(self, img_buf=None, dformat=None): + + if img_buf is None: + + self.width = None + self.height = None + self.channels = None + self.array = None + self.wavelength = None + + elif isinstance(img_buf, cuvis_il.cuvis_imbuffer_t): + + if dformat is None: + raise TypeError("Missing format for reading image buffer") + + if img_buf.format == 1: + self.array = cuvis_il.cuvis_read_imbuf_uint8(img_buf) + elif img_buf.format == 2: + self.array = cuvis_il.cuvis_read_imbuf_uint16(img_buf) + elif img_buf.format == 3: + self.array = cuvis_il.cuvis_read_imbuf_uint32(img_buf) + elif img_buf.format == 4: + self.array = cuvis_il.cuvis_read_imbuf_float32(img_buf) + else: + raise SDKException() + + self.width = img_buf.width + self.height = img_buf.height + self.channels = img_buf.channels + + if img_buf.wavelength is not None: + self.wavelength = [ + cuvis_il.p_unsigned_int_getitem( + img_buf.wavelength, z) for z + in + range(self.channels)] + + # print("got image of size {}.".format(self.array.shape)) + + else: + raise TypeError( + "Wrong data type for image buffer: {}".format(type(img_buf))) + + def __getitem__(self, key) -> Union[np.ndarray, tuple[np.ndarray, np.ndarray], object]: + """ + Enables slicing and indexing of the image data. + Example: + pixel, wavelengths = image_data[100, 50] # Single pixel spectrum plus wavelengths + band_slice = image_data[:, :, 10:20] # Subset of Image and Bands results in a new ImageData object + single_channel = image_data[:,:,10] # Single Channel returns a normal numpy array + """ + if self.array is None: + raise ValueError("Image array is not initialized.") + sliced_array = self.array[key] + + if sliced_array.ndim == 1: + start_band, end_band = self._get_band_range(key) + return sliced_array, self.wavelength[start_band:end_band] + elif sliced_array.ndim == 2: + return sliced_array + elif sliced_array.ndim == 3 and sliced_array.shape[-1] > 1: + if self.wavelength is None: + raise ValueError("Wavelength data is not available.") + start_band, end_band = self._get_band_range(key) + sliced_wavelength = self.wavelength[start_band:end_band] + return ImageData.from_array( + sliced_array, + width=sliced_array.shape[1], + height=sliced_array.shape[0], + channels=sliced_array.shape[2], + wavelength=sliced_wavelength, + ) + + def _get_band_range(self, key): + """ + Helper method to determine the band range based on the slicing key. + """ + if isinstance(key, tuple) and len(key) == 3: + if isinstance(key[2], slice): + start = key[2].start or 0 + stop = key[2].stop or self.channels + return start, stop + elif isinstance(key[2], int): + return key[2], key[2] + 1 + return 0, self.channels + + def to_numpy(self) -> np.ndarray: + """ + Returns the spectral data as a NumPy array. + """ + return self.array + + @classmethod + def from_array(cls, array: np.ndarray, width: int, height: int, channels: int, wavelength=None): + """ + Creates an ImageData instance from a NumPy array and metadata. + """ + instance = cls() + instance.array = array + instance.width = width + instance.height = height + instance.channels = channels + instance.wavelength = wavelength + return instance diff --git a/cuvis/cuvis_aux.py b/cuvis/cuvis_aux.py index 5f918c6..c63c288 100644 --- a/cuvis/cuvis_aux.py +++ b/cuvis/cuvis_aux.py @@ -1,7 +1,7 @@ from dataclasses import dataclass import cuvis.cuvis_types as internal -from typing import List, Union +from typing import Union from ._cuvis_il import cuvis_il import logging import datetime @@ -150,7 +150,7 @@ def all(self): def __init__(self, value): self._value = value - def strings(self) -> List[str]: + def strings(self) -> list[str]: """"Returns a list containing the string values of the current members of the Bitset""" return _bit_translate(self._value, type(self)._translation_dict) @@ -182,7 +182,7 @@ def __contains__(self, member): raise ValueError(f'Cannot call operator with type {type(member)}') @classmethod - def from_strings(cls, *values: List[str]): + def from_strings(cls, *values: list[str]): """" Creates a Bitset from a list of strings """ return cls(sum([cls._translation_dict[v] for v in values])) diff --git a/setup.py b/setup.py index e12589f..21a324e 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ here = os.path.abspath(os.path.dirname(__file__)) NAME = 'cuvis' -VERSION = '3.3.0.post1' +VERSION = '3.3.1rc1' DESCRIPTION = 'CUVIS Python SDK.'