Repository navigation
Releases
v0.9.0a5 — 2D plane mode and CUDA install extras
Fifth pre-release of the 0.9 series. The Allen Cell Structure Segmenter workflows now also segment a single 2D plane, and GPU support installs through a pip extra. pip install cubic continues to resolve 0.8.0 until 0.9.0 ships.
Plane mode for workflow_sec61b and workflow_tomm20 (#59)
Both workflows now accept a 2D (Y, X) plane as well as a 3D (Z, Y, X) stack, and return a mask of the input's shape on the input's device. Any other dimensionality raises ValueError.
- A plane is segmented as a one-plane volume
(1, Y, X). The result is exactly whataicssegmentationreturns for that one-plane input, with GPU equal to CPU. - This is not the same as slicing the 3D mask. In a volume, normalization uses volume statistics, smoothing is 3D, the vesselness response is combined with the z-MIP, and the size filter uses 3D connectivity. Segmenting a plane alone drops all of these couplings.
- To keep some z-context, pass a few neighbouring planes as a small volume, for example
workflow_sec61b(vol[z - 1 : z + 2])[1]. - 3D input is unchanged: masks are bitwise identical to 0.9.0a4.
On one GPU, a 640x960 plane takes about 5 ms for SEC61B and 3 ms for TOMM20, against 0.13 s and 0.08 s for a 48x640x960 volume.
cuda12 and cuda13 extras for GPU support (#60)
GPU support no longer needs CuPy, cuCIM and a CUDA Toolkit installed by hand. On Linux, install one extra:
pip install --pre 'cubic[cuda13]' # driver with CUDA 13.x and a Turing or newer GPU
pip install --pre 'cubic[cuda12]' # driver with CUDA 12.x, or a Maxwell, Pascal or Volta GPU- Each extra installs CuPy 14+ and cuCIM, plus the CUDA runtime libraries from PyPI, so only the NVIDIA driver is needed.
nvidia-smishows the driver's CUDA version. - cuCIM publishes Linux wheels only.
- Install one extra, into an environment with no other
cupy-cuda*package. uv refuses to combine the two, but pip does not. uv sync --all-extrasnow errors on this project. Name the extras you want instead.- When CuPy or cuCIM fails to import, the CPU-fallback warning now quotes the original error and names the extras. CuPy also raises an import error when it is installed but cannot load its CUDA libraries.
v0.9.0a4 — GPU segmenter workflows and metric kernels
Fourth pre-release of the 0.9 series: GPU ports of two Allen Cell Structure Segmenter workflows, and fused GPU kernels for MicroMS3IM, per-region GLCM texture and FRC/FSC bin sums. pip install cubic continues to resolve 0.8.0 until 0.9.0 ships.
Existing APIs keep their results: the new kernels are used only on GPU input and agree with the array path to floating-point reduction order (about 1e-15 relative).
Allen Cell Structure Segmenter workflows (#57)
New cubic.segmentation.workflow_sec61b and workflow_tomm20 are device-agnostic ports of the aicssegmentation ER and mitochondria classic workflows, including ITK's gradient anisotropic diffusion and the slice-wise 2D vesselness filter, rewritten as whole-volume array operations with CUDA kernels on GPU.
- For float32 input, masks were bitwise equal to
aicssegmentationon every volume tested: 250 light-sheet ER and mitochondria z-stacks of 48x640x960 voxels, acquired and predicted. - On an NVIDIA A40, one such volume takes 0.37 s for SEC61B (up to 3 s when the serial-
Kreplay triggers) against 85-88 s for the CPU reference, and 0.2 s for TOMM20 against 28-29 s. - The reference itself is not bitwise across CPUs: on AVX-512 hosts NumPy's float32
np.powerrounds differently and moves 1-3 voxels per volume. The CPU path follows the host; the GPU kernel reproduces glibc'spowfand so matches the AVX2 result. - The match is measured, not guaranteed for every input; see
_K_SERIAL_MARGINfor the one heuristic involved.
Metric kernels (#58)
- MicroMS3IM. The RI-factor fit evaluates its objective in one fused GPU reduction and stops the bisection at its float fixed point (56 objective evaluations instead of 204, identical alpha). New
MicroMS3IM.score_stack(gt, pred)scores every slice of an(N, H, W)stack in batched passes, each slice scoring whatscoregives it alone. - Per-region GLCM. New
cubic.feature.glcm_features_by_label(image, labels, ...)computes Haralick features for every labeled region in one pass per direction, matchingglcm_featuresper region. Regions without a voxel pair, or holding a non-finite voxel, return NaN rows. Histogram memory is bounded by processing labels in chunks. - FRC/FSC bin sums. Radial and sectioned bin sums use a shared-memory histogram kernel on GPUs of compute capability 6.0 and above, falling back to
bincountotherwise.
Measured on an NVIDIA A40:
| step | before | after |
|---|---|---|
| MicroMS3IM fit, 576 slices of 634x954 | 45.9 s | 1.5 s |
| MicroMS3IM scoring, 480 slices of 640x960 | 4.92 s | 0.72 s |
| GLCM, 48x640x960 volume with 13 labeled regions | 0.60 s (per-region loop) | 0.13 s |
v0.9.0a3 — single-image FRC/FSC split validity and ordering
Third pre-release of the 0.9 series: single-image FRC/FSC now warns when a split is used outside its assumptions, judges the sampling limit per direction in physical units, and splits raw counts before isotropic resampling instead of after. pip install cubic continues to resolve 0.8.0 until 0.9.0 ships.
split_type="checkerboard" remains the default; existing checkerboard workflows are unchanged in behavior and cost.
Single-image splits warn outside their assumptions (#55)
Single-image FRC/FSC has assumptions the API cannot check for the caller, and previously returned a plausible-looking number when any was violated.
binomial_split now warns when counts_mode='poisson_thinning' is handed non-negative integers. Thinning draws two Poisson variates from a shared rate, so the halves keep a high-frequency correlation floor (~0.33 measured on simulated counts) that can stop the curve ever crossing 1/7 and return nan with no explanation. This is a value-domain test, so it has no false positives on valid input.
Two inferential checks — per-axis bandwidth and a variance-vs-mean slope — were tried and removed after both misfired on synthetic counts. The module constants record why, and why neither interpolated nor deconvolved input is detectable from the array alone. Value sampling strides every axis, so a stack is not judged from its first plane, and caps at 2M values to stay off the critical path.
Sampling floor is per direction, in length (#56)
_CHECKERBOARD_VALIDITY_RADIUS = 0.9 was replaced. A normalized radius cannot express a physical, per-direction criterion, and 0.9 was wrong in three ways that share that cause:
- Too loose. Reported resolution is
2·d_px / (r·f(r)), so Rieger'sd_min ≥ 4·d_pxis already breached atr = 0.797and Koho's2√2·d_pxatr = 0.883, whiler = 0.9still reports2.56·d_px— inside both. Everything in[0.797, 0.9)broke the criterion the warning itself quoted. - Wrong axis. The frequency grid is normalized by one Nyquist for the whole volume, so on anisotropic data one radius means different physical resolutions along XY and Z.
- Never deduplicated. The message interpolated the radius into its own text, so Python's once-per-location filter never merged repeats.
grid_crop_resolutionreaches it once per slice per crop; 200 varying calls now collapse to 1 warning (suite-wide count 194 → 19).
_warn_if_below_split_floor compares the measured resolution against 2√2 × the pixel size for the direction measured, judging XY and axial separately.
Binomial split runs before resampling (#56)
resample_isotropic previously ran before the binomial split, so both halves were drawn from an interpolated volume and shared source samples; 0.9.0a3's predecessor only warned about it. The split now runs first and the same interpolation is applied to each half — a linear operator applied separately to independent inputs leaves them independent.
MEASURED against two genuinely independent Poisson acquisitions of one object:
| XY error | Z error | |
|---|---|---|
| resample → split (old) | −47.4% | −0.3% |
| split → resample (new) | +8.5% | +0.5% |
The old path also fed 44% non-integer values into a counts-mode split, an interpolated volume no longer being counts. The split runs inside the repeat loop, so n_repeats still yields per-ring and resolution standard deviations.
This does not rescue every binomial case: on the Koho pollen stack the one-bit crossing is absent with or without resampling, which is a threshold problem rather than an ordering one.
Pollen reference corrected (#55)
The 0.59 / 3.91 µm pair the 3D example was validated against appears in neither the paper nor the reference implementation — 3.91 was the outermost radial tick of the Fig. 4b polar plot. miplib's own notebook prints FWHM (Z: 3.8875587137876315 µm, XY: 0.5986743538754852 µm), which Fig. 4b rounds to 0.6 × 3.9 µm. Matching the notebook's settings (resample_order=0, bin_delta=10, one-bit, spline), cubic gives Z 3.872 / XY 0.578 µm — within 0.4% and 3.5%. The gap previously blamed on the axial formula was bin width and curve-fit type.
Behavior changes
split_type="binomial"withresample_isotropic=Truenow splits before resampling rather than warning; results change on that path (see the table above).- New
RuntimeWarning/UserWarningcan fire on existing calls that were previously silent — notably a checkerboard crossing between2√2·d_pxand the old0.9radius, andpoisson_thinningon integer data. fsc_resolution,frc_resolutionand their defaults are otherwise unchanged.
Deprecated mask backend brought in line
Two paths the reworked checks did not reach, found in review of #56:
backend="mask"withsplit_type="binomial"andresample_isotropic=Trueanalysed the original anisotropic array whilespacinghad already been rewritten to isotropic values. Pre-splitting resamples only the metadata up front, deferring the voxels to the repeat loop, and the mask backend returns before that loop. MEASURED: a(12, 48, 48)volume atspacing=[0.3, 0.1, 0.1]reached the backend unsplit at its original shape withspacingreading[0.1, 0.1, 0.1]— the axial pitch understated three-fold. It now receives the pre-split, resampled pair.calculate_sectioned_fsc(public) andfsc_resolution(backend="mask"), which returns through it, applied the cut-off correction without any validity check, silently accepting sampling-limited results that warned before the rework. MEASURED: a 64³ Poisson volume reports 2.466 px in the sectors at 90° and 270°, below the 2.828 px floor, with no warning. The floor is now checked where the correction is applied, using a new_sector_pixel_sizehelper — a sector's polar angle is measured from the Z axis, so its pitch issqrt((d_z·cos θ)² + (d_xy·sin θ)²), reducing to the axis pitch at 0°, 90°, 180° and 270°.
Testing
819 passed, 2 skipped locally on an A40 (GPU paths exercised); CI green on Python 3.10/3.11/3.12. All seven example notebooks re-executed against this code and committed with outputs. The two validity tests previously poked a private correction helper with a hand-built dataset, which cannot see a normalization bug; they now drive frc_resolution end to end.
Full changes: #55, #56, 10a1cc8, b4fb140.
🤖 Generated with Claude Code
v0.9.0a2 — MicroSSIM RI-factor fit GPU memory fix
Second pre-release of the 0.9 series: a GPU-memory fix for the MicroSSIM range-invariant (RI) factor fit. pip install cubic continues to resolve 0.8.0 until 0.9.0 ships.
MicroSSIM RI-factor fit memory
MicroSSIM.fit / MicroMS3IM.fit ran out of memory on 40–48 GB GPUs for realistic calibration sets. Two causes in get_global_ri_factor / get_ri_factor:
- Per-slice elements were pooled with lists +
np.concatenate, holding both copies at once. They are now written into preallocated(N, h, w)buffers. - Each objective evaluation of the bisection built ~15 temporaries the size of the whole pooled stack. The
S/dSmeans are now reduced in bounded chunks (_CHUNK_ELEMS), recursing into slices larger than a chunk, with float64 accumulation on the device.
Measured on an A40, 576 slices of 640×960 float32: peak pool usage 40.51 GB → 12.86 GB, fitted alpha identical to 10 decimals, fit time unchanged. A single 1024² slice goes from 15.0× to 0.94× its element size above the input.
Behavior changes
- The objective's chunk sums accumulate in float64, so float32 inputs give slightly more accurate means than NumPy's float32
mean()(measured difference ~4e-8 on a (3, 64, 64) float32 stack). get_global_ri_factoron a zero-slice stack andget_ri_factoron empty element arrays raise a clearValueError(0.9.0a1 raised NumPy'sconcatenateerror and a bracketingRuntimeError, respectively).
🤖 Generated with Claude Code
v0.9.0a1 — repository-wide bug sweep (77 bugs)
First pre-release of the 0.9 series. Repository-wide bug sweep: 77 bugs fixed across all seven modules, plus stale-code removal, simplification, and tooling changes. Test suite 401 → 791 passing.
Shipped as an alpha rather than 0.9.0 final: several fixes change previously-returned numbers, ~24 public symbols are gone, and two correctness questions in the single-image FRC/FSC split are still open (see Known issues). pip install cubic continues to resolve 0.8.0 until 0.9.0 ships.
Highlights
- Silently-wrong results fixed. The worst class in the sweep: functions that returned plausible numbers instead of failing, so no test caught them.
morphology_correlationsread true-vs-true entries and never touched the prediction (identical inputs scored 0.921 instead of 1.0). Sectioned FSC reported 0.1225 µm at 0.13 µm sampling — below the physical floor. Every binomial FRC returned NaN.ms_ssimwrappeduint8arithmetic mod 256. Empty GLCM directions were averaged in as zeros, flipping a texture correlation's sign. - Even-shaped PSFs in
create_backprojectorwere wrong two independent ways: centered grids used(S-1)/2whereifftshiftputsS//2at DC (non-Hermitian mask,np.realdiscarding a large imaginary part), andf[::-1]mapsj → -j-1on even axes so the flipped PSF was not the conjugate OTF. Imaginary leakage 5.32e-1 → 1.46e-17. Odd shapes stay bit-identical to the reference. - Code that had never worked:
five_crop_resolution/grid_crop_resolutionraisedIndexErrorunconditionally;Image.to_cpu()and cross-deviceto_same_devicerejected CuPy input;remove_touching_objectsfailed on every GPU and every 2-D array; the skimage proxy had no host fallback. - Input mutation.
richardson_lucy_xpdidimage *= mask, andimg_as_floatreturns the same object for float input, so it destroyed the caller's array. Same class of bug in five other functions. - Performance, all byte-identical:
remove_touching_objects426× (5.0 s → 0.012 s),find_objects165×, mesh extraction 11×, GLCM peak GPU pool 7.5× smaller (3.95 GB → 0.53 GB at 256³).
Resolution metrics
- Sectioned FSC now projects a sector's shell radius onto Z rather than reporting it as an axial period, and uses the Koho et al. (2019) eq. (5) correction on isotropically resampled input. Reproduces the paper's Fig. 4b pollen stack at XY 0.586 µm / Z 4.38 µm against a published 0.59 / 3.91.
- New
axial_floor_factor(default 2.0) reportszasnanwhen the result falls below the pre-resampling axial Nyquist limit. - The one-image checkerboard calibration factor is clamped at 1.0. The Koho fit is an exponential centred on 0.98 that passes 1.0 at a crossing of ~0.925 and reaches 1.82 at the band edge, so a near-Nyquist crossing was reported up to 45% below the sampling limit — in both directions, and
xyhas no floor guard of its own. spacing=Nonenow means cycles-per-pixel consistently betweenradial_edgesandradial_bin_id; previously it clipped every non-DC voxel into the last bin.ssimgainsnormalize, which it had silently ignored whilepsnrandnrmsehonoured it.
Breaking changes
This is not a patch release. ~24 removed public symbols, 13 signatures with a parameter removed, mask now keyword-only on pcc/nrmse/psnr/ssim, extract_features returning three values, clahe(kernel_size→n_tiles), and ~14 inputs that were previously accepted and now raise. The cubic.metrics.frc shims are gone.
Fixes that change previously-returned numbers — re-running an analysis will not reproduce prior values:
remove_thin_objectschangedsize_z <= min_zto< min_z, matching its docstring.min_zis defaulted, so every existing caller changes: atmin_z=2, objects spanning 1/2/3/4 Z planes went from keeping{3,4}to keeping{2,3,4}.band_limited_pcc,band_limited_ssimandspectral_pccreturnnanfor degenerate input, matchingpcc.clear_xy_bordersno longer relabels in 3-D, socellpose_segmentids can be gapped where they used to be contiguous.get_true_featuresno longer derives feature ranges from the ground truth whenfeature_ranges is None— the old path normalized the true side but not the prediction.
Tooling
ruff selected only D and I, so pyflakes never ran and unused imports had accumulated; now ["D", "F", "I"]. rtree added to the mesh extra — extract_mesh_features raised ModuleNotFoundError for anyone following the documented install.
Known issues
- Single-image checkerboard split is optimistic. Against a synthetic known band limit, two-image FRC is accurate to 1–2% but the checkerboard split reads ~20% too fine, and it collapses once the true limit approaches the split halves' Nyquist — which is half the image's, since the halves are 2× coarser per axis. On interpolated input it is far worse: 0.68× after a subpixel shift, 0.35× after a Gaussian smooth, and no threshold crossing at all after 2× upsampling. The binomial split (
split_type="binomial") tracks the two-image reference to ~3% in all of those cases and is the better choice wherever photon counts are available; it is not reliable on deskewed/upsampled data either (0.40×). - Near-axis sectioned FSC does not decorrelate. With the default
disable_3d_sum=False,checkerboard_splitsums consecutive Z pairs before splitting in Y/X, so both halves share their Z content exactly and FSC on the purek_zline is identically 1.0.axial_floor_factorcatches the downstream symptom; the split geometry is the cause.
Numbers above were measured against scikit-image 0.26.0, while CI resolves 0.25.2. The library handles both, but DCR curve shapes differ enough between them that one test asserts a version-independent invariant instead of a fixture-specific value.
Install
pip install cubic==0.9.0a1Full changelog: v0.8.0...v0.9.0a1
v0.8.0
First stable release of the 0.8 series, consolidating the two v0.8.0 alpha pre-releases (v0.8.0a1–a2). 5 commits since v0.7.0.
Highlights
- Cellpose v4 DINO support — the GPU-resident segmentation path now covers all four v4 models: the SAM backbones (
cpsam,cpsam_v2) and the DINOv3 backbones (cpdino,cpdino-vitb). - Wiener-Butterworth back projector for Richardson-Lucy deconvolution — converges in ~1–2 iterations instead of 10–50 (Guo et al. 2020), with the matched path byte-for-byte unchanged by default.
New features
Segmentation
segment_cellposeis now the canonical GPU-resident entry point, supporting all four Cellpose v4 backbones (SAM + DINO).segment_cpsamis kept as a deprecated alias (emitsDeprecationWarning, slated for removal after 0.8). (#51)- Tunable tile size via a new
bsizeparameter (defaults 256 for SAM, 384 for DINO), centralized in_resolve_bsizewith cellpose's SAM→256 guard and non-positive-bsizevalidation. (#51)
Preprocessing — deconvolution
create_backprojector(cubic.preprocessing) — device-agnostic NumPy/CuPy port ofBackProjector.m, supportingtraditional,gaussian,butterworth,wiener, andwiener-butterworthtypes. (#52)backprojectortoggle onrichardson_lucy_xp, threaded throughdecon_xpy,richardson_lucy_iter, anddeconv_iter_num_finder. Faithful unmatched RL update (noH^T1, nocorrection<0clip, estimate floored at 1e-3, both projectors sum-normalized); circulant only, and the default (None) leaves the matched path unchanged. (#52)
Packaging
- Require
cellpose>=4.2.0(the release that first shipscpsam_v2/cpdino). - New
cellpose-dinoextra for thedinov3dependency (pip install cubic[cellpose-dino]).
Docs & examples
- New notebook
deconvolution_wb_backprojector_3d.ipynb— matched-vs-WB convergence by PSNR/SSIM and absolute FSC resolution (+ paired script + README row). - 3D feature-extraction notebook gains a CPU-vs-GPU comparison.
Install
pip install cubic==0.8.0Full changelog: v0.7.0...v0.8.0
v0.8.0a2 — Wiener-Butterworth back projector for fast RL deconvolution
Adds an unmatched Wiener-Butterworth (WB) back projector to Richardson-Lucy deconvolution (Guo et al. 2020, Nat. Biotechnol. 38:1337), converging in ~1–2 iterations instead of 10–50. The matched path is byte-for-byte unchanged by default.
Highlights
create_backprojector(cubic.preprocessing) — device-agnostic NumPy/CuPy port ofBackProjector.m:traditional,gaussian,butterworth,wiener,wiener-butterworth.backprojectortoggle onrichardson_lucy_xp, threaded throughdecon_xpy,richardson_lucy_iter, anddeconv_iter_num_finder. Faithful unmatched RL update (noH^T1, nocorrection<0clip, estimate floored at 1e-3, both projectors sum-normalized); circulant only.- Example notebook
deconvolution_wb_backprojector_3d.ipynb(+ paired script + README row).
Validation
- Matched path byte-exact vs pre-change on CPU/GPU; CPU/GPU parity for the WB path ≤ 4e-8.
- On real astrocyte data (A40 GPU): WB@2 reaches matched@~10 quality with sharper absolute FSC resolution.
ruff,ruff format,mypy, fullpytest(397 passed) green.
Full Changelog: v0.8.0a1...v0.8.0a2
v0.8.0a1 — Cellpose v4 DINO model support
Features
- Cellpose v4 DINO models: the GPU-resident segmentation path now supports all four Cellpose v4 models — the SAM backbones (
cpsam,cpsam_v2) and the DINOv3 backbones (cpdino,cpdino-vitb). The canonical entry point is nowsegment_cellpose;segment_cpsamis kept as a deprecated alias (slated for removal after 0.8). - Tunable tile size:
segment_cellposeexposes absizeparameter (defaults to 256 for the SAM backbone, 384 for DINO), centralized in_resolve_bsizewith cellpose's SAM→256 guard and non-positive-bsizevalidation.
Packaging
- Require
cellpose>=4.2.0(the release that first shipscpsam_v2/cpdino). - The DINO backbones additionally require
dinov3, which is published only on GitHub:pip install git+https://github.com/facebookresearch/dinov3.
Notes
- Resident masks match stock
CellposeModel.eval(AP@0.5 ≥ 0.95) for all four models in 2D and 3D.
Full changes in #51.
v0.7.0
First stable release of the 0.7 series, consolidating work from 13 alpha pre-releases (v0.7.0a1–a13). 79 commits since v0.6.0.
Highlights
- Expanded
cubic.metrics— a full device-agnostic metrics suite: resolution (FRC/FSC/DCR, band-limited), image quality (PSNR, SSIM, MS-SSIM, MicroSSIM/MicroMS3IM), and Pearson correlation (PCC). - GPU-resident Cellpose-SAM segmentation with the v4 API.
- Device-agnostic GLCM texture features and
regionpropsextra_propertiesforwarding. - torch interop in the device layer — zero-copy CUDA↔CuPy bridging.
New features
Metrics — resolution
- DCR, histogram FSC, and band-limited metrics (#32)
- Binomial splitting for single-image FRC/FSC (#40)
Metrics — image quality
- MicroSSIM / MicroMS3IM port and a general-purpose
ms_ssim(Wang 2003, torchmetrics-compatible), withcompute_ssim_elements, an RI-factor solver (bracket + bisection on dS/dα), andimage_processinghelpers alpha_min/alpha_maxkwargs for MicroSSIM (#41)pcc(Pearson correlation coefficient)normalize='min_max'forpsnr/nrmse;spatial_dimsfor 5-D SSIM inputsreturn_ioupassthrough inaverage_precision
Feature extraction
- Device-agnostic GLCM texture features
extra_propertiesforwarded throughregionprops/regionprops_table
Segmentation
- GPU-resident Cellpose-SAM eval + v4 API migration (#46)
Device layer & examples
- torch tensors recognized in
asnumpy/get_device; CUDA tensors route to CuPy (zero-copy) - Shared device-routing helpers across the scipy/skimage proxies; proxies now scan all args for device
- Segmentation & feature-extraction example notebooks (#33); Zenodo + pooch auto-download for example data (#48)
Notable fixes
- SSIM mask footprint moved to the mask's device for cuCIM erosion (GPU per-cell SSIM)
- FSC/FRC: mean-center before Hamming window (#42); return NaN when the curve never legitimately crosses threshold (#45); honor
use_max_nyquist - Segmentation: handle empty FOVs, migrate off deprecated skimage
min_size=(#44);np.isinfor numpy ≥2.2 (#43) average_precisioncounts distinct labelscrop_cornercorrectness for single-axis and bottom-right crops- Rank-agnostic deconvolution slicing with a faithful observer path
- GLCM: float64 counts, empty-pair directions on CuPy, distance validation
Install
```bash
pip install cubic==0.7.0
```
Full changelog: v0.6.0...v0.7.0
v0.7.0a13 — fix GPU masked SSIM (cuCIM erosion footprint device)
Fixes
- GPU masked SSIM: move the erosion footprint onto the mask's device before
morphology.erosion.morphology.square/cubereceive only an int, so the device-agnostic proxy returned a host footprint; cuCIM'serosionrejected a NumPy footprint paired with a GPU mask (footprint must be either an ndarray or Sequence). Per-cell SSIM now runs on GPU. (1cd5f08)
Full changelog: v0.7.0a12...v0.7.0a13