Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ All notable changes to the `cuvis` Python wrapper are documented here.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Entry wording follows the conventions in [CONTRIBUTING.md](CONTRIBUTING.md#changelog-conventions) - every API entry names the fully qualified symbol first, then states the change with one of the fixed predicates.

Versions are `MAJOR.MINOR.PATCH.TWEAK`.
`MAJOR.MINOR.PATCH` is the cuvis SDK release the wrapper targets; `TWEAK` counts wrapper-only revisions against that same SDK.
Versions are `WORLD.MAJOR.MINOR.PATCH`.
`WORLD.MAJOR.MINOR` is the cuvis SDK release the wrapper targets; `PATCH` counts wrapper-only revisions against that same SDK.
See [CONTRIBUTING.md](CONTRIBUTING.md#version-scheme) for the full scheme.

Entries for versions released before this file existed were reconstructed from the published PyPI artifacts and from an AST-level diff of the public `cuvis` API surface between the corresponding commits.
Expand All @@ -15,6 +15,14 @@ Pre-releases (`b*`, `rc*`) are not listed.

### Added

- `cuvis.ReferenceType.WhiteSpectrum` - new enum member.
- `cuvis.ReferenceType.TargetSpectrum` - new enum member.
- `cuvis.ProcessingContext.set_reference` - new parameter `effective_bit_depth: int | None = None`.
- `cuvis.ProcessingContext.set_reference` - new parameter `integration_time: float = 0.0`.
- `cuvis.ProcessingContext.get_reference_spectrum` - new method.
Returns the white or target reference spectrum as an `ImageData`, or `None` when the slot is empty.
Target values are reflectance fractions (1.0 = 100 percent); the white spectrum additionally carries `effective_bit_depth` and `integration_time` attributes.
- `tests/` - behavior tests for the reference spectra: a 0.5 reflectivity target halves the reflectance cube, white spectrum and white measurement clear each other, four times the white counts quarter the cube, and both spectra survive a legacy `.cu3` save behind `.cu3sp` sidecar links and the stored cube is bit-identical after reload.
- `CI` - `.github/workflows/ci.yml` runs the test suite and a lint job enforcing `ruff check` and `ruff format --check` on every pull request and on every push to `develop` and `main`.
- `CI` - `.github/workflows/release.yml` is driven by `v*.*.*.*` tags: it validates the tag against `pyproject.toml` and against this file, builds, publishes to TestPyPI, and publishes to PyPI plus a GitHub Release after manual approval.
- `CI` - `scripts/check_changelog.py` validates this file's structure (header format, allowed section names, descending versions) and the tag/version/changelog agreement at release time.
Expand All @@ -36,6 +44,8 @@ Pre-releases (`b*`, `rc*`) are not listed.
### Changed

- Whole tree reformatted with `ruff format`; no behaviour change.
- `cuvis.ProcessingContext.set_reference` - type changed from `(mesu: Measurement, refType: ReferenceType)` to `(data: Measurement | ImageData | tuple, refType: ReferenceType)`.
The spectrum reference types take spectrum data instead of a `Measurement`; the first parameter is renamed from `mesu` to `data`.
- `README.md` - documents the version scheme, and lists Python 3.14 among the supported interpreters as `pyproject.toml` already did.
- `prebuild.py` - writes `cuvis/git-hash.txt` instead of `git-hash.txt` at the repository root, so the file lands inside the package that declares it as package data.
- `cuvis.General.init` - parameter `settings_path` type changed from `str` to `Union[str, Path, SdkSettings]`.
Expand Down
143 changes: 140 additions & 3 deletions cuvis/ProcessingContext.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .FileWriteSettings import ProcessingArgs
from .Measurement import Measurement
from .SessionFile import SessionFile
from .cube_utils import ImageData
from .cuvis_aux import SDKException
from .cuvis_types import ReferenceType, ProcessingMode

Expand All @@ -11,6 +12,35 @@

import dataclasses

import numpy as np

_SPECTRUM_REFERENCES = (ReferenceType.WhiteSpectrum, ReferenceType.TargetSpectrum)


def _spectrum_arrays(data: ImageData | tuple) -> tuple[np.ndarray, np.ndarray]:
"""(wavelengths_nm, values) as validated 1-d arrays out of an ImageData or a pair."""
if isinstance(data, ImageData):
if data.wavelength is None:
raise ValueError("The spectrum ImageData carries no wavelengths.")
wavelengths = np.asarray(data.wavelength).reshape(-1)
values = np.asarray(data.array).reshape(-1)
else:
try:
wavelengths, values = data
except (TypeError, ValueError):
raise TypeError(
"A reference spectrum is an ImageData with wavelengths, or a"
" (wavelengths, values) pair of one-dimensional arrays."
)
wavelengths = np.asarray(wavelengths).reshape(-1)
values = np.asarray(values).reshape(-1)
if wavelengths.size == 0 or wavelengths.size != values.size:
raise ValueError(
"A reference spectrum needs equally many wavelengths and values,"
f" got {wavelengths.size} and {values.size}."
)
return np.ascontiguousarray(wavelengths, dtype=np.float32), values


class ProcessingContext(object):
def __init__(
Expand Down Expand Up @@ -60,9 +90,75 @@ def apply(self, mesu: Measurement) -> Measurement:
raise SDKException("Can only apply ProcessingContext to Measurement!")
pass

def set_reference(self, mesu: Measurement, refType: ReferenceType) -> None:
if cuvis_il.status_ok != cuvis_il.cuvis_proc_cont_set_reference(
self._handle, mesu._handle, internal.__CuvisReferenceType__[refType]
def set_reference(
self,
data: Measurement | ImageData | tuple,
refType: ReferenceType,
*,
effective_bit_depth: int | None = None,
integration_time: float = 0.0,
) -> None:
"""Set a reference for processing.

The classic reference types take a Measurement. The two spectrum types take an
ImageData carrying wavelengths, or a (wavelengths, values) pair of arrays, with
wavelengths in nanometres:

- TargetSpectrum: reflectance values as fractions, 1.0 meaning 100 percent.
- WhiteSpectrum: raw sensor counts (uint16); effective_bit_depth (1 to 16) is
required, integration_time [ms] describes the recording.
"""
if refType not in _SPECTRUM_REFERENCES:
if effective_bit_depth is not None or integration_time:
raise TypeError(
"Spectrum metadata only applies to WhiteSpectrum and TargetSpectrum references."
)
if not isinstance(data, Measurement):
raise TypeError(f"Reference type {refType} takes a Measurement.")
if cuvis_il.status_ok != cuvis_il.cuvis_proc_cont_set_reference(
self._handle, data._handle, internal.__CuvisReferenceType__[refType]
):
raise SDKException()
return

if isinstance(data, Measurement):
raise TypeError(
f"Reference type {refType} takes spectrum data"
" (an ImageData with wavelengths, or a (wavelengths, values) pair)."
)
wavelengths, values = _spectrum_arrays(data)

if refType is ReferenceType.TargetSpectrum:
if effective_bit_depth is not None or integration_time:
raise TypeError(
"Counts metadata does not apply to the target spectrum."
)
values = np.ascontiguousarray(values, dtype=np.float32)
if (
cuvis_il.status_ok
!= cuvis_il.cuvis_proc_cont_set_reference_target_spectrum_swig(
self._handle, wavelengths, values
)
):
raise SDKException()
return

if effective_bit_depth is None:
raise ValueError(
"A white counts spectrum needs effective_bit_depth (1 to 16)."
)
if values.min() < 0 or values.max() > 0xFFFF:
raise ValueError("White spectrum counts must fit uint16 (0 to 65535).")
values = np.ascontiguousarray(values, dtype=np.uint16)
if (
cuvis_il.status_ok
!= cuvis_il.cuvis_proc_cont_set_reference_white_spectrum_swig(
self._handle,
wavelengths,
values,
int(effective_bit_depth),
float(integration_time),
)
):
raise SDKException()
pass
Expand All @@ -75,6 +171,8 @@ def clear_reference(self, refType: ReferenceType) -> None:
pass

def get_reference(self, refType: ReferenceType) -> Measurement:
"""The reference measurement, or None. Spectrum references are not measurements;
for those use get_reference_spectrum."""
has_ref = self.has_reference(refType)
if not has_ref:
return None
Expand All @@ -85,6 +183,45 @@ def get_reference(self, refType: ReferenceType) -> Measurement:
raise SDKException()
return Measurement(cuvis_il.p_int_value(_ptr))

def get_reference_spectrum(self, refType: ReferenceType) -> ImageData:
"""The reference spectrum as an ImageData (shape 1 x 1 x channels, wavelengths in
nanometres), or None when the slot is empty.

Only ReferenceType.WhiteSpectrum and ReferenceType.TargetSpectrum are spectra;
other types live in get_reference. For the white spectrum the returned ImageData
additionally carries the effective_bit_depth and integration_time it was set
with, as plain attributes (they do not survive slicing or arithmetic).
"""
if refType not in _SPECTRUM_REFERENCES:
raise ValueError(
f"Reference type {refType} is not a spectrum; use get_reference."
)
if not self.has_reference(refType):
return None
if refType is ReferenceType.TargetSpectrum:
status, wavelengths, values = (
cuvis_il.cuvis_proc_cont_get_reference_target_spectrum_swig(
self._handle
)
)
metadata = {}
else:
status, wavelengths, values, bit_depth, integration_time = (
cuvis_il.cuvis_proc_cont_get_reference_white_spectrum_swig(self._handle)
)
metadata = {
"effective_bit_depth": bit_depth,
"integration_time": integration_time,
}
if cuvis_il.status_ok != status:
raise SDKException()
spectrum = ImageData.from_array(
values, wavelength=[float(wl) for wl in wavelengths]
)
for key, value in metadata.items():
setattr(spectrum, key, value)
return spectrum

def has_reference(self, refType: ReferenceType) -> bool:
_ptr = cuvis_il.new_p_int()
if cuvis_il.status_ok != cuvis_il.cuvis_proc_cont_has_reference(
Expand Down
4 changes: 4 additions & 0 deletions cuvis/cuvis_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ class ReferenceType(Enum):
WhiteDark = 3
SpRad = 4
Distance = 5
WhiteSpectrum = 6
TargetSpectrum = 7


__CuvisReferenceType__ = {
Expand All @@ -93,6 +95,8 @@ class ReferenceType(Enum):
ReferenceType.WhiteDark: cuvis_il.Reference_WhiteDark,
ReferenceType.SpRad: cuvis_il.Reference_SpRad,
ReferenceType.Distance: cuvis_il.Reference_Distance,
ReferenceType.WhiteSpectrum: cuvis_il.Reference_WhiteSpectrum,
ReferenceType.TargetSpectrum: cuvis_il.Reference_TargetSpectrum,
}

__ReferenceType__ = __inverseTranslationDict(__CuvisReferenceType__)
Expand Down
Loading
Loading