PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
htj2k_codec.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
7
8#include <algorithm>
9#include <cstring>
10
11#ifdef PACS_WITH_HTJ2K_CODEC
12#include <openjph/ojph_codestream.h>
13#include <openjph/ojph_file.h>
14#include <openjph/ojph_mem.h>
15#include <openjph/ojph_params.h>
16#endif
17
19
21 bool use_rpcl,
22 float compression_ratio,
23 int resolution_levels)
24 : lossless_(lossless),
25 use_rpcl_(use_rpcl),
26 compression_ratio_(compression_ratio),
27 resolution_levels_(resolution_levels) {}
28
30
31htj2k_codec::htj2k_codec(htj2k_codec&&) noexcept = default;
32htj2k_codec& htj2k_codec::operator=(htj2k_codec&&) noexcept = default;
33
34std::string_view htj2k_codec::transfer_syntax_uid() const noexcept {
35 if (lossless_) {
36 return use_rpcl_ ? kTransferSyntaxUIDRPCL : kTransferSyntaxUIDLossless;
37 }
38 return kTransferSyntaxUIDLossy;
39}
40
41std::string_view htj2k_codec::name() const noexcept {
42 if (lossless_) {
43 return use_rpcl_ ? "HTJ2K with RPCL (Lossless)" : "HTJ2K (Lossless)";
44 }
45 return "HTJ2K (Lossy)";
46}
47
48bool htj2k_codec::is_lossy() const noexcept {
49 return !lossless_;
50}
51
52bool htj2k_codec::can_encode(const image_params& params) const noexcept {
53 if (params.width == 0 || params.height == 0) {
54 return false;
55 }
56
57 if (params.samples_per_pixel != 1 && params.samples_per_pixel != 3) {
58 return false;
59 }
60
61 if (params.bits_stored < 1 || params.bits_stored > 16) {
62 return false;
63 }
64
65 return true;
66}
67
68bool htj2k_codec::can_decode(const image_params& params) const noexcept {
69 return can_encode(params);
70}
71
72bool htj2k_codec::is_lossless_mode() const noexcept {
73 return lossless_;
74}
75
76bool htj2k_codec::is_rpcl_mode() const noexcept {
77 return use_rpcl_;
78}
79
80float htj2k_codec::compression_ratio() const noexcept {
81 return compression_ratio_;
82}
83
84int htj2k_codec::resolution_levels() const noexcept {
85 return resolution_levels_;
86}
87
88#ifdef PACS_WITH_HTJ2K_CODEC
89
90namespace {
91
92// Clamp resolution levels to valid range for image dimensions
93int compute_resolution_levels(int requested, uint16_t width, uint16_t height) {
94 int max_levels = 1;
95 auto min_dim = std::min(width, height);
96 while (min_dim > 1 && max_levels < requested) {
97 min_dim >>= 1;
98 ++max_levels;
99 }
100 return std::clamp(max_levels, 1, 32);
101}
102
103} // namespace
104
105codec_result htj2k_codec::encode(
106 std::span<const uint8_t> pixel_data,
107 const image_params& params,
108 [[maybe_unused]] const compression_options& options) const {
109
110 if (pixel_data.empty()) {
113 }
114
115 if (params.width == 0 || params.height == 0) {
117 kcenon::pacs::error_codes::compression_error, "Invalid image dimensions");
118 }
119
120 const int bytes_per_sample = (params.bits_stored <= 8) ? 1 : 2;
121 const size_t expected_size = static_cast<size_t>(params.width)
122 * params.height * params.samples_per_pixel * bytes_per_sample;
123 if (pixel_data.size() < expected_size) {
126 "Pixel data too small: expected " + std::to_string(expected_size)
127 + " bytes, got " + std::to_string(pixel_data.size()));
128 }
129
130 try {
131 ojph::codestream codestream;
132
133 // Configure SIZ marker (image dimensions and components)
134 ojph::param_siz siz = codestream.access_siz();
135 siz.set_image_extent(ojph::point{
136 static_cast<ojph::ui32>(params.width),
137 static_cast<ojph::ui32>(params.height)});
138 siz.set_image_offset(ojph::point{0, 0});
139 siz.set_tile_size(ojph::size{0, 0});
140 siz.set_tile_offset(ojph::point{0, 0});
141 siz.set_num_components(params.samples_per_pixel);
142
143 bool is_signed = (params.pixel_representation != 0);
144 for (ojph::ui32 c = 0; c < params.samples_per_pixel; ++c) {
145 siz.set_component(c, ojph::point{1, 1}, params.bits_stored, is_signed);
146 }
147
148 // Configure COD marker (coding parameters)
149 ojph::param_cod cod = codestream.access_cod();
150 int num_levels = compute_resolution_levels(
151 resolution_levels_, params.width, params.height);
152 cod.set_num_decomposition(static_cast<ojph::ui32>(num_levels));
153
154 // Block dimensions (64x64 is standard for HTJ2K)
155 cod.set_block_dims(64, 64);
156
157 // Reversible for lossless, irreversible for lossy
158 cod.set_reversible(lossless_);
159
160 // Color transform for multi-component images
161 cod.set_color_transform(params.samples_per_pixel == 3);
162
163 // Progression order
164 if (use_rpcl_) {
165 cod.set_progression_order("RPCL");
166 } else {
167 cod.set_progression_order("CPRL");
168 }
169
170 // For lossy mode, set quantization step
171 if (!lossless_) {
172 ojph::param_qcd qcd = codestream.access_qcd();
173 // Use a quality-preserving delta scaled by compression ratio.
174 // JPEG 2000 quantization cascades across subbands, so the
175 // base delta must be small enough for acceptable reconstruction.
176 float delta = compression_ratio_ / 10000.0f;
177 qcd.set_irrev_quant(delta);
178 }
179
180 // Set planar mode based on component count
181 // Planar = process one component fully before the next
182 // Non-planar needed when color transform is used
183 codestream.set_planar(params.samples_per_pixel == 1);
184
185 // Write to memory buffer
186 ojph::mem_outfile output;
187 output.open();
188
189 codestream.write_headers(&output);
190
191 // Push pixel data row by row
192 const auto width = static_cast<ojph::ui32>(params.width);
193 const auto height = static_cast<ojph::ui32>(params.height);
194 const auto num_comps = static_cast<ojph::ui32>(params.samples_per_pixel);
195
196 ojph::ui32 next_comp = 0;
197 ojph::line_buf* line = codestream.exchange(nullptr, next_comp);
198
199 for (ojph::ui32 y = 0; y < height; ++y) {
200 for (ojph::ui32 c = 0; c < num_comps; ++c) {
201 // Fill line buffer with pixel data for this row/component.
202 // OpenJPH uses i32 at the API boundary for both lossless and
203 // lossy modes; the library handles float conversion internally
204 // during the irreversible wavelet transform.
205 auto* dst = line->i32;
206 if (bytes_per_sample == 1) {
207 if (num_comps == 1) {
208 const uint8_t* src = pixel_data.data()
209 + static_cast<size_t>(y) * width;
210 for (ojph::ui32 x = 0; x < width; ++x) {
211 dst[x] = is_signed
212 ? static_cast<ojph::si32>(static_cast<int8_t>(src[x]))
213 : static_cast<ojph::si32>(src[x]);
214 }
215 } else {
216 const uint8_t* src = pixel_data.data()
217 + static_cast<size_t>(y) * width * num_comps;
218 for (ojph::ui32 x = 0; x < width; ++x) {
219 dst[x] = is_signed
220 ? static_cast<ojph::si32>(
221 static_cast<int8_t>(src[x * num_comps + next_comp]))
222 : static_cast<ojph::si32>(src[x * num_comps + next_comp]);
223 }
224 }
225 } else {
226 if (num_comps == 1) {
227 const auto* src = reinterpret_cast<const uint16_t*>(
228 pixel_data.data() + static_cast<size_t>(y) * width * 2);
229 for (ojph::ui32 x = 0; x < width; ++x) {
230 dst[x] = is_signed
231 ? static_cast<ojph::si32>(static_cast<int16_t>(src[x]))
232 : static_cast<ojph::si32>(src[x]);
233 }
234 } else {
235 const auto* src = reinterpret_cast<const uint16_t*>(
236 pixel_data.data()
237 + static_cast<size_t>(y) * width * num_comps * 2);
238 for (ojph::ui32 x = 0; x < width; ++x) {
239 dst[x] = is_signed
240 ? static_cast<ojph::si32>(
241 static_cast<int16_t>(src[x * num_comps + next_comp]))
242 : static_cast<ojph::si32>(src[x * num_comps + next_comp]);
243 }
244 }
245 }
246
247 line = codestream.exchange(line, next_comp);
248 }
249 }
250
251 codestream.flush();
252
253 // Copy compressed data BEFORE close (close deallocates the buffer)
254 auto compressed_size = static_cast<size_t>(output.tell());
255 std::vector<uint8_t> result_data(compressed_size);
256 if (compressed_size > 0) {
257 std::memcpy(result_data.data(), output.get_data(), compressed_size);
258 }
259
260 codestream.close();
261
262 return kcenon::pacs::ok<compression_result>(
263 compression_result{std::move(result_data), params});
264
265 } catch (const std::exception& e) {
268 std::string("HTJ2K encoding failed: ") + e.what());
269 }
270}
271
272codec_result htj2k_codec::decode(
273 std::span<const uint8_t> compressed_data,
274 const image_params& params) const {
275
276 if (compressed_data.empty()) {
278 kcenon::pacs::error_codes::decompression_error, "Empty compressed data");
279 }
280
281 try {
282 ojph::codestream codestream;
283
284 // Read from memory
285 ojph::mem_infile input;
286 input.open(compressed_data.data(), compressed_data.size());
287
288 codestream.read_headers(&input);
289
290 // Get image information from the codestream headers
291 ojph::param_siz siz = codestream.access_siz();
292 ojph::point extent = siz.get_image_extent();
293 auto decoded_width = static_cast<uint16_t>(extent.x);
294 auto decoded_height = static_cast<uint16_t>(extent.y);
295 auto num_comps = static_cast<uint16_t>(siz.get_num_components());
296 auto bit_depth = static_cast<uint16_t>(siz.get_bit_depth(0));
297 bool is_signed = siz.is_signed(0);
298
299 // Build output parameters
300 image_params output_params = params;
301 output_params.width = decoded_width;
302 output_params.height = decoded_height;
303 output_params.samples_per_pixel = num_comps;
304 output_params.bits_stored = bit_depth;
305 output_params.bits_allocated = (bit_depth <= 8) ? 8 : 16;
306 output_params.high_bit = bit_depth - 1;
307 output_params.pixel_representation = is_signed ? 1 : 0;
308 output_params.planar_configuration = 0;
309
310 if (num_comps == 1) {
311 output_params.photometric = photometric_interpretation::monochrome2;
312 } else if (num_comps == 3) {
313 output_params.photometric = photometric_interpretation::rgb;
314 }
315
316 // Validate dimensions if provided
317 if (params.width > 0 && params.width != decoded_width) {
320 "Image width mismatch: expected " + std::to_string(params.width)
321 + ", got " + std::to_string(decoded_width));
322 }
323 if (params.height > 0 && params.height != decoded_height) {
326 "Image height mismatch: expected " + std::to_string(params.height)
327 + ", got " + std::to_string(decoded_height));
328 }
329
330 // Set planar mode matching the encoder
331 codestream.set_planar(num_comps == 1);
332
333 codestream.create();
334
335 // Allocate output buffer
336 const int bytes_per_sample = (bit_depth <= 8) ? 1 : 2;
337 const size_t output_size = static_cast<size_t>(decoded_width)
338 * decoded_height * num_comps * bytes_per_sample;
339 std::vector<uint8_t> output_data(output_size);
340
341 // Pull decoded data row by row.
342 // OpenJPH returns i32 from pull() for both reversible and
343 // irreversible modes (internal float-to-int conversion is done).
344 ojph::ui32 comp_num = 0;
345 for (ojph::ui32 y = 0; y < decoded_height; ++y) {
346 for (ojph::ui32 c = 0; c < num_comps; ++c) {
347 ojph::line_buf* line = codestream.pull(comp_num);
348
349 if (bytes_per_sample == 1) {
350 if (num_comps == 1) {
351 auto* dst = output_data.data()
352 + static_cast<size_t>(y) * decoded_width;
353 for (ojph::ui32 x = 0; x < decoded_width; ++x) {
354 auto val = line->i32[x];
355 dst[x] = static_cast<uint8_t>(std::clamp(val, 0, 255));
356 }
357 } else {
358 auto* dst = output_data.data()
359 + static_cast<size_t>(y) * decoded_width * num_comps;
360 for (ojph::ui32 x = 0; x < decoded_width; ++x) {
361 auto val = line->i32[x];
362 dst[x * num_comps + comp_num] =
363 static_cast<uint8_t>(std::clamp(val, 0, 255));
364 }
365 }
366 } else {
367 if (num_comps == 1) {
368 auto* dst = reinterpret_cast<uint16_t*>(
369 output_data.data()
370 + static_cast<size_t>(y) * decoded_width * 2);
371 for (ojph::ui32 x = 0; x < decoded_width; ++x) {
372 auto val = line->i32[x];
373 if (is_signed) {
374 dst[x] = static_cast<uint16_t>(
375 static_cast<int16_t>(
376 std::clamp(val, -32768, 32767)));
377 } else {
378 dst[x] = static_cast<uint16_t>(
379 std::clamp(val, 0, 65535));
380 }
381 }
382 } else {
383 auto* dst = reinterpret_cast<uint16_t*>(
384 output_data.data()
385 + static_cast<size_t>(y) * decoded_width * num_comps * 2);
386 for (ojph::ui32 x = 0; x < decoded_width; ++x) {
387 auto val = line->i32[x];
388 if (is_signed) {
389 dst[x * num_comps + comp_num] =
390 static_cast<uint16_t>(
391 static_cast<int16_t>(
392 std::clamp(val, -32768, 32767)));
393 } else {
394 dst[x * num_comps + comp_num] =
395 static_cast<uint16_t>(
396 std::clamp(val, 0, 65535));
397 }
398 }
399 }
400 }
401 }
402 }
403
404 codestream.close();
405
406 return kcenon::pacs::ok<compression_result>(
407 compression_result{std::move(output_data), output_params});
408
409 } catch (const std::exception& e) {
412 std::string("HTJ2K decoding failed: ") + e.what());
413 }
414}
415
416#else // !PACS_WITH_HTJ2K_CODEC
417
419 [[maybe_unused]] std::span<const uint8_t> pixel_data,
420 [[maybe_unused]] const image_params& params,
421 [[maybe_unused]] const compression_options& options) const {
424 "HTJ2K codec not available: OpenJPH library not found at build time");
425}
426
428 [[maybe_unused]] std::span<const uint8_t> compressed_data,
429 [[maybe_unused]] const image_params& params) const {
432 "HTJ2K codec not available: OpenJPH library not found at build time");
433}
434
435#endif // PACS_WITH_HTJ2K_CODEC
436
437} // namespace kcenon::pacs::encoding::compression
High-Throughput JPEG 2000 (HTJ2K) codec implementation.
Definition htj2k_codec.h:47
float compression_ratio() const noexcept
Gets the current compression ratio setting.
bool can_decode(const image_params &params) const noexcept override
Checks if this codec can decode data with given parameters.
bool is_rpcl_mode() const noexcept
Checks if RPCL progression order is enabled.
bool can_encode(const image_params &params) const noexcept override
Checks if this codec supports the given image parameters.
bool is_lossless_mode() const noexcept
Checks if this codec is configured for lossless mode.
std::string_view name() const noexcept override
Returns a human-readable name for the codec.
codec_result encode(std::span< const uint8_t > pixel_data, const image_params &params, const compression_options &options={}) const override
Compresses pixel data to HTJ2K format.
htj2k_codec(bool lossless=true, bool use_rpcl=false, float compression_ratio=kDefaultCompressionRatio, int resolution_levels=kDefaultResolutionLevels)
Constructs an HTJ2K codec instance.
int resolution_levels() const noexcept
Gets the number of DWT resolution levels.
bool is_lossy() const noexcept override
Checks if this codec produces lossy compression.
codec_result decode(std::span< const uint8_t > compressed_data, const image_params &params) const override
Decompresses HTJ2K data.
constexpr dicom_tag pixel_data
Pixel Data.
@ monochrome2
Minimum pixel value displayed as black.
constexpr int compression_error
Definition result.h:78
constexpr int decompression_error
Definition result.h:79
Result< T > pacs_error(int code, const std::string &message, const std::string &details="")
Create a PACS error result with module context.
Definition result.h:234
Result<T> type aliases and helpers for PACS system.
Compression quality settings for lossy codecs.
Parameters describing image pixel data.