Skip to content

Commit

Permalink
feat: better color object (#218)
Browse files Browse the repository at this point in the history
* feat: better color object

* update docs
  • Loading branch information
tlambert03 authored Mar 17, 2024
1 parent d1853b0 commit 53a2e1a
Show file tree
Hide file tree
Showing 8 changed files with 70 additions and 17 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,13 @@ Metadata(
contents=Contents(channelCount=2, frameCount=60),
channels=[
Channel(
channel=ChannelMeta(name='Widefield Green', index=0, colorRGB=65371, emissionLambdaNm=535.0, excitationLambdaNm=None),
channel=ChannelMeta(
name='Widefield Green',
index=0,
color=Color(r=91, g=255, b=0, a=1.0),
emissionLambdaNm=535.0,
excitationLambdaNm=None
),
loops=LoopIndices(NETimeLoop=None, TimeLoop=0, XYPosLoop=1, ZStackLoop=2),
microscope=Microscope(
objectiveMagnification=10.0,
Expand Down Expand Up @@ -204,7 +210,13 @@ Metadata(
)
),
Channel(
channel=ChannelMeta(name='Widefield Red', index=1, colorRGB=22015, emissionLambdaNm=620.0, excitationLambdaNm=None),
channel=ChannelMeta(
name='Widefield Red',
index=1,
color=Color(r=255, g=85, b=0, a=1.0),
emissionLambdaNm=620.0,
excitationLambdaNm=None
),
loops=LoopIndices(NETimeLoop=None, TimeLoop=0, XYPosLoop=1, ZStackLoop=2),
microscope=Microscope(
objectiveMagnification=10.0,
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ filterwarnings = [
"ignore:The distutils package is deprecated::",
"ignore:The distutils.sysconfig module is deprecated::",
"ignore:distutils Version classes are deprecated:",
"ignore:::xarray",
]

# https://mypy.readthedocs.io/en/stable/config_file.html
Expand Down
5 changes: 5 additions & 0 deletions scripts/nd2_describe.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from pathlib import Path

import nd2
from nd2 import structures
from nd2._parse._chunk_decode import iter_chunks


Expand All @@ -34,6 +35,10 @@ def get_nd2_stats(path: Path) -> "tuple[str, dict]":
with nd2.ND2File(path) as nd:
meta = nd.metadata if isinstance(nd.metadata, dict) else asdict(nd.metadata)
for channel in meta.get("channels", []):
# we changed colorRGB to color inb v0.10.0
if color := channel["channel"].pop("color", None):
if isinstance(color, structures.Color):
channel["channel"]["colorRGB"] = color.as_abgr_u4()
# Remove custom loops if null... they're super rare, and
# readlimfile.json doesn't include them
if channel.get("loops") and not channel["loops"].get("CustomLoop"):
Expand Down
2 changes: 1 addition & 1 deletion src/nd2/_ome.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ def nd2_ome_metadata(
if not f.is_rgb:
# if you include any of this for RGB images, the Bioformats OMETiffReader
# will show all three RGB channels with the same color
channel.color = m.Color(ch.channel.rgba_tuple())
channel.color = m.Color(ch.channel.color)
channel.emission_wavelength = ch.channel.emissionLambdaNm
channel.emission_wavelength_unit = UnitsLength.NANOMETER
channel.excitation_wavelength = ch.channel.excitationLambdaNm
Expand Down
2 changes: 1 addition & 1 deletion src/nd2/_parse/_parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,7 @@ def load_metadata(raw_meta: RawMetaDict, global_meta: GlobalMetadata) -> strct.M
channel_meta = strct.ChannelMeta(
index=k,
name=plane.get("sDescription", ""),
colorRGB=plane.get("uiColor", 0),
color=strct.Color.from_abgr_u4(plane.get("uiColor", 0)),
emissionLambdaNm=em or None,
excitationLambdaNm=ex or None,
)
Expand Down
8 changes: 4 additions & 4 deletions src/nd2/nd2file.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ def metadata(self) -> Metadata:
channel=ChannelMeta(
name="Widefield Green",
index=0,
colorRGB=65371,
color=Color(r=91, g=255, b=0, a=1.0),
emissionLambdaNm=535.0,
excitationLambdaNm=None,
),
Expand Down Expand Up @@ -483,7 +483,7 @@ def metadata(self) -> Metadata:
channel=ChannelMeta(
name="Widefield Red",
index=1,
colorRGB=22015,
color=Color(r=255, g=85, b=0, a=1.0),
emissionLambdaNm=620.0,
excitationLambdaNm=None,
),
Expand Down Expand Up @@ -549,7 +549,7 @@ def frame_metadata(self, seq_index: int | tuple) -> FrameMetadata | dict:
channel=ChannelMeta(
name="Widefield Green",
index=0,
colorRGB=65371,
color=Color(r=91, g=255, b=0, a=1.0),
emissionLambdaNm=535.0,
excitationLambdaNm=None,
),
Expand Down Expand Up @@ -601,7 +601,7 @@ def frame_metadata(self, seq_index: int | tuple) -> FrameMetadata | dict:
channel=ChannelMeta(
name="Widefield Red",
index=1,
colorRGB=22015,
color=Color(r=255, g=85, b=0, a=1.0),
emissionLambdaNm=620.0,
excitationLambdaNm=None,
),
Expand Down
2 changes: 1 addition & 1 deletion src/nd2/readers/_legacy/legacy_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ def _load_metadata(
channel_meta = strct.ChannelMeta(
name=plane["OpticalConfigName"],
index=int(idx),
colorRGB=plane["Color"],
color=strct.Color.from_abgr_u4(plane["Color"]),
emissionLambdaNm=None,
excitationLambdaNm=None,
)
Expand Down
51 changes: 43 additions & 8 deletions src/nd2/structures.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import builtins
import warnings
from dataclasses import dataclass, field
from enum import IntEnum
from typing import TYPE_CHECKING, Literal, NamedTuple, TypedDict, Union
Expand Down Expand Up @@ -267,22 +268,56 @@ def __post_init__(self) -> None:
self.loops = LoopIndices(**self.loops)


class Color(NamedTuple):
r: int
g: int
b: int
a: float = 1.0

def as_hex(self) -> str: # pragma: no cover
"""Return color as a hex string."""
return f"#{self.r:02x}{self.g:02x}{self.b:02x}"

@classmethod
def from_abgr_u4(cls, val: int) -> Color:
"""Create a color from an unsigned 4-byte (32-bit) integer in ABGR format."""
return cls(
r=val & 255,
g=val >> 8 & 255,
b=val >> 16 & 255,
# it's not clear if the alpha channel is used in NIS Elements
# so we default to 1.0 if it comes in as 0
a=((val >> 24 & 255) / 255) or 1.0,
)

def as_abgr_u4(self) -> int:
"""Return color as an unsigned 4-byte (32-bit) integer in ABGR format.
This is the native format of NIS Elements.
"""
# for the sake of round-tripping, we'll assume that 1.0 alpha is 0
alpha = 0 if self.a == 1.0 else int(self.a * 255)
return (alpha << 24) + (self.b << 16) + (self.g << 8) + self.r


@dataclass
class ChannelMeta:
name: str
index: int
colorRGB: int # probably 0xAABBGGRR
color: Color
emissionLambdaNm: float | None = None
excitationLambdaNm: float | None = None

def rgba_tuple(self) -> tuple[int, int, int, int]:
"""Return the color as a tuple of (R, G, B, A)."""
return (
self.colorRGB & 255,
(self.colorRGB >> 8) & 255,
(self.colorRGB >> 16) & 255,
(self.colorRGB >> 24) & 255,
@property
def colorRGBA(self) -> int:
"""Return color as unsigned 4-byte (32-bit) integer in ABGR format."""
warnings.warn(
"`.colorRGBA` is deprecated, use `.color..as_abgr_u4()` instead "
"if you want the color in the original 32-bit ABGR format.",
DeprecationWarning,
stacklevel=2,
)
return self.color.as_abgr_u4()


@dataclass
Expand Down

0 comments on commit 53a2e1a

Please sign in to comment.