Repository navigation
Add trpx filter - #413
Add trpx filter#413dirkboye wants to merge 3 commits into
Conversation
git-subtree-dir: lib/trpx2.0 git-subtree-split: 93cf8e4b7e832da3e318e9d37d73f70e3ce08eeb
Add the Terse HDF5 filter (registered filter ID 32029) from TERSE/PROLIX v2.0 (TRPX, https://github.com/senikm/trpx2.0), embedded as a git subtree in lib/trpx2.0 (MIT license). TERSE/PROLIX is a fast, lossless compression algorithm for integer greyscale images with a high dynamic range, such as X-ray and electron diffraction patterns. It encodes the data in small blocks of values (12 by default): each block is stripped of its unused most significant bits and is preceded by a short header giving the number of bits per value, which is reused from the previous block when unchanged. Version 2.0 compresses in parallel and adds compression modes for unsigned data with overloads and for very weak data. See S. Matinyan et al., Ultramicroscopy 281 (2026) 114298, https://doi.org/10.1016/j.ultramic.2025.114298. The filter supports signed and unsigned 8-, 16- and 32-bit integer datasets. It takes no user options: its set_local callback stores the dataset's data type code in cd_values. hdf5plugin.Terse provides the compression arguments for h5py. The filter is written in C++20, so the build now also handles the C++20 compile flag for filter extensions (previously only for static libraries).
|
Hi, I tried the plugin and found that recompressing uint16 data (this dataset: https://www.silx.org/pub/pyFAI/kevlar/) was randomly wrong ... Same for uin32. Signed datasets looks OK. I believe this is a killer for a lossless compression plugin. Performances are also 50% worse the bslz4 when it comes to compression ratio and 100% worse for the read-speed, but a laptop is not a decent benchmarking tool. Claude/Opus provided a minimum reproducing example: |
|
Here is also the report (AI-generated): Terse filter is not lossless for
|
| Number of elements | Sub-frames | Result |
|---|---|---|
| up to 3·2²⁰ + 12 | up to 12 | always correct |
| 4·2²⁰ + 4 | 16 | reproducibly wrong |
A full Eiger 4M frame (4.49 M pixels, 17 sub-frames) is affected.
Constant data is not affected. All-zero, all-one and constant-7 data round-trip correctly.
Real detector data is affected badly. On real Eiger 4M uint16 frames, which contain many
65535 gap/masked pixels, up to 1.8 M pixels out of 4.5 M are wrong after a round-trip. 18 of the
20 frames tested were corrupted. The same frames converted to int16 (bitwise view) or int32
were lossless on 200 frames.
Possible cause
This is only a guess. The read-time non-determinism, together with the fact that only multi-frame
chunks fail, points to Terse<Concurrent>::prolix(container), which decodes sub-frames in parallel
into one shared buffer. The unsigned decoders (f_prolix_unsigned, f_prolix_small_unsigned*)
could read or write past to on the last partial block, for example by working on whole blocks of
d_block values. Neighbouring sub-frames would then race on those few values.
Two quick checks could confirm this:
- set
TERSE_DEFAULT_CHUNK_SIZEto a multiple of 12; - decode the sub-frames sequentially.
Suggestions for the PR
- Add a round-trip test on a large, realistic unsigned array to
src/hdf5plugin/test.py, for
example a Poisson-distributeduint16array of at least 4·2²⁰ elements with a few 65535 values.
The current test usesnumpy.arange(256**2), which is a single sub-frame, so it cannot catch
this. - Report the bug upstream in senikm/trpx2.0 if it is in
Terse.hpprather than infilter.cpp.
The HDF5 filter splits a chunk into frames of 2^18 values. As 2^18 is not a multiple of the block size of 12, every frame ends with a short block, which the decoder read with the bits of a full block, past the end of the frame (silx-kit/hdf5plugin#413). test_short_last_block decodes uint16 and uint32 data written by the encoder before the fix in the layout of the HDF5 filter, with frames of 16 values. The zero padding of the frames is filled with ones, so the test fails reliably without the fix instead of depending on the memory after the frames. test_hdf5_filter_frames is a round trip of an Eiger 4M sized image of small Poisson distributed values split into frames like the HDF5 filter. Both tests decode frame by frame, as prolix() of a Terse object with several frames but without dimensions, e.g. loaded from a file, expects an array for one frame only.
Eiger images contain saturated pixels in the module gaps, for which the encoder and decoder disagreed on the block headers (silx-kit/hdf5plugin#413 reports up to 1.8 M of 4.5 M pixels wrong). test_saturated_module_gaps is a round trip of an Eiger 4M like image of small Poisson distributed values with saturated module gaps, for uint16 and uint32. The number of values is a multiple of the block size of 12, so the test does not depend on the decoding of a short last block. test_saturated_block_followed_by_small_values is a round trip of a saturated block followed by blocks of small values, which failed for uint32 values as the maximum value + 1 wraps around to 0.
|
Thank you for the review! I've created 2 merge requests into https://github.com/senikm/trpx2.0/ which should fix the issues: |
Add the Terse HDF5 filter (registered filter ID 32029) from
TERSE/PROLIX v2.0 (TRPX, https://github.com/senikm/trpx2.0), embedded
as a git subtree in lib/trpx2.0 (MIT license).
TERSE/PROLIX is a fast, lossless compression algorithm for integer
greyscale images with a high dynamic range, such as X-ray and
electron diffraction patterns. It encodes the data in small blocks of
values (12 by default): each block is stripped of its unused most
significant bits and is preceded by a short header giving the number
of bits per value, which is reused from the previous block when
unchanged. Version 2.0 compresses in parallel and adds compression
modes for unsigned data with overloads and for very weak data.
See S. Matinyan et al., Ultramicroscopy 281 (2026) 114298,
https://doi.org/10.1016/j.ultramic.2025.114298.
The filter supports signed and unsigned 8-, 16- and 32-bit integer
datasets. It takes no user options: its set_local callback stores the
dataset's data type code in cd_values. hdf5plugin.Terse provides the
compression arguments for h5py.
The filter is written in C++20, so the build now also handles the C++20
compile flag for filter extensions (previously only for static
libraries).