cft-zarr 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.3
2
+ Name: cft-zarr
3
+ Version: 0.0.2
4
+ Summary: CFT Zarr codecs for 12-bit fluorescence and RGB compression
5
+ Author: Eli White
6
+ Author-email: eliwhite@gmail.com
7
+ Requires-Python: >=3.11,<=3.14
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Requires-Dist: imagecodecs (>=2024.1.1)
13
+ Requires-Dist: numpy (>=1.26.4,<2.0.0)
14
+ Requires-Dist: zarr (>=3.0.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # CFT Zarr Custom Codecs
18
+
19
+ Custom codecs for Zarr v3 optimized for CFT (Cryo-Fluorescence Tomography) data storage.
20
+
21
+ ## Codecs
22
+
23
+ ### Chunked JPEG Codec (`cft_zarr.chunked_jpeg`)
24
+
25
+ Efficient lossy compression for RGB images using chunked JPEG encoding.
26
+
27
+ - **Purpose**: Compress RGB images in chunks of N slices (default: 4) with JPEG compression
28
+ - **Chunk Shape**: Configurable, default (4, 512, 512, 3) for RGB
29
+ - **Quality**: Configurable JPEG quality (0-100, default: 85)
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ from cft_zarr.chunked_jpeg import ChunkedJPEGCodec
35
+ import zarr
36
+
37
+ # Create codec
38
+ codec = ChunkedJPEGCodec(quality=85, chunk_shape=(4, 512, 512, 3))
39
+
40
+ # Use with Zarr array
41
+ arr = zarr.open_array(
42
+ 'rgb.zarr',
43
+ mode='w',
44
+ shape=(100, 512, 512, 3),
45
+ chunks=(4, 512, 512, 3),
46
+ dtype='uint8',
47
+ codec=codec
48
+ )
49
+ ```
50
+
51
+ ## Installation
52
+
53
+ ```bash
54
+ cd src/python/public/cft_zarr
55
+ poetry install
56
+ ```
57
+
58
+
@@ -0,0 +1,41 @@
1
+ # CFT Zarr Custom Codecs
2
+
3
+ Custom codecs for Zarr v3 optimized for CFT (Cryo-Fluorescence Tomography) data storage.
4
+
5
+ ## Codecs
6
+
7
+ ### Chunked JPEG Codec (`cft_zarr.chunked_jpeg`)
8
+
9
+ Efficient lossy compression for RGB images using chunked JPEG encoding.
10
+
11
+ - **Purpose**: Compress RGB images in chunks of N slices (default: 4) with JPEG compression
12
+ - **Chunk Shape**: Configurable, default (4, 512, 512, 3) for RGB
13
+ - **Quality**: Configurable JPEG quality (0-100, default: 85)
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from cft_zarr.chunked_jpeg import ChunkedJPEGCodec
19
+ import zarr
20
+
21
+ # Create codec
22
+ codec = ChunkedJPEGCodec(quality=85, chunk_shape=(4, 512, 512, 3))
23
+
24
+ # Use with Zarr array
25
+ arr = zarr.open_array(
26
+ 'rgb.zarr',
27
+ mode='w',
28
+ shape=(100, 512, 512, 3),
29
+ chunks=(4, 512, 512, 3),
30
+ dtype='uint8',
31
+ codec=codec
32
+ )
33
+ ```
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ cd src/python/public/cft_zarr
39
+ poetry install
40
+ ```
41
+
@@ -0,0 +1,159 @@
1
+ """cft_zarr package - runtime registration of custom Zarr v3 codecs.
2
+
3
+ This module registers available codecs so Zarr can resolve them by name:
4
+ - cft_zarr.shift12jls (ArrayBytesCodec serializer - no incremental updates)
5
+ - cft_zarr.jpeg (ArrayBytesCodec serializer - no incremental updates)
6
+ - cft_zarr.shift12jls_compressor (BytesBytesCodec compressor - supports incremental updates)
7
+ - cft_zarr.jpeg_compressor (BytesBytesCodec compressor - supports incremental updates)
8
+ - cft_zarr.jpegxl
9
+
10
+ Registration is attempted against the Zarr v3 registry API. If unavailable,
11
+ we try a legacy-style register function as a best effort.
12
+ """
13
+
14
+ from typing import Any
15
+
16
+ __all__ = [
17
+ "Shift12JLSCodec",
18
+ "JPEGCodec",
19
+ "Shift12JLSCompressor",
20
+ "JPEGCompressor",
21
+ ]
22
+
23
+ # Import codecs if available
24
+ Shift12JLSCodec: Any = None
25
+ JPEGCodec: Any = None
26
+ Shift12JLSCompressor: Any = None
27
+ JPEGCompressor: Any = None
28
+ JPEGXLCodec: Any = None
29
+
30
+ try:
31
+ from .shift12jls import Shift12JLSCodec as _Shift12JLSCodec
32
+ Shift12JLSCodec = _Shift12JLSCodec
33
+ except Exception:
34
+ Shift12JLSCodec = None
35
+
36
+ try:
37
+ from .jpeg import JPEGCodec as _JPEGCodec
38
+ JPEGCodec = _JPEGCodec
39
+ except Exception:
40
+ JPEGCodec = None
41
+
42
+ try:
43
+ from .shift12jls_compressor import Shift12JLSCompressor as _Shift12JLSCompressor
44
+ Shift12JLSCompressor = _Shift12JLSCompressor
45
+ except Exception:
46
+ Shift12JLSCompressor = None
47
+
48
+ try:
49
+ from .jpeg_compressor import JPEGCompressor as _JPEGCompressor
50
+ JPEGCompressor = _JPEGCompressor
51
+ except Exception:
52
+ JPEGCompressor = None
53
+
54
+ try:
55
+ from .jpegxl import JPEGXLCodec as _JPEGXLCodec
56
+ JPEGXLCodec = _JPEGXLCodec
57
+ except Exception:
58
+ JPEGXLCodec = None
59
+
60
+
61
+ def _register_if_possible(cls: Any) -> None:
62
+ if cls is None:
63
+ return
64
+ # Use Zarr v3 registry API (zarr.registry.register_codec)
65
+ # Signature: register_codec(key: str, codec_cls: type[Codec])
66
+ try:
67
+ from zarr import registry as zregistry # type: ignore
68
+ codec_id = getattr(cls, 'codec_id', None)
69
+ if codec_id:
70
+ zregistry.register_codec(codec_id, cls) # type: ignore[attr-defined]
71
+ except Exception as e:
72
+ # Log error for debugging but don't fail
73
+ import warnings
74
+ warnings.warn(f"Failed to register codec {cls}: {e}", UserWarning)
75
+
76
+
77
+ def _force_map(cls: Any) -> None:
78
+ """Best-effort: insert codec class into known registry maps for immediate availability."""
79
+ if cls is None:
80
+ return
81
+ cid = getattr(cls, 'codec_id', None)
82
+ cname = getattr(cls, '__name__', None)
83
+ if not cid and not cname:
84
+ return
85
+ # zarr.registry private maps - try all possible registry attribute names
86
+ try:
87
+ from zarr import registry as zregistry # type: ignore
88
+ for attr_name in ('_CODECS', 'CODECS', '_codecs', 'codecs', '_registry', 'registry'):
89
+ reg = getattr(zregistry, attr_name, None)
90
+ if isinstance(reg, dict):
91
+ if cid:
92
+ reg[cid] = cls
93
+ if cname:
94
+ reg[cname] = cls
95
+ # Also try name lookup (Zarr might look up by class name)
96
+ if cname and cname != cid:
97
+ reg[cname] = cls
98
+ except Exception:
99
+ pass
100
+ # zarr.codecs.registry private maps
101
+ try:
102
+ from zarr.codecs import registry as cregistry # type: ignore
103
+ for attr_name in ('_CODECS', 'CODECS', '_codecs', 'codecs', '_registry', 'registry'):
104
+ reg = getattr(cregistry, attr_name, None)
105
+ if isinstance(reg, dict):
106
+ if cid:
107
+ reg[cid] = cls
108
+ if cname:
109
+ reg[cname] = cls
110
+ # Also try name lookup
111
+ if cname and cname != cid:
112
+ reg[cname] = cls
113
+ except Exception:
114
+ pass
115
+ # Try registering via get_codec if it exists (Zarr v3 resolver)
116
+ try:
117
+ from zarr.codecs import get_codec # type: ignore
118
+ # This might register it
119
+ except Exception:
120
+ pass
121
+ # Force register with any resolver functions we can find
122
+ try:
123
+ from zarr import registry as zregistry # type: ignore
124
+ # Try to find and call a resolver or register function
125
+ for attr_name in ('resolve_codec', 'get_codec', 'register', 'add'):
126
+ resolver = getattr(zregistry, attr_name, None)
127
+ if callable(resolver):
128
+ try:
129
+ if cid:
130
+ resolver(cid, cls) # type: ignore
131
+ except Exception:
132
+ try:
133
+ resolver(cls) # type: ignore
134
+ except Exception:
135
+ pass
136
+ except Exception:
137
+ pass
138
+
139
+
140
+ # Runtime registration
141
+ _register_if_possible(Shift12JLSCodec)
142
+ _register_if_possible(JPEGCodec)
143
+ _register_if_possible(Shift12JLSCompressor)
144
+ _register_if_possible(JPEGCompressor)
145
+ _register_if_possible(JPEGXLCodec)
146
+
147
+ # Force-map as a last resort so reopened arrays can resolve codecs immediately
148
+ _force_map(Shift12JLSCodec)
149
+ _force_map(JPEGCodec)
150
+ _force_map(Shift12JLSCompressor)
151
+ _force_map(JPEGCompressor)
152
+ _force_map(JPEGXLCodec)
153
+
154
+ from .shift12jls import Shift12JLSCodec
155
+ from .jpeg import JPEGCodec
156
+ from .shift12jls_compressor import Shift12JLSCompressor
157
+ from .jpeg_compressor import JPEGCompressor
158
+
159
+ __all__ = ["Shift12JLSCodec", "JPEGCodec", "Shift12JLSCompressor", "JPEGCompressor"]
@@ -0,0 +1,228 @@
1
+ """Chunked JPEG codec for RGB images in Zarr v3.
2
+
3
+ This codec handles RGB images in chunks of N slices (default: 4) with JPEG compression,
4
+ optimizing for spatial locality and efficient storage of photographic images.
5
+ """
6
+
7
+ import io
8
+ import numpy as np
9
+ from typing import Any, Dict, Optional
10
+
11
+ try:
12
+ import cv2
13
+ CV2_AVAILABLE = True
14
+ except ImportError:
15
+ CV2_AVAILABLE = False
16
+
17
+ try:
18
+ from numcodecs import Codec
19
+ from numcodecs.registry import register_codec
20
+ except ImportError:
21
+ # Fallback for Zarr v3 direct codec interface
22
+ Codec = object
23
+ register_codec = lambda *args, **kwargs: None
24
+
25
+
26
+ class ChunkedJPEGCodec(Codec):
27
+ """Codec for RGB images using chunked JPEG compression.
28
+
29
+ Encodes N×512×512×3 numpy arrays (RGB images) as JPEG-compressed bytes.
30
+ Default chunk shape is (4, 512, 512, 3) but is configurable.
31
+
32
+ Configuration:
33
+ quality: JPEG quality (0-100, default: 85)
34
+ chunk_shape: Tuple of (N, height, width, channels) or (N, height, width) for grayscale
35
+ Default: (4, 512, 512, 3) for RGB, (4, 512, 512) for grayscale
36
+ """
37
+
38
+ codec_id = "cft_zarr.chunked_jpeg"
39
+
40
+ def __init__(
41
+ self,
42
+ quality: int = 85,
43
+ chunk_shape: Optional[tuple] = None,
44
+ ):
45
+ """Initialize chunked JPEG codec.
46
+
47
+ Args:
48
+ quality: JPEG quality (0-100), default 85
49
+ chunk_shape: Chunk shape tuple, default (4, 512, 512, 3) for RGB
50
+ """
51
+ self.quality = max(0, min(100, quality))
52
+ self.chunk_shape = chunk_shape or (4, 512, 512, 3)
53
+
54
+ # Validate chunk shape
55
+ if len(self.chunk_shape) not in (3, 4):
56
+ raise ValueError(f"chunk_shape must be 3D (grayscale) or 4D (RGB), got {len(self.chunk_shape)}D")
57
+
58
+ self.is_rgb = len(self.chunk_shape) == 4
59
+ if self.is_rgb:
60
+ self.n_slices, self.height, self.width, self.channels = self.chunk_shape
61
+ else:
62
+ self.n_slices, self.height, self.width = self.chunk_shape
63
+ self.channels = 1
64
+
65
+ def encode(self, buf: np.ndarray) -> bytes:
66
+ """Encode numpy array to JPEG-compressed bytes.
67
+
68
+ Args:
69
+ buf: Numpy array of shape (N, height, width, 3) or (N, height, width)
70
+
71
+ Returns:
72
+ Compressed bytes containing JPEG-encoded slices
73
+ """
74
+ if not CV2_AVAILABLE:
75
+ raise ImportError("cv2 is required for chunked JPEG codec")
76
+
77
+ if not isinstance(buf, np.ndarray):
78
+ buf = np.asarray(buf)
79
+
80
+ # Validate shape
81
+ expected_shape = self.chunk_shape
82
+ if buf.shape != expected_shape:
83
+ # Handle partial chunks (last chunk may have fewer slices)
84
+ if buf.shape[1:] == expected_shape[1:]:
85
+ # Same spatial dimensions, just fewer slices
86
+ pass
87
+ else:
88
+ raise ValueError(
89
+ f"Expected shape {expected_shape}, got {buf.shape}. "
90
+ "Partial chunks (fewer slices) are allowed, but spatial dimensions must match."
91
+ )
92
+
93
+ # Convert to uint8 if needed
94
+ if buf.dtype != np.uint8:
95
+ buf = buf.astype(np.uint8)
96
+
97
+ # Encode each slice as JPEG using cv2
98
+ jpeg_bytes_list = []
99
+ n_slices = buf.shape[0]
100
+
101
+ # JPEG encoding parameters for cv2
102
+ encode_params = [cv2.IMWRITE_JPEG_QUALITY, self.quality, cv2.IMWRITE_JPEG_OPTIMIZE, 1]
103
+
104
+ for i in range(n_slices):
105
+ slice_data = buf[i]
106
+
107
+ # Handle RGB vs grayscale
108
+ if self.is_rgb:
109
+ # RGB: shape is (height, width, 3)
110
+ if slice_data.shape != (self.height, self.width, 3):
111
+ # Resize if needed
112
+ slice_data = cv2.resize(slice_data, (self.width, self.height), interpolation=cv2.INTER_LANCZOS4)
113
+ # cv2.imencode expects BGR for color images
114
+ slice_data_bgr = cv2.cvtColor(slice_data, cv2.COLOR_RGB2BGR)
115
+ else:
116
+ # Grayscale: shape is (height, width)
117
+ if slice_data.shape != (self.height, self.width):
118
+ slice_data = cv2.resize(slice_data, (self.width, self.height), interpolation=cv2.INTER_LANCZOS4)
119
+ slice_data_bgr = slice_data
120
+
121
+ # Encode as JPEG using cv2
122
+ success, jpeg_bytes = cv2.imencode('.jpg', slice_data_bgr, encode_params)
123
+ if not success:
124
+ raise RuntimeError(f"Failed to encode slice {i} as JPEG")
125
+
126
+ jpeg_bytes_list.append(jpeg_bytes.tobytes())
127
+
128
+ # Combine all JPEG bytes with a simple format:
129
+ # [n_slices (4 bytes)][slice_0_len (4 bytes)][slice_0_data][slice_1_len (4 bytes)][slice_1_data]...
130
+ result = io.BytesIO()
131
+ result.write(n_slices.to_bytes(4, byteorder='big'))
132
+ for jpeg_bytes in jpeg_bytes_list:
133
+ result.write(len(jpeg_bytes).to_bytes(4, byteorder='big'))
134
+ result.write(jpeg_bytes)
135
+
136
+ return result.getvalue()
137
+
138
+ def decode(self, buf: bytes, out: Optional[np.ndarray] = None) -> np.ndarray:
139
+ """Decode JPEG-compressed bytes to numpy array.
140
+
141
+ Args:
142
+ buf: Compressed bytes containing JPEG-encoded slices
143
+ out: Optional output array (not used, but required by Codec interface)
144
+
145
+ Returns:
146
+ Numpy array of shape (N, height, width, 3) or (N, height, width)
147
+ """
148
+ if not CV2_AVAILABLE:
149
+ raise ImportError("cv2 is required for chunked JPEG codec")
150
+
151
+ buffer = io.BytesIO(buf)
152
+
153
+ # Read number of slices
154
+ n_slices_bytes = buffer.read(4)
155
+ if len(n_slices_bytes) < 4:
156
+ raise ValueError("Invalid chunked JPEG format: missing slice count")
157
+ n_slices = int.from_bytes(n_slices_bytes, byteorder='big')
158
+
159
+ # Decode each slice
160
+ slices = []
161
+ for i in range(n_slices):
162
+ # Read slice length
163
+ len_bytes = buffer.read(4)
164
+ if len(len_bytes) < 4:
165
+ raise ValueError(f"Invalid chunked JPEG format: missing length for slice {i}")
166
+ slice_len = int.from_bytes(len_bytes, byteorder='big')
167
+
168
+ # Read slice data
169
+ slice_bytes = buffer.read(slice_len)
170
+ if len(slice_bytes) < slice_len:
171
+ raise ValueError(f"Invalid chunked JPEG format: incomplete data for slice {i}")
172
+
173
+ # Decode JPEG using cv2
174
+ slice_data = cv2.imdecode(np.frombuffer(slice_bytes, dtype=np.uint8), cv2.IMREAD_UNCHANGED)
175
+ if slice_data is None:
176
+ raise RuntimeError(f"Failed to decode JPEG slice {i}")
177
+
178
+ # Convert BGR to RGB if needed
179
+ if self.is_rgb and slice_data.ndim == 3:
180
+ slice_data = cv2.cvtColor(slice_data, cv2.COLOR_BGR2RGB)
181
+
182
+ # Ensure correct shape
183
+ if self.is_rgb:
184
+ if slice_data.shape != (self.height, self.width, 3):
185
+ slice_data = cv2.resize(slice_data, (self.width, self.height), interpolation=cv2.INTER_LANCZOS4)
186
+ else:
187
+ if slice_data.shape != (self.height, self.width):
188
+ slice_data = cv2.resize(slice_data, (self.width, self.height), interpolation=cv2.INTER_LANCZOS4)
189
+
190
+ slices.append(slice_data.astype(np.uint8))
191
+
192
+ # Stack slices
193
+ result = np.stack(slices, axis=0)
194
+
195
+ # Pad to expected chunk shape if needed (for partial chunks)
196
+ if result.shape[0] < self.n_slices:
197
+ if self.is_rgb:
198
+ padding_shape = (self.n_slices - result.shape[0], self.height, self.width, 3)
199
+ else:
200
+ padding_shape = (self.n_slices - result.shape[0], self.height, self.width)
201
+ padding = np.zeros(padding_shape, dtype=np.uint8)
202
+ result = np.concatenate([result, padding], axis=0)
203
+
204
+ return result
205
+
206
+ def get_config(self) -> Dict[str, Any]:
207
+ """Get codec configuration."""
208
+ return {
209
+ "id": self.codec_id,
210
+ "quality": self.quality,
211
+ "chunk_shape": list(self.chunk_shape),
212
+ }
213
+
214
+ @classmethod
215
+ def from_config(cls, config: Dict[str, Any]) -> "ChunkedJPEGCodec":
216
+ """Create codec from configuration."""
217
+ quality = config.get("quality", 85)
218
+ chunk_shape = config.get("chunk_shape")
219
+ if chunk_shape:
220
+ chunk_shape = tuple(chunk_shape)
221
+ return cls(quality=quality, chunk_shape=chunk_shape)
222
+
223
+
224
+ # Register codec with numcodecs (for Zarr v2 compatibility)
225
+ try:
226
+ register_codec(ChunkedJPEGCodec)
227
+ except Exception:
228
+ pass