Add 16-bit Bayer pixel format support#54
Conversation
Add RGGB_16 and GBRG_16 pixel format definitions and supporting infrastructure for encoding and decoding 16-bit raw Bayer sensor data. - Add PIXEL_FORMAT_RAW_RGGB_16 and PIXEL_FORMAT_RAW_GBRG_16 enums - Add 16-bit log curve tables (EncoderLogCurve16, DecoderLogCurve16) with 65536-entry LUTs for the larger input domain - Add integer-based uncompanding table (InitUncompandTable) to replace double-precision cubic evaluation in the decoder hot path - Extend image format handling for 16-bit pixel formats - Add 16-bit support in bitstream and wavelet common code Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend the VC5 encoder to support 16-bit Bayer input with appropriate quantization scaling and three new high-quality presets for the larger dynamic range. 16-bit encoding: - Add VC5_ENCODER_PIXEL_FORMAT_RGGB_16 and GBRG_16 format handling - Add UnpackImage_16() for 16-bit raw Bayer data - Scale quantization tables by 12/bits for bit depths > 12, protecting the lowpass band (index 0) which must always keep quant=1 - Adjust midpoint_prequant for 16-bit (value 3 for >= 15-bit) - Use dynamic VC5 buffer sizing (1.5x raw size + 1MB) instead of fixed 10MB to handle larger 16-bit payloads Quality presets: - Add Q6 (Filmscan-3/Edit-Safe), Q7 (Filmscan-4/Near-Lossless), Q8 (Filmscan-5/Virtually-Lossless) quality settings Performance: - Parallelize forward wavelet transforms using pthreads (one thread per Bayer channel, ~2x speedup on multi-core) - Link encoder against pthreads Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extend the VC5 decoder to support 16-bit Bayer output with optimized entropy decoding and parallel inverse wavelet transforms. 16-bit decoding: - Add VC5_DECODER_PIXEL_FORMAT_RGGB_16 and GBRG_16 format handling - Add fast 16-bit lowpass coefficient bulk read path that extracts coefficients directly from the 32-bit bitstream buffer - Pass actual bit depth through to RGB conversion instead of hardcoding 12 or 14 bits - Add 16-bit repacking in decoder raw output VLC prefix lookup table: - Build a 4096-entry prefix table (12-bit peek) from the codebook for O(1) codeword lookup instead of linear search through 264 entries - Thread-safe initialization via pthread_once - Fast combined RLV + sign bit decode (GetRunFast) with automatic fallback to original GetRun for long codewords - Safety valve: set VLC_NO_FAST=1 to disable for verification Optimized highpass run-length decoding: - Fast path for runs that stay within a single row (common case) - Use memset for zero-valued runs instead of per-pixel loop - Batch row-boundary handling for cross-row runs Performance: - Parallelize inverse wavelet transforms using pthreads (one thread per Bayer channel) - Fix VLC table initialization race condition with pthread_once - Check GetBuffer() return value in lowpass band reader - Link decoder against pthreads Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wire 16-bit Bayer pixel format support through the GPR SDK layer and command-line tools, with several robustness fixes discovered during testing. SDK integration: - Add PIXEL_FORMAT_RGGB_16 and PIXEL_FORMAT_GBRG_16 to gpr_tuning_info - Add quality parameter to gpr_parameters for explicit quality selection - Pass 16-bit pixel formats through set_vc5_encoder_parameters() - Auto-detect bit depth from DNG WhiteLevel during decoding - Add default_crop_size_h/v fields to gpr_tuning_info Bug fixes: - Guard ProfileByIndex() call against empty profile count - Guard GetLinearizationInfo() against null profile pointer - Add HueSatMap 64-bit overflow check (reject > 8M entries) - Deep-copy and properly destroy HueSatMap data in gpr_parameters - Auto-compute input pitch from width when pitch is 0 or unset - Set saturation levels correctly per pixel format bit depth CLI (gpr_tools): - Add --Quality/-q flag for explicit encoder quality selection (0=Low through 8=Virtually-Lossless, -1=auto) - Add rggb16 and gbrg16 to --InputPixelFormat options - Auto-compute input pitch from width (default 0 instead of 8000) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Enable link-time optimization (LTO/IPO) for cross-translation-unit inlining of hot paths (GetBits, GetBuffer, VLC lookup) - Auto-detect ARM NEON on arm64/aarch64 (Apple Silicon M-series) instead of requiring manual -DNEON=1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Detailed PSNR, ENOB, and editing latitude analysis for all Q0-Q8 presets tested on 100MP 16-bit Hasselblad X2D sensor data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
This is great stuff, I have to find the time to try it all out. Thanks. |
The optimized fast path in DecodeBandRuns consumed all run pixels but never zeroed run.count, causing the post-loop assertion `assert(data_count == 0 && run.count == 0)` to fail on every decode. The slow path correctly decremented run.count to zero, but the fast path (which handles the common case of runs within a single row) skipped this step. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test Plan VerificationAll 5 checklist items have been verified. One critical bug was found and fixed (commit 28dd522). Results
Bug Fixed:
|
| Quality | GPR Size | PSNR |
|---|---|---|
| Q0 | 3.2 MB | 70.91 dB |
| Q2 | 4.5 MB | 75.21 dB |
| Q4 | 5.4 MB | 79.34 dB |
🤖 Tested with Claude Code
…o DNGs Two fixes discovered while testing with a Hasselblad X2D 100C DNG: 1. gpr.cpp: EXIF software_version and user_comment fields used assert + unchecked memcpy, crashing on DNGs with strings longer than 32 bytes. Now truncates safely with null termination. 2. syntax.c: PutTagPair/PutTagPairOptional asserted that tag values fit in 16 bits, but TAGWORD (int16_t) sign-extends to int when the high bit is set. The packed prescale shift for 16-bit data (0xBC00) became 0xFFFFBC00 (-17408), failing the assertion. Now masks through uint16_t before the check. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Additional Fixes and Verified PSNR DataNew fixes (8ec08a0)
Verified 16-bit PSNR (Hasselblad X2D 100C, 11664×8750)Measured via RAW round-trip on the actual X2D test image:
All values match the PR description within rounding. Note on 12-bit output differences vs masterThe widening of 🤖 Tested with Claude Code |
- VLC: Assert single-codebook assumption instead of silently overwriting the global codebook pointer on every call to GetRunFast - raw.c: Add explicit GBRG format cases and assert on unknown pixel formats instead of silently defaulting to GBRG order Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…erflow - vc5_encoder.c: Add lower-bound check on quality_setting to prevent negative index into quant_table array (buffer underflow) - gpr.cpp: Add comment clarifying GBRG 14-bit maps to GBRG_16 (no GBRG_14 enum exists); zero HueSatMap dims when sanity check triggers to prevent downstream out-of-bounds read from stale dimensions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Full PR Review and RetestCode ReviewThree parallel review agents audited the decoder, encoder, and SDK/common changes (~4K lines across 43 files). Findings triaged and addressed: Bugs fixed (451c2bf, 221968f):
Reviewed and dismissed:
Test Results
Commits added since last update
🤖 Reviewed and tested with Claude Code |
The COMPONENT_VALUE type was widened from uint16_t (2 bytes) to int32_t (4 bytes), but the pitch-to-element-count conversion in the GPR_RGB_RESOLUTION_HALF path still divided by 2 instead of sizeof(COMPONENT_VALUE). This caused WaveletToRGB to use a stride of 2*width instead of width, reading past allocated buffer boundaries and producing corrupted RGB output at half resolution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Systematic Codebase Audit (10 Risk Areas)Five parallel audit agents reviewed the entire PR diff across 10 risk areas. One critical bug found and fixed. Critical Bug Fixed (90bf199)WaveletToRGB pitch/sizeof mismatch — Audit Summary
All Commits in PR (10 total)
🤖 Audited with Claude Code |
Summary
Add support for encoding and decoding 16-bit raw Bayer sensor data (RGGB_16, GBRG_16) in the VC5/GPR codec, along with performance optimizations and robustness fixes.
Quality Analysis — 16-Bit Sensor Data
Tested on a 100-megapixel 16-bit Hasselblad X2D image (11664×8750, 194.7 MB raw). This sensor captures ~14 bits of real photographic data at base ISO — typical of modern medium-format and high-end full-frame sensors.
Raw Bayer Domain PSNR
This is the domain RAW editors (Lightroom, Capture One, RawTherapee, etc.) operate in. Errors here directly affect all downstream edits.
ENOB = Effective Number of Bits = PSNR / 6.02. DN = out of 65535 for 16-bit.
Q6-Q8 (bold) are the new presets added in this PR.
What This Means for Editing Latitude
Each stop (EV) of exposure adjustment doubles or halves pixel values, which effectively costs ~1 bit of precision. The practical editing headroom is roughly:
ENOB − display bits = available editing stops
For an 8-bit final output (web, prints, most displays):
14-Bit vs 16-Bit Context
Most current cameras output 14-bit raw data. 16-bit sensors (like the tested X2D) add 2 more bits of dynamic range (~12 additional dB), which is critical for:
For 14-bit sensor data (e.g., GoPro HERO, most DSLRs/mirrorless), the quantization tables already scale automatically (
scale = 12/bits), meaning:For 16-bit sensor data, the new Q6-Q8 presets are essential:
Recommended Presets
Commits
Backwards Compatibility
VLC_NO_FAST=1)Test plan
VLC_NO_FAST=1— verify bit-exact match with fast path🤖 Generated with Claude Code