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.
- cft_zarr-0.0.2/PKG-INFO +58 -0
- cft_zarr-0.0.2/README.md +41 -0
- cft_zarr-0.0.2/cft_zarr/__init__.py +159 -0
- cft_zarr-0.0.2/cft_zarr/chunked_jpeg.py +228 -0
- cft_zarr-0.0.2/cft_zarr/jpeg.py +368 -0
- cft_zarr-0.0.2/cft_zarr/jpeg_compressor.py +474 -0
- cft_zarr-0.0.2/cft_zarr/jpegxl.py +142 -0
- cft_zarr-0.0.2/cft_zarr/shift12jls.py +281 -0
- cft_zarr-0.0.2/cft_zarr/shift12jls_compressor.py +321 -0
- cft_zarr-0.0.2/pyproject.toml +22 -0
cft_zarr-0.0.2/PKG-INFO
ADDED
|
@@ -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
|
+
|
cft_zarr-0.0.2/README.md
ADDED
|
@@ -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
|