Source code for wsic.enums

"""Enumerated types used by wsic."""
from enum import Enum
from numbers import Number
from typing import Any, Dict, Optional, Tuple


[docs]class Codec(str, Enum): """Compression codecs / algorithms / formats.""" AVIF = "AVIF" # AV1 Image Format (HEIF) BLOSC = "Blosc" # Block based meta-compression algorithm BLOSC2 = "Blosc2" # Block based meta-compression version 2 BROTLI = "Brotli" # Google Brotli BZ2 = "BZ2" # Bzip2 DEFLATE = "DEFLATE" DELTA = "Delta" # Delta coding GIF = "GIF" # Graphics Interchange Format GZIP = "GZIP" # Gzip J2K = "J2K" # JPEG 2000 raw codestream JPEG = "JPEG" # Original JPEG # noqa: PIE796 JPEG2000 = "JPEG 2000" JPEGLS = "JPEG-LS" # Lossless (or near) JPEG JPEGXL = "JPEG XL" JPEGXR = "JPEG XR" LERC = "LERC" # Limited Error Raster Compression LJPEG = "LJPEG" # Lossy JPEG (old version) LZ4 = "LZ4" LZ4F = "LZ4F" LZ77 = "LZ77" LZMA = "LZMA" # Lempel-Ziv-Welch chain algorithm LZW = "LZW" # Lempel-Ziv-Welch NONE = "None" # No compression PACKBITS = "PackBits" PNG = "PNG" # Portable Network Graphics QOI = "QOI" # Quite OK Image SNAPPY = "Snappy" # Google Snappy WEBP = "WebP" ZFP = "ZFP" # Floating point compression (up to 4 dimensions) ZLIB = "Zlib" ZLIBNG = "ZlibNG" # ZlibNG, zlib replacement for "next generation" systems ZOPFLI = "Zopfli" ZSTD = "Zstd" # Zstandard
[docs] def condensed(self) -> str: """Convert to a string without spaces or dashes.""" return self.value.replace(" ", "").replace("-", "")
[docs] def to_numcodecs_config( self, level: Optional[Number] = None, dtype: Optional[str] = None ) -> Dict[str, Any]: """Convert to numcodecs Codec ID string.""" configs = { Codec.AVIF: {"id": "imagecodecs_avif"}, Codec.BLOSC: {"id": "blosc"}, Codec.BLOSC2: {"id": "imagecodecs_blosc2"}, Codec.BROTLI: {"id": "imagecodecs_brotli"}, Codec.DEFLATE: {"id": "imagecodecs_deflate"}, Codec.BZ2: {"id": "imagecodecs_bz2"}, Codec.DELTA: {"id": "delta", "dtype": "uint8"}, Codec.GIF: {"id": "imagecodecs_gif"}, Codec.GZIP: {"id": "gzip"}, Codec.J2K: {"id": "imagecodecs_jpeg2k", "codecformat": "j2k"}, Codec.JPEG: {"id": "imagecodecs_jpeg"}, Codec.JPEG2000: {"id": "imagecodecs_jpeg2k", "codecformat": "jp2"}, Codec.JPEGLS: {"id": "imagecodecs_jpegls"}, Codec.JPEGXL: {"id": "imagecodecs_jpegxl"}, Codec.JPEGXR: {"id": "imagecodecs_jpegxr"}, Codec.LERC: {"id": "imagecodecs_lerc"}, Codec.LJPEG: {"id": "imagecodecs_ljpeg"}, Codec.LZ4: {"id": "lz4"}, Codec.LZ4F: {"id": "imagecodecs_lz4f"}, Codec.LZMA: {"id": "lzma"}, Codec.LZW: {"id": "imagecodecs_lzw"}, Codec.PACKBITS: {"id": "imagecodecs_packbits"}, Codec.PNG: {"id": "imagecodecs_png"}, Codec.QOI: {"id": "imagecodecs_qoi"}, Codec.SNAPPY: {"id": "imagecodecs_snappy"}, Codec.WEBP: {"id": "imagecodecs_webp"}, Codec.ZFP: {"id": "imagecodecs_zfp"}, Codec.ZLIB: {"id": "zlib"}, Codec.ZLIBNG: {"id": "imagecodecs_zlibng"}, Codec.ZOPFLI: {"id": "imagecodecs_zopfli"}, Codec.ZSTD: {"id": "zstd"}, Codec.NONE: {}, } try: config = configs[self] except KeyError as e: raise ValueError(f"{self} is not a supported codec") from e if level is not None and self not in FIXED_LEVEL_CODECS: level_key = "clevel" if self in NUMCODECS_CODECS else "level" config[level_key] = level if dtype: config["dtype"] = dtype return config
[docs] def to_dicom_transfer_syntax(self, lossless: bool = True) -> str: """Convert to a DICOM UID (str) for transfer syntax.""" if self == Codec.JPEG: return "1.2.840.10008.1.2.4.50" if self == Codec.JPEGLS: return "1.2.840.10008.1.2.4.80" if lossless else "1.2.840.10008.1.2.4.81" if self == Codec.JPEG2000: return "1.2.840.10008.1.2.4.90" if lossless else "1.2.840.10008.1.2.4.91" raise ValueError(f"{self} is not a supported DICOM transfer syntax")
[docs] @classmethod def from_string(cls, string: str) -> "Codec": """Convert string to Compression enum.""" condensed_upper = string.replace(" ", "").replace("-", "").upper() iso_names_map = { "ISO_10918_1": "JPEG", "ISO_14495_1": "JPEGLS", "ISO_15444_1": "JPEG2000", } condensed_upper = iso_names_map.get(condensed_upper, condensed_upper) try: return getattr(cls, condensed_upper) except AttributeError as e: raise ValueError(f"Unknown codec: {string}") from e
[docs] @classmethod def from_dicom_transfer_syntax(cls, uid: str) -> "Codec": """Convert DICOM UID to Compression enum.""" dicom_uid_map = { "1.2.840.10008.1.2.4.50": cls.JPEG, "1.2.840.10008.1.2.4.80": cls.JPEGLS, "1.2.840.10008.1.2.4.81": cls.JPEGLS, "1.2.840.10008.1.2.4.90": cls.JPEG2000, "1.2.840.10008.1.2.4.91": cls.JPEG2000, } return dicom_uid_map[uid]
[docs] @classmethod def from_tiff(cls, compression: int) -> "Codec": """Convert TIFF compression value to Compression enum. Args: compression: TIFF compression value. """ compression_codec_mapping = { 1: cls.NONE, 5: cls.LZW, 7: cls.JPEG, 34712: cls.JPEG2000, 33003: cls.JPEG2000, # Leica Aperio YCBC 33005: cls.JPEG2000, # Leica Aperio RGB 34933: cls.PNG, 34934: cls.JPEGXR, 22610: cls.JPEGXR, # NDPI JPEG XR 32946: cls.DEFLATE, 34927: cls.WEBP, # Deprecated 50001: cls.WEBP, 34926: cls.ZSTD, # Deprecated 50000: cls.ZSTD, 50002: cls.JPEGXL, } if compression in compression_codec_mapping: return compression_codec_mapping[compression] raise ValueError(f"Unknown TIFF compression: {compression}")
NUMCODECS_CODECS = ( Codec.BLOSC, Codec.DELTA, Codec.GZIP, Codec.LZ4, Codec.LZMA, Codec.PACKBITS, Codec.ZLIB, Codec.ZSTD, ) IMAGECODECS_CODECS = ( Codec.AVIF, Codec.BLOSC2, Codec.BROTLI, Codec.DEFLATE, Codec.DELTA, Codec.GIF, Codec.J2K, Codec.JPEG, Codec.JPEG2000, Codec.JPEGLS, Codec.JPEGXL, Codec.JPEGXR, Codec.LERC, Codec.LJPEG, Codec.LZ4, Codec.LZ4F, Codec.LZMA, Codec.PNG, Codec.SNAPPY, Codec.WEBP, Codec.ZFP, Codec.ZLIB, Codec.ZLIBNG, Codec.ZOPFLI, Codec.LZW, ) FIXED_LEVEL_CODECS = (Codec.LZ4, Codec.LZW, Codec.ZSTD)
[docs]class ColorSpace(str, Enum): """Color spaces.""" RGB = "RGB" # Standard Red Green Blue, assumed to be sRGB SRGB = "RGB" # noqa: PIE796 RGBA = "RGBA" # LINEAR = "Linear" # Generic linear color space GREY = "Grey" # Generic greyscale GRAY = "Grey" # noqa: PIE796 GREYSCALE = "Grey" # noqa: PIE796 GRAYSCALE = "Grey" # noqa: PIE796 MINISBLACK = "Grey" # noqa: PIE796 MINISWHITE = "Min is White" CMYK = "CMYK" # Cyan Magenta Yellow Black CMYKA = "CMYKA" # Cyan Magenta Yellow Black Alpha YCBCR = "YCbCr" # Assumed to be BT.601 / Rec. 601 YCRCB = "YCrCb" BT601 = "YCbCr" # noqa: PIE796 REC601 = "YCbCr" # noqa: PIE796 YCOCG = "YCoCg" # Reversible modified YUV used by JPEG 2000, defined in ITU-T H.273 YCC = "YCoCg" # noqa: PIE796 YUV = "YUV" # BT.1700 BT1700 = "YUV" # noqa: PIE796 BT.1700 CIELAB = "CIE L*a*b*" # CIE L*a*b* CIELUV = "CIE L*u*v*" # CIE L*u*v* HSV = "HSV" # Hue Saturation Value HSL = "HSL" # Hue Saturation Lightness LMS = "LMS" # Cone-cone LMS XYB = "XYB" # Used by JPEG XL, derived from LMS RLAB = "RLAB" # https://en.wikipedia.org/wiki/Color_appearance_model#RLAB LLAB = "LLAB" # https://en.wikipedia.org/wiki/Color_appearance_model#LLAB OKLAB = "OKLab" # https://en.wikipedia.org/wiki/Color_appearance_model#OKLab PALETTE = "Palette" # Used by PNG, JPEG, and TIFF
[docs] def condensed(self) -> str: """Convert to a string without spaces, dashes, and asterisks.""" return self.value.replace(" ", "").replace("-", "").replace("*", "")
[docs] @classmethod def from_string(cls, string: str) -> "Codec": """Convert string to ColorSpace enum.""" condensed_upper = ( string.replace(" ", "").replace("-", "").replace("*", "").upper() ) try: return getattr(cls, condensed_upper) except AttributeError as e: raise ValueError(f"Unknown color space: {string}") from e
[docs] def to_tiff(self) -> "ColorSpace": """Convert to tifffile compatible color space.""" # tifffile doesn't recognise YCrCb, so use YCbCr as this works return ColorSpace.YCBCR if self == ColorSpace.YCRCB else self
[docs] @classmethod def from_tiff( cls, photometric: int, compression: Optional[int] = None ) -> "ColorSpace": """Convert TIFF value to ColorSpace enum. Args: photometric: TIFF photometric value. compression: TIFF compression value. """ compression_colorspace_mapping = { 33003: cls.YCBCR, 33005: cls.RGB, } photometric_color_space_mapping = { 1: cls.GRAY, 2: cls.RGB, 3: cls.PALETTE, 5: cls.CMYK, 6: cls.YCBCR, 8: cls.CIELAB, 34892: cls.LINEAR, } if compression in compression_colorspace_mapping: return compression_colorspace_mapping[compression] if photometric in photometric_color_space_mapping: return photometric_color_space_mapping[photometric] raise ValueError(f"Unsupported TIFF photometric interpretation: {photometric}")
[docs] @classmethod def from_dicom(cls, photometric: str) -> "ColorSpace": """Convert DICOM value to ColorSpace enum. Args: photometric: DICOM photometric value. """ photometric_color_space_mapping = { "MONOCHROME1": cls.MINISBLACK, "MONOCHROME2": cls.MINISWHITE, "RGB": cls.RGB, "PALETTE_COLOR": cls.PALETTE, "YBR_FULL": cls.YCRCB, "YBR_FULL_422": cls.YCRCB, "YBR_PARTIAL_420": cls.YCRCB, } if photometric in photometric_color_space_mapping: return photometric_color_space_mapping[photometric] raise ValueError(f"Unsupported DICOM photometric interpretation: {photometric}")
[docs] def to_jp2(self) -> "ColorSpace": """Convert to JPEG2000 color space.""" if self == ColorSpace.RGB: return 16 if self == ColorSpace.CMYK: return 12 if self == ColorSpace.GRAYSCALE: return 17 if self == ColorSpace.YCBCR: return 18 raise ValueError(f"Color space `{self}` has no known JP2 equivalent.")
[docs] def to_dicom_photometric_interpretation( self, subsampling: Tuple[int, int] = (4, 2, 2) ) -> str: """Convert to DICOM photometric interpretation.""" if self == ColorSpace.RGB: return "RGB" if self == ColorSpace.YCBCR: if subsampling in (None, (4, 4, 4)): return "YBR_FULL" if subsampling == (4, 2, 2): return "YBR_FULL_422" raise ValueError( f"Color space `{self}` has no known DICOM photometric interpretation." )