From 7ea0d1c093482099b899d77565e30fb4f3823c55 Mon Sep 17 00:00:00 2001 From: Wim Pomp Date: Sat, 23 Aug 2025 23:13:57 +0200 Subject: [PATCH] - add colormap from string to rust code --- Cargo.toml | 4 +++- py/tiffwrite/__init__.py | 46 +++++----------------------------------- pyproject.toml | 2 +- src/lib.rs | 27 ++++++++++++++++++++++- src/py.rs | 12 +++-------- 5 files changed, 38 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c2500d8..90a77b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiffwrite" -version = "2025.8.0" +version = "2025.8.1" edition = "2024" rust-version = "1.85.1" authors = ["Wim Pomp "] @@ -18,6 +18,8 @@ crate-type = ["cdylib", "rlib"] [dependencies] anyhow = "1.0.98" +colorcet = "0.2.1" +colorgrad = "0.7.2" chrono = "0.4.41" css-color = "0.2.8" flate2 = "1.1.1" diff --git a/py/tiffwrite/__init__.py b/py/tiffwrite/__init__.py index 598a884..16ada99 100644 --- a/py/tiffwrite/__init__.py +++ b/py/tiffwrite/__init__.py @@ -6,8 +6,6 @@ from pathlib import Path from typing import Any, Callable, Sequence from warnings import warn -import colorcet -import matplotlib import numpy as np from numpy.typing import ArrayLike, DTypeLike from tqdm.auto import tqdm @@ -77,6 +75,9 @@ class IJTiffFile(rs.IJTiffFile): else: return codecs.get(int(idx), 50000) + if colors is not None and colormap is not None: + warn("Cannot have colors and colormap simultaneously.", TiffWriteWarning, stacklevel=2) + self.path = Path(path) self.dtype = np.dtype(dtype) if compression is not None: @@ -88,7 +89,7 @@ class IJTiffFile(rs.IJTiffFile): if colors is not None: self.colors = [str(color) for color in colors] if colormap is not None: - self.colormap = get_colormap(colormap) + self.colormap = str(colormap) if pxsize is not None: self.px_size = float(pxsize) if deltaz is not None: @@ -96,7 +97,7 @@ class IJTiffFile(rs.IJTiffFile): if timeinterval is not None: self.time_interval = float(timeinterval) if comment is not None: - self.comment = comment + self.comment = str(comment) if extratags is not None: for extra_tag in extratags: self.append_extra_tag(extra_tag, None) @@ -106,8 +107,6 @@ class IJTiffFile(rs.IJTiffFile): TiffWriteWarning, stacklevel=2, ) - if colors is not None and colormap is not None: - warn("Cannot have colors and colormap simultaneously.", TiffWriteWarning, stacklevel=2) def __enter__(self) -> IJTiffFile: return self @@ -146,41 +145,6 @@ class IJTiffFile(rs.IJTiffFile): self.append_extra_tag(extra_tag, (c, z, t)) -def get_colormap(colormap: str) -> np.ndarray: - if hasattr(colorcet, colormap.rstrip("_r")): - cm = np.array( - [[int("".join(i), 16) for i in zip(*[iter(s[1:])] * 2)] for s in getattr(colorcet, colormap.rstrip("_r"))] - ).astype("uint8") - if colormap.endswith("_r"): - cm = cm[::-1] - if colormap.startswith("glasbey") or colormap.endswith("glasbey"): - cm[0] = 255, 255, 255 - cm[-1] = 0, 0, 0 - else: - cmap = matplotlib.colormaps.get_cmap(colormap) - if cmap.N < 256: - cm = ( - 255 - * np.vstack( - ( - (1, 1, 1), - matplotlib.cm.ScalarMappable(matplotlib.colors.Normalize(1, 254), cmap).to_rgba( - np.arange(1, 254) - )[:, :3], - (0, 0, 0), - ) - ) - ).astype("uint8") - else: - cm = ( - 255 - * matplotlib.cm.ScalarMappable(matplotlib.colors.Normalize(0, 255), cmap).to_rgba(np.arange(256))[ - :, :3 - ] - ).astype("uint8") - return cm - - def tiffwrite( file: str | Path, data: np.ndarray, diff --git a/pyproject.toml b/pyproject.toml index 7fc3255..cd7156c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ "Programming Language :: Python :: 3.14", ] -dependencies = ["colorcet", "matplotlib", "numpy", "tqdm"] +dependencies = ["numpy", "tqdm"] [project.optional-dependencies] test = ["pytest", "tifffile", "imagecodecs"] diff --git a/src/lib.rs b/src/lib.rs index a7892c3..dd6a34b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,8 @@ mod py; use anyhow::{Result, anyhow}; use chrono::Utc; +use colorcet::ColorMap; +use colorgrad::{Gradient, LinearGradient}; use css_color::Srgb; use flate2::write::ZlibEncoder; use ndarray::{ArcArray2, AsArray, Ix2, s}; @@ -709,11 +711,12 @@ impl IJTiffFile { } /// set colors from css color names and #C01085 - pub fn set_colors(&mut self, colors: &[String]) -> Result<()> { + pub fn set_colors>(&mut self, colors: &[S]) -> Result<()> { self.colors = Colors::Colors( colors .iter() .map(|c| { + let c = c.as_ref(); let lc = c.to_lowercase(); let c = match lc.as_str() { "r" => "#ff0000", @@ -740,6 +743,28 @@ impl IJTiffFile { Ok(()) } + pub fn set_colormap>(&mut self, name: S) -> Result<()> { + let name = name.as_ref(); + let colormap: LinearGradient = name.parse::()?.try_into()?; + let mut colormap = colormap + .colors(256) + .into_iter() + .map(|c| { + vec![ + (c.r * 255.0).round() as u8, + (c.g * 255.0).round() as u8, + (c.b * 255.0).round() as u8, + ] + }) + .collect::>(); + if name.starts_with("glasbey") || name.ends_with("glasbey") { + colormap[0] = vec![255, 255, 255]; + colormap[255] = vec![0, 0, 0]; + } + self.colors = Colors::Colormap(colormap); + Ok(()) + } + /// to be saved in description tag (270) pub fn description(&self, c_size: usize, z_size: usize, t_size: usize) -> String { let mut desc: String = String::from("ImageJ=1.11a"); diff --git a/src/py.rs b/src/py.rs index 5c75851..42a3226 100644 --- a/src/py.rs +++ b/src/py.rs @@ -1,7 +1,6 @@ use crate::{Colors, Compression, IJTiffFile, Tag}; -use ndarray::s; use num::{Complex, FromPrimitive, Rational32}; -use numpy::{PyArrayMethods, PyReadonlyArray2}; +use numpy::PyReadonlyArray2; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; @@ -221,14 +220,9 @@ impl PyIJTiffFile { } #[setter] - fn set_colormap(&mut self, colormap: PyReadonlyArray2) -> PyResult<()> { + fn set_colormap(&mut self, colormap: &str) -> PyResult<()> { if let Some(ijtifffile) = &mut self.ijtifffile { - let a = colormap.to_owned_array(); - ijtifffile.colors = Colors::Colormap( - (0..a.shape()[0]) - .map(|i| Vec::from(a.slice(s![i, ..]).as_slice().unwrap())) - .collect(), - ); + ijtifffile.set_colormap(colormap)?; } Ok(()) }