OpenJPH
Open-source implementation of JPEG2000 Part-15
Loading...
Searching...
No Matches
test_nlt.cpp
Go to the documentation of this file.
1//***************************************************************************/
2// This software is released under the 2-Clause BSD license, included
3// below.
4//
5// Copyright (c) 2019, Aous Naman
6// Copyright (c) 2019, Kakadu Software Pty Ltd, Australia
7// Copyright (c) 2019, The University of New South Wales, Australia
8//
9// Redistribution and use in source and binary forms, with or without
10// modification, are permitted provided that the following conditions are
11// met:
12//
13// 1. Redistributions of source code must retain the above copyright
14// notice, this list of conditions and the following disclaimer.
15//
16// 2. Redistributions in binary form must reproduce the above copyright
17// notice, this list of conditions and the following disclaimer in the
18// documentation and/or other materials provided with the distribution.
19//
20// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
21// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
22// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
23// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
26// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31//***************************************************************************/
32// This file is part of the OpenJPH software implementation.
33// File: test_nlt.cpp
34//***************************************************************************/
35// Tests of the non-linearity point transformation (NLT) marker segment, for
36// the three nonlinearities this library supports, namely
37//
38// OJPH_NLT_LUT_STYLE_NLT = 2 -- LUT style
39// OJPH_NLT_BINARY_COMPLEMENT_NLT = 3 -- binary complement (SMAG)
40// OJPH_NLT_BINARY_COMPLEMENT_PLUS_LUT = 4 -- binary complement plus a LUT
41//
42// The tests drive the library directly rather than the ojph_compress and
43// ojph_expand executables, because only the library lets a caller choose the
44// nonlinearity and describe the look-up table it uses. Encoding and
45// decoding are both exercised: each test encodes an image with a
46// nonlinearity in effect and then decodes the codestream it produced,
47// comparing the decoded samples with the samples that were fed in.
48//
49// The nonlinearity is a point transformation applied before the wavelet
50// transform when encoding, and undone after the inverse wavelet transform
51// when decoding. Type 3 is exactly reversible, so a round trip through a
52// reversible (5/3) codestream returns the samples bit for bit. Types 2 and 4
53// describe the transformation with a look-up table, which is an
54// approximation, so those round trips are only approximately lossless.
55//***************************************************************************/
56
57#include <algorithm>
58#include <cmath>
59#include <cstdio>
60#include <cstring>
61#include <iostream>
62#include <string>
63#include <vector>
64
65#include "ojph_arch.h"
66#include "ojph_mem.h"
67#include "ojph_message.h"
68#include "ojph_file.h"
69#include "ojph_codestream.h"
70#include "ojph_params.h"
71#include "gtest/gtest.h"
72
73using namespace ojph;
74
75#ifdef OJPH_OS_WINDOWS
76#define REF_FILE_DIR ".\\jp2k_test_codestreams\\openjph\\references\\"
77#define OUT_FILE_DIR ".\\"
78#else
79#define REF_FILE_DIR "./jp2k_test_codestreams/openjph/references/"
80#define OUT_FILE_DIR "./"
81#endif
82
83// the JPEG2000 Part 2 marker segment that carries the nonlinearity; see
84// JP2K_MARKER::NLT
85#define NLT_MARKER_HIGH 0xFF
86#define NLT_MARKER_LOW 0x76
87
88namespace {
89
91 // The look-up table that ojph_compress installs for floating point (.pfm)
92 // images. It is repeated here so that this test does not depend on the
93 // application. Dmin and Dmax are the smallest and the largest 32 bit
94 // pattern the samples of an image can take, the points are the look-up
95 // table itself, and the precision of a point (pt_val) is 32 bits.
97 const ui32 pfmLutNumPoints = 152;
98 const ui32 pfmLutDmin = 67109888;
99 const ui32 pfmLutDmax = 4227857408;
100 const ui32 pfmLutPoints[pfmLutNumPoints] = {
101 67109888, 88737098, 106890847, 122160968, 135661590, 157092189,
102 175049327, 190122837, 204213292, 225447280, 243207807, 258084706,
103 272764994, 293736834, 311300750, 326046575, 341316696, 362026388,
104 379393693, 394008444, 409802861, 430315942, 447486636, 461904776,
105 478289026, 498605496, 515579579, 529866645, 546775191, 566829513,
106 583672522, 597762977, 615261356, 635053530, 651699928, 665659309,
107 683681984, 703343084, 719792871, 733555641, 752102612, 771501564,
108 787820277, 801451973, 820523240, 839725581, 855847683, 869348305,
109 888943868, 907949598, 923875089, 937179100, 957364496, 976108078,
110 991836958, 1005075432, 1025719587, 1044266558, 1059864364, 1072906227,
111 1094074678, 1112425038, 1127891770, 1140802559, 1163281750, 1183532683,
112 1201620895, 1227180325, 1254836939, 1281838183, 1321029309, 1362907452,
113 1412912183, 1485985938, 1596677931, 1830579484, 2464387812, 2698289365,
114 2808981358, 2882055113, 2932059844, 2973937987, 3013129113, 3040130357,
115 3067786971, 3093346401, 3111434613, 3131685546, 3154164737, 3167075526,
116 3182542258, 3200892618, 3222061069, 3235102932, 3250700738, 3269247709,
117 3289891864, 3303130338, 3318859218, 3337602800, 3357788196, 3371092207,
118 3387017698, 3406023428, 3425618991, 3439119613, 3455241715, 3474444056,
119 3493515323, 3507147019, 3523465732, 3542864684, 3561411655, 3575174425,
120 3591624212, 3611285312, 3629307987, 3643267368, 3659913766, 3679705940,
121 3697204319, 3711294774, 3728137783, 3748192105, 3765100651, 3779387717,
122 3796361800, 3816678270, 3833062520, 3847480660, 3864651354, 3885164435,
123 3900958852, 3915573603, 3932940908, 3953650600, 3968920721, 3983666546,
124 4001230462, 4022202302, 4036882590, 4051759489, 4069520016, 4090754004,
125 4104844459, 4119917969, 4137875107, 4159305706, 4172806328, 4188076449,
126 4206230198, 4227857408};
127
129 // A look-up table that runs over the whole range of 32 bit patterns. The
130 // curve is a straight line, hence this table describes an identity
131 // transformation and a round trip through it has to be as accurate as a
132 // round trip without a nonlinearity.
134 const ui32 identityLutNumPoints = 129;
135 ui32 identityLutPoints[identityLutNumPoints];
136 bool identityLutReady = false;
137
138 void prepare_identity_lut()
139 {
140 if (identityLutReady)
141 return;
142 for (ui32 i = 0; i < identityLutNumPoints; ++i)
143 identityLutPoints[i] =
144 (ui32)(((double)i / (identityLutNumPoints - 1)) * 4294967295.0);
145 identityLutReady = true;
146 }
147
149 // A look-up table whose entries only cover the middle half of the range of
150 // 32 bit patterns, from 1/4 to 3/4 of it, although the table is indexed over
151 // the whole range. The curve is a straight line. The first and the last
152 // entry of a table are not, in general, the smallest and the largest value
153 // the table is indexed with, and this table checks that the encoder does
154 // not mix the two up.
156 const ui32 narrowLutNumPoints = 129;
157 ui32 narrowLutPoints[narrowLutNumPoints];
158 bool narrowLutReady = false;
159
160 void prepare_narrow_lut()
161 {
162 if (narrowLutReady)
163 return;
164 for (ui32 i = 0; i < narrowLutNumPoints; ++i)
165 narrowLutPoints[i] = (ui32)(1073741824.0 +
166 ((double)i / (narrowLutNumPoints - 1)) * 2147483648.0);
167 narrowLutReady = true;
168 }
169
171 // A look-up table of three entries that are not on a straight line: the
172 // curve is made of two straight segments that meet at the middle entry,
173 // where the slope changes. The inverse of this table changes slope at the
174 // value of the middle entry, and that value does not coincide with an entry
175 // of the uniformly sampled table the encoder derives from it. Like the
176 // table above, it is indexed over the whole range of 32 bit patterns and
177 // its entries run from 1/4 to 3/4 of that range.
179 const ui32 bentLutNumPoints = 3;
180 const ui8 bentLutPoints8[bentLutNumPoints] =
181 { 0x40u, 0x50u, 0xC0u };
182 const ui16 bentLutPoints16[bentLutNumPoints] =
183 { 0x4000u, 0x5000u, 0xC000u };
184 const ui32 bentLutPoints[bentLutNumPoints] =
185 { 0x40000000u, 0x50000000u, 0xC0000000u };
186
188 // A look-up table of 1024 entries of which 512, starting at the 128th, are
189 // packed close together: the table has a long run of almost flat segments,
190 // and the gaps inside that run alternate between 32768 and 262144, a ratio
191 // of one to eight. The ratio of the range of the table to its smallest gap
192 // is 114688, which is more than the 8192 entries the encoder is willing to
193 // derive from the table, so the encoder warns about flat segments and
194 // derives 8192 entries instead. One step of that many entries is worth four
195 // of the alternating gaps of the run, which means that inverting the table
196 // at a value inside the run has to look four entries ahead of the entry the
197 // value is found at; this is the only table used below for which the encoder
198 // has to search more than one step. The gaps of the run are not equal on
199 // purpose, and neither is the run placed in the middle of the table: with
200 // equal gaps, taking one segment too few still gives the right answer,
201 // because the straight line of the segment before continues into the segment
202 // that should have been used, and the run has to end where the table is
203 // steep again for the departure to show up in the samples.
205 const ui32 clusteredLutNumPoints = 1024;
206 ui32 clusteredLutPoints[clusteredLutNumPoints];
207 bool clusteredLutReady = false;
208
209 void prepare_clustered_lut()
210 {
211 if (clusteredLutReady)
212 return;
213 const ui32 first = 0x10000000u, last = 0xF0000000u;
214 const ui32 start = 128, num_small = 512; // the entries 128 to 639
215 const ui32 small_gap = 32768; // about a quarter of a step
216 const ui32 large_gap = 8 * small_gap;
217 ui32 run = 0;
218 for (ui32 i = 0; i < num_small; ++i)
219 run += (i & 1) ? large_gap : small_gap;
220 ui32 big_gap = ((last - first) - run) /
221 (clusteredLutNumPoints - 1 - num_small);
222 clusteredLutPoints[0] = first;
223 for (ui32 i = 1; i < clusteredLutNumPoints; ++i)
224 clusteredLutPoints[i] = clusteredLutPoints[i - 1] +
225 ((i >= start && i < start + num_small)
226 ? (((i - start) & 1) ? large_gap : small_gap) : big_gap);
227 clusteredLutReady = true;
228 }
229
231 // An image held in memory. A sample is the 32 bit pattern of an image
232 // sample, which is what a .pfm file holds; for a floating point image these
233 // are the IEEE-754 single precision bit patterns of the samples.
235 struct test_image
236 {
237 ui32 width = 0, height = 0, num_comps = 0;
238 std::vector<std::vector<si32> > samples; // one entry per component
239
240 size_t num_samples() const
241 { return (size_t)width * height * num_comps; }
242 };
243
245 // How well a decoded image agrees with the image it was decoded from
247 struct comparison
248 {
249 long long max_abs_error = 0; // largest error in 32 bit pattern units
250 double mean_abs_error = 0.0;
251 double max_rel_error = 0.0; // largest error relative to the sample
252 double mean_rel_error = 0.0;
253 size_t num_bit_exact = 0; // samples that came back unchanged
254 size_t num_samples = 0;
255
256 bool is_bit_exact() const { return num_bit_exact == num_samples; }
257 };
258
260 // Reads a pfm file. The samples of a pfm file are 32 bit floating point
261 // numbers, and they are handed to the library as the 32 bit patterns they
262 // are stored as; the library works on those patterns.
264 bool load_pfm(const std::string& filename, test_image& img)
265 {
266 FILE* f = fopen(filename.c_str(), "rb");
267 if (f == NULL)
268 return false;
269
270 char magic[3] = { 0, 0, 0 };
271 if (fscanf(f, "%2s", magic) != 1 ||
272 (magic[1] != 'f' && magic[1] != 'F'))
273 {
274 fclose(f);
275 return false;
276 }
277
278 int w = 0, h = 0;
279 float scale = 0.0f;
280 if (fscanf(f, "%d %d %f", &w, &h, &scale) != 3 || w <= 0 || h <= 0)
281 {
282 fclose(f);
283 return false;
284 }
285 fgetc(f); // the single white space that follows the header
286
287 // a positive scale means the file is big endian, a negative one means it
288 // is little endian. The samples are read as 32 bit words, whose byte
289 // order is the machine's, so they have to be swapped whenever the file's
290 // byte order is not the machine's -- the test has to give the same samples
291 // to the library on a little endian and on a big endian machine.
292 const bool file_little_endian = scale < 0.0f;
293 const bool swap = file_little_endian != is_machine_little_endian;
294
295 img.width = (ui32)w;
296 img.height = (ui32)h;
297 img.num_comps = magic[1] == 'f' ? 1 : 3;
298 img.samples.assign(img.num_comps,
299 std::vector<si32>((size_t)w * (size_t)h, 0));
300
301 std::vector<ui32> line((size_t)w * img.num_comps, 0);
302 for (int y = h - 1; y >= 0; --y) // pfm stores the last line first
303 {
304 if (fread(&line[0], sizeof(ui32), (size_t)w * img.num_comps, f)
305 != (size_t)w * img.num_comps)
306 {
307 fclose(f);
308 return false;
309 }
310 for (int x = 0; x < w; ++x)
311 for (ui32 c = 0; c < img.num_comps; ++c)
312 {
313 ui32 u = line[(size_t)x * img.num_comps + c];
314 if (swap)
315 u = (u >> 24) | ((u >> 8) & 0x0000FF00u) |
316 ((u << 8) & 0x00FF0000u) | (u << 24);
317 img.samples[c][(size_t)y * (size_t)w + (size_t)x] = (si32)u;
318 }
319 }
320
321 fclose(f);
322 return true;
323 }
324
326 // Locates one of the images the tests need. The images belong to the
327 // repository that stores the test images, jp2k_test_codestreams. The other
328 // directories looked in are a convenience, so that an image can be tested
329 // before it has been added to that repository. An empty string is returned
330 // when the image cannot be found.
332 std::string find_test_image(const std::string& name)
333 {
334 const char* dirs[] = { REF_FILE_DIR };
335 for (size_t i = 0; i < sizeof(dirs) / sizeof(dirs[0]); ++i)
336 {
337 std::string path = std::string(dirs[i]) + name;
338 FILE* f = fopen(path.c_str(), "rb");
339 if (f != NULL)
340 {
341 fclose(f);
342 return path;
343 }
344 }
345 return std::string();
346 }
347
349 // The two images the tests use: random.pfm is a single component image of
350 // floating point samples that are large and of both signs, and rgb.pfm has
351 // three components.
353 const char* const image_names[2] = { "random.pfm", "rgb.pfm" };
354 const size_t num_images = sizeof(image_names) / sizeof(image_names[0]);
355
357 // Loads both images; returns the names of those that could not be loaded.
359 std::string load_test_images(test_image images[num_images])
360 {
361 std::string missing;
362 for (size_t i = 0; i < num_images; ++i)
363 {
364 std::string path = find_test_image(image_names[i]);
365 if (path.empty() || !load_pfm(path, images[i]))
366 {
367 if (!missing.empty())
368 missing += " and ";
369 missing += image_names[i];
370 }
371 }
372 return missing;
373 }
374
376 // The message used when the test images are not available
378 // std::string missing_images_message(const std::string& missing)
379 // {
380 // return std::string("the test image(s) ") + missing + " could not be "
381 // "found; they belong to the repository that stores the test images, "
382 // "jp2k_test_codestreams, in the folder openjph/references, and the tests "
383 // "that use them are skipped until they are added there";
384 // }
385
387 // Fills an image with a ramp that covers the range of 32 bit patterns.
388 // Samples whose magnitude is very small are left out, because the parts of
389 // the transformation that work with floating point numbers cannot resolve
390 // the samples close to the middle of the range.
392 void make_synthetic_image(test_image& img, ui32 width, ui32 height)
393 {
394 img.width = width;
395 img.height = height;
396 img.num_comps = 1;
397 img.samples.assign(1, std::vector<si32>((size_t)width * height, 0));
398
399 // the limits below sit inside the range the look-up table for .pfm
400 // images covers, so that no sample is clamped by it
401 const si64 lowest = -2070000000LL;
402 const si64 highest = 2070000000LL;
403 const si64 gap = 1LL << 20; // the samples around zero are skipped
404 const si64 half = (si64)((size_t)width * height / 2);
405 std::vector<si32>& s = img.samples[0];
406 for (size_t i = 0; i < s.size(); ++i)
407 {
408 if (i < (size_t)half)
409 s[i] = (si32)(gap + (highest - gap) * (si64)i / (half - 1));
410 else
411 s[i] = (si32)(-gap +
412 (lowest + gap) * (si64)(i - (size_t)half) / (half - 1));
413 }
414 }
415
417 // The nonlinearity a test asks for, together with the coding settings used
418 // with it
420 struct nlt_setting
421 {
422 ui8 type; // 0, 2, 3 or 4
423 bool use_pfm_lut; // true: the look-up table used for .pfm images
424 bool use_narrow_lut = false; // true: the table of narrowLutPoints
425 bool use_bent_lut = false; // true: the table of bentLutPoints
426 bool use_clustered_lut = false; // true: the table of clusteredLutPoints
427 bool use_approx_inverse = false; // true: approx encoding LUT
428 ui8 pt_val = 32; // precision of each LUT point
429 bool reversible; // false: the 9/7 wavelet, true: the 5/3 wavelet
430 float qstep; // the quantization step used with the 9/7 wavelet
431 };
432
434 // The most common setting used below, the 9/7 wavelet with a quantization
435 // step of 1/16384, which is the smallest step ojph_compress uses for .pfm
436 // images.
438 float default_qstep() { return 1.0f / 16384.0f; }
439
441 // Installs the nonlinearity on a codestream that is about to be written
443 void apply_nlt(codestream& cs, const nlt_setting& setting)
444 {
445 param_nlt nlt = cs.access_nlt();
447 {
450 }
451 else if (setting.type == param_nlt::OJPH_NLT_LUT_STYLE_NLT ||
453 {
454 ui32 d_min, d_max, num_points;
455 void* points;
456 if (setting.use_bent_lut)
457 { // indexed over the whole range of 32 bit patterns
458 d_min = 0;
459 d_max = 0xFFFFFFFFu;
460 num_points = bentLutNumPoints;
461 if (setting.pt_val <= 8)
462 points = (void*)bentLutPoints8;
463 else if (setting.pt_val <= 16)
464 points = (void*)bentLutPoints16;
465 else
466 points = (void*)bentLutPoints;
467 }
468 else if (setting.use_clustered_lut)
469 { // indexed over the whole range of 32 bit patterns, with a long run
470 // of almost flat segments among its entries
471 prepare_clustered_lut();
472 d_min = 0;
473 d_max = 0xFFFFFFFFu;
474 num_points = clusteredLutNumPoints;
475 points = (void*)clusteredLutPoints;
476 }
477 else if (setting.use_narrow_lut)
478 { // indexed over the whole range of 32 bit patterns, like the identity
479 // table, but its entries only cover the middle half of it
480 prepare_narrow_lut();
481 d_min = 0;
482 d_max = 0xFFFFFFFFu;
483 num_points = narrowLutNumPoints;
484 points = (void*)narrowLutPoints;
485 }
486 else if (setting.use_pfm_lut)
487 {
488 d_min = pfmLutDmin;
489 d_max = pfmLutDmax;
490 num_points = pfmLutNumPoints;
491 points = (void*)pfmLutPoints;
492 }
493 else
494 { // the identity table runs over the whole range of 32 bit patterns
495 prepare_identity_lut();
496 d_min = 0;
497 d_max = 0xFFFFFFFFu;
498 num_points = identityLutNumPoints;
499 points = (void*)identityLutPoints;
500 }
502 d_min, d_max, setting.pt_val, (ui16)num_points, points, setting.type,
503 setting.use_approx_inverse, 0);
504 }
505 }
506
508 // Encodes an image with the requested nonlinearity in effect
510 void encode_image(const std::string& filename, const test_image& img,
511 const nlt_setting& setting)
512 {
513 codestream cs;
514
515 param_siz siz = cs.access_siz();
516 siz.set_image_extent(point(img.width, img.height));
517 siz.set_num_components(img.num_comps);
518 for (ui32 c = 0; c < img.num_comps; ++c)
519 siz.set_component(c, point(1, 1), 32, true);
520 siz.set_image_offset(point(0, 0));
521 siz.set_tile_size(size(img.width, img.height));
522 siz.set_tile_offset(point(0, 0));
523
524 param_cod cod = cs.access_cod();
526 cod.set_block_dims(64, 64);
527 cod.set_color_transform(false);
528 cod.set_reversible(setting.reversible);
529 if (!setting.reversible)
530 cs.access_qcd().set_irrev_quant(setting.qstep);
531
532 apply_nlt(cs, setting);
533
534 cs.set_planar(true);
535
536 j2c_outfile file;
537 file.open(filename.c_str());
538 cs.write_headers(&file);
539
540 ui32 next_comp;
541 line_buf* cur_line = cs.exchange(NULL, next_comp);
542 ASSERT_TRUE(cur_line != NULL) << filename;
543 for (ui32 c = 0; c < img.num_comps; ++c)
544 for (ui32 y = 0; y < img.height; ++y)
545 {
546 ASSERT_EQ(next_comp, c) << filename;
547 for (ui32 x = 0; x < img.width; ++x)
548 cur_line->i32[x] = img.samples[c][(size_t)y * img.width + x];
549 cur_line = cs.exchange(cur_line, next_comp);
550 }
551
552 cs.flush();
553 cs.close();
554 }
555
557 // Decodes an image; also reports the nonlinearity found in the codestream
559 void decode_image(const std::string& filename, test_image& img,
560 bool* has_nlt, ui8* nlt_type, ui8* nlt_bit_depth,
561 bool* nlt_is_signed)
562 {
563 codestream cs;
564 j2c_infile file;
565 file.open(filename.c_str());
566 cs.read_headers(&file);
567
568 if (has_nlt != NULL || nlt_type != NULL || nlt_bit_depth != NULL ||
569 nlt_is_signed != NULL)
570 {
571 ui8 bit_depth = 0, type = 0;
572 bool is_signed = false;
573 bool found = cs.access_nlt().get_nonlinear_transform(
574 param_nlt::ALL_COMPS, bit_depth, is_signed, type);
575 if (has_nlt != NULL) *has_nlt = found;
576 if (nlt_type != NULL) *nlt_type = type;
577 if (nlt_bit_depth != NULL) *nlt_bit_depth = bit_depth;
578 if (nlt_is_signed != NULL) *nlt_is_signed = is_signed;
579 }
580
581 param_siz siz = cs.access_siz();
582 img.num_comps = siz.get_num_components();
583 img.width = siz.get_recon_width(0);
584 img.height = siz.get_recon_height(0);
585 img.samples.assign(img.num_comps,
586 std::vector<si32>((size_t)img.width * img.height, 0));
587
589 cs.set_planar(true);
590 cs.create();
591
592 for (ui32 c = 0; c < img.num_comps; ++c)
593 for (ui32 y = 0; y < img.height; ++y)
594 {
595 ui32 comp_num = 0;
596 line_buf* line = cs.pull(comp_num);
597 ASSERT_TRUE(line != NULL) << filename;
598 ASSERT_EQ(comp_num, c) << filename;
599 for (ui32 x = 0; x < img.width; ++x)
600 img.samples[c][(size_t)y * img.width + x] = line->i32[x];
601 }
602
603 cs.close();
604 }
605
607 // Compares the image that was encoded with the image that was decoded. The
608 // samples are compared both as the 32 bit patterns they are and as the
609 // floating point numbers those patterns stand for.
611 comparison compare_images(const test_image& before, const test_image& after)
612 {
613 comparison result;
614
615 double peak = 0.0;
616 for (ui32 c = 0; c < before.num_comps; ++c)
617 for (size_t i = 0; i < before.samples[c].size(); ++i)
618 {
619 float v;
620 memcpy(&v, &before.samples[c][i], sizeof(float));
621 peak = std::max(peak, (double)fabs(v));
622 }
623 // a relative error is not meaningful for a sample that is essentially
624 // zero; the floor below keeps such samples from dominating the result
625 const double floor = std::max(peak * 1e-5, 1e-30);
626
627 double sum_abs = 0.0, sum_rel = 0.0;
628 for (ui32 c = 0; c < before.num_comps; ++c)
629 for (size_t i = 0; i < before.samples[c].size(); ++i)
630 {
631 si32 a = before.samples[c][i], b = after.samples[c][i];
632 si64 err = (si64)a - (si64)b;
633 if (err < 0)
634 err = -err;
635 result.max_abs_error = std::max(result.max_abs_error, (long long)err);
636 sum_abs += (double)err;
637
638 float av, bv;
639 memcpy(&av, &a, sizeof(float));
640 memcpy(&bv, &b, sizeof(float));
641 double rel = fabs((double)av - (double)bv) /
642 std::max((double)fabs(av), floor);
643 result.max_rel_error = std::max(result.max_rel_error, rel);
644 sum_rel += rel;
645
646 if (a == b)
647 ++result.num_bit_exact;
648 ++result.num_samples;
649 }
650
651 result.mean_abs_error = sum_abs / (double)result.num_samples;
652 result.mean_rel_error = sum_rel / (double)result.num_samples;
653 return result;
654 }
655
657 // Encodes an image and decodes it again, and reports how well the decoded
658 // image agrees with the image that was encoded
660 comparison round_trip(const std::string& tag, const test_image& img,
661 const nlt_setting& setting)
662 {
663 const std::string filename = std::string(OUT_FILE_DIR) + tag + ".j2c";
664 encode_image(filename, img, setting);
665
666 test_image decoded;
667 decode_image(filename, decoded, NULL, NULL, NULL, NULL);
668
669 EXPECT_EQ(decoded.width, img.width) << tag;
670 EXPECT_EQ(decoded.height, img.height) << tag;
671 EXPECT_EQ(decoded.num_comps, img.num_comps) << tag;
672
673 return compare_images(img, decoded);
674 }
675
677 // Tells whether a codestream holds an NLT marker segment
679 bool has_nlt_marker(const std::string& filename)
680 {
681 FILE* f = fopen(filename.c_str(), "rb");
682 if (f == NULL)
683 return false;
684
685 bool found = false;
686 int prev = EOF, cur;
687 while ((cur = fgetc(f)) != EOF)
688 {
689 if (prev == NLT_MARKER_HIGH && cur == NLT_MARKER_LOW)
690 {
691 found = true;
692 break;
693 }
694 prev = cur;
695 }
696 fclose(f);
697 return found;
698 }
699
701 // Collects the messages the library issues, so that a test can tell the
702 // message it expects apart from an unrelated failure.
704 struct warning_collector : public message_warning
705 {
706 int num_messages = 0;
707 int last_code = 0;
708
709 virtual void operator() (int warn_code, const char* file_name,
710 int line_num, const char* fmt, ...)
711 {
712 (void)file_name;
713 (void)line_num;
714 (void)fmt;
715 ++num_messages;
716 last_code = warn_code;
717 }
718 };
719
720 struct error_collector : public message_error
721 {
722 int num_messages = 0;
723 int last_code = 0;
724
725 virtual void operator() (int error_code, const char* file_name,
726 int line_num, const char* fmt, ...)
727 {
728 (void)file_name;
729 (void)line_num;
730 (void)fmt;
731 ++num_messages;
732 last_code = error_code;
733 throw std::runtime_error("ojph error"); // as the library does
734 }
735 };
736
737 struct message_capture
738 {
739 warning_collector warnings;
740 error_collector errors;
741 message_warning* previous_warning = NULL;
742 message_error* previous_error = NULL;
743
744 message_capture()
745 {
746 previous_warning = get_warning();
747 previous_error = get_error();
748 configure_warning(&warnings);
749 configure_error(&errors);
750 }
751
752 ~message_capture()
753 {
754 configure_warning(previous_warning);
755 configure_error(previous_error);
756 }
757 };
758
760 // Presents a complete codestream to the parser, but reports a short read
761 // when a read crosses readable_size. The requested bytes are still copied
762 // and the cursor is advanced over them: this keeps a parser that ignores
763 // the short count deterministic instead of letting it inspect uninitialized
764 // test-process memory.
766 class short_read_infile : public infile_base
767 {
768 public:
769 short_read_infile(const std::vector<ui8>& data, size_t readable_size)
770 : readable_size(readable_size), short_read(false)
771 {
772 EXPECT_FALSE(data.empty());
773 EXPECT_LE(readable_size, data.size());
774 file.open(&data[0], data.size());
775 }
776
777 size_t read(void* ptr, size_t size) override
778 {
779 si64 start = file.tell();
780 size_t bytes_read = file.read(ptr, size);
781 size_t bytes_to_report = 0;
782 if (start >= 0 && (size_t)start < readable_size)
783 bytes_to_report = std::min(size, readable_size - (size_t)start);
784 bytes_to_report = std::min(bytes_to_report, bytes_read);
785 if (bytes_to_report != size)
786 short_read = true;
787
788 return bytes_to_report;
789 }
790
791 int seek(si64 offset, enum infile_base::seek origin) override
792 { return file.seek(offset, origin); }
793
794 si64 tell() override { return file.tell(); }
795 bool eof() override { return file.eof(); }
796
797 bool saw_short_read() const { return short_read; }
798
799 private:
800 mem_infile file;
801 size_t readable_size;
802 bool short_read;
803 };
804
806 // Reads a codestream into memory, for tests that need to expose only part
807 // of it to the parser.
809 bool load_codestream(const std::string& filename, std::vector<ui8>& data)
810 {
811 FILE* f = fopen(filename.c_str(), "rb");
812 if (f == NULL)
813 return false;
814
815 ui8 buffer[4096];
816 size_t num_read;
817 while ((num_read = fread(buffer, 1, sizeof(buffer), f)) != 0)
818 data.insert(data.end(), buffer, buffer + num_read);
819 bool ok = ferror(f) == 0;
820 fclose(f);
821 return ok;
822 }
823
825 // Returns the byte offset of an NLT marker, or data.size() if absent.
827 size_t find_nlt_marker(const std::vector<ui8>& data)
828 {
829 for (size_t i = 0; i + 1 < data.size(); ++i)
830 if (data[i] == NLT_MARKER_HIGH && data[i + 1] == NLT_MARKER_LOW)
831 return i;
832 return data.size();
833 }
834
836 // Inserts an NLT marker segment for a LUT style nonlinearity into a
837 // codestream, right before its first tile part. The library refuses to
838 // write that combination, so a codestream that carries it has to be put
839 // together here, the way another encoder may have produced it.
841 bool insert_lut_nlt_marker(const std::string& filename, ui8 nlt_type)
842 {
843 FILE* f = fopen(filename.c_str(), "rb");
844 if (f == NULL)
845 return false;
846
847 std::vector<ui8> data;
848 ui8 buffer[4096];
849 size_t num_read;
850 while ((num_read = fread(buffer, 1, sizeof(buffer), f)) > 0)
851 data.insert(data.end(), buffer, buffer + num_read);
852 fclose(f);
853
854 // the marker goes just before the first tile part
855 size_t sot = data.size();
856 for (size_t i = 0; i + 1 < data.size(); ++i)
857 if (data[i] == 0xFF && data[i + 1] == 0x90)
858 {
859 sot = i;
860 break;
861 }
862 if (sot == data.size())
863 return false;
864
865 const ui16 num_points = 17;
866 const ui32 d_min = 0, d_max = 0xFFFFFFFFu;
867 const ui32 length = 17u + 4u * (ui32)num_points; // as the library does
868
869 std::vector<ui8> marker;
870 marker.push_back(0xFF);
871 marker.push_back(0x76); // NLT
872 marker.push_back((ui8)(length >> 8));
873 marker.push_back((ui8)(length & 0xFF));
874 marker.push_back(0xFF);
875 marker.push_back(0xFF); // Cnlt = ALL_COMPS
876 marker.push_back(0x9F); // BDnlt: 32 bits, signed
877 marker.push_back(nlt_type);
878 marker.push_back((ui8)((num_points - 1) >> 8)); // the field carries
879 marker.push_back((ui8)((num_points - 1) & 0xFF)); // one point less
880 for (int i = 3; i >= 0; --i)
881 marker.push_back((ui8)(d_min >> (8 * i)));
882 for (int i = 3; i >= 0; --i)
883 marker.push_back((ui8)(d_max >> (8 * i)));
884 marker.push_back(32); // pt_val
885 for (ui32 i = 0; i < (ui32)num_points; ++i)
886 {
887 ui32 point =
888 (ui32)(((double)i / (num_points - 1)) * 4294967295.0);
889 for (int b = 3; b >= 0; --b)
890 marker.push_back((ui8)(point >> (8 * b)));
891 }
892
893 data.insert(data.begin() + (long)sot, marker.begin(), marker.end());
894
895 f = fopen(filename.c_str(), "wb");
896 if (f == NULL)
897 return false;
898 bool ok = fwrite(&data[0], 1, data.size(), f) == data.size();
899 fclose(f);
900 return ok;
901 }
902
904 // Encodes a two component image in which component 1 is coded with the
905 // irreversible wavelet while component 0 keeps the reversible one; a COC
906 // marker segment is what lets the two differ. The LUT style nonlinearity
907 // is attached to the component the caller names.
909 void encode_two_component_image(const std::string& filename,
910 const test_image& img, ui32 nlt_comp)
911 {
912 codestream cs;
913
914 param_siz siz = cs.access_siz();
915 siz.set_image_extent(point(img.width, img.height));
916 siz.set_num_components(2);
917 for (ui32 c = 0; c < 2; ++c)
918 siz.set_component(c, point(1, 1), 32, true);
919 siz.set_image_offset(point(0, 0));
920 siz.set_tile_size(size(img.width, img.height));
921 siz.set_tile_offset(point(0, 0));
922
923 param_cod cod = cs.access_cod();
925 cod.set_block_dims(64, 64);
926 cod.set_color_transform(false);
927 cod.set_reversible(true); // component 0 stays reversible
928 cod.set_reversible(1, false); // component 1 is irreversible
929 cs.access_qcd().set_irrev_quant(default_qstep());
930
931 cs.access_nlt().set_nonlinear_transform(nlt_comp, 32, true,
932 pfmLutDmin, pfmLutDmax, 32, (ui16)pfmLutNumPoints, (void*)pfmLutPoints,
934
935 cs.set_planar(true);
936
937 j2c_outfile file;
938 file.open(filename.c_str());
939 cs.write_headers(&file);
940
941 ui32 next_comp;
942 line_buf* cur_line = cs.exchange(NULL, next_comp);
943 ASSERT_TRUE(cur_line != NULL) << filename;
944 for (ui32 c = 0; c < 2; ++c)
945 for (ui32 y = 0; y < img.height; ++y)
946 {
947 ASSERT_EQ(next_comp, c) << filename;
948 for (ui32 x = 0; x < img.width; ++x)
949 cur_line->i32[x] = img.samples[0][(size_t)y * img.width + x];
950 cur_line = cs.exchange(cur_line, next_comp);
951 }
952
953 cs.flush();
954 cs.close();
955 }
956
957} // anonymous namespace
958
960// The NLT marker segment has to survive a write and a read of the codestream
961// headers, for each of the three nonlinearities, and the nonlinearity and the
962// format of the samples the nonlinearity works on have to come back unchanged.
964TEST(NltTest, MarkerSegmentRoundTrip)
965{
966 test_image img;
967 make_synthetic_image(img, 64, 64);
968
969 const ui8 types[3] = { param_nlt::OJPH_NLT_LUT_STYLE_NLT,
972 for (size_t i = 0; i < 3; ++i)
973 {
974 nlt_setting setting;
975 setting.type = types[i];
976 setting.use_pfm_lut = true;
977 setting.use_approx_inverse = true;
978 setting.reversible = false;
979 setting.qstep = default_qstep();
980
981 const std::string filename = OUT_FILE_DIR "nlt_marker.j2c";
982 encode_image(filename, img, setting);
983
984 bool has_nlt = false, is_signed = false;
985 ui8 type = 0, bit_depth = 0;
986 test_image decoded;
987 decode_image(filename, decoded, &has_nlt, &type, &bit_depth, &is_signed);
988
989 EXPECT_TRUE(has_nlt) << "nonlinearity " << (int)types[i];
990 EXPECT_EQ((int)type, (int)types[i]) << "nonlinearity " << (int)types[i];
991 EXPECT_EQ((int)bit_depth, 32) << "nonlinearity " << (int)types[i];
992 EXPECT_TRUE(is_signed) << "nonlinearity " << (int)types[i];
993 EXPECT_TRUE(has_nlt_marker(filename)) << "nonlinearity " << (int)types[i];
994 }
995}
996
998// Every short read inside an NLT marker segment must make the codestream be
999// rejected, with one error message coming from the NLT parameter reader.
1000// Test both the fixed-size marker and all three widths of LUT point arrays at
1001// every possible byte boundary.
1003TEST(NltTest, TruncatedMarkerSegmentIsRejected)
1004{
1005 test_image img;
1006 make_synthetic_image(img, 64, 64);
1007
1008 nlt_setting settings[4];
1010 settings[0].use_pfm_lut = false;
1011 settings[0].reversible = false;
1012 settings[0].qstep = default_qstep();
1013
1014 const ui8 precisions[3] = { 8, 16, 32 };
1015 for (size_t i = 1; i < 4; ++i)
1016 {
1017 settings[i].type = param_nlt::OJPH_NLT_LUT_STYLE_NLT;
1018 settings[i].use_pfm_lut = false;
1019 settings[i].use_bent_lut = true;
1020 settings[i].use_approx_inverse = true;
1021 settings[i].pt_val = precisions[i - 1];
1022 settings[i].reversible = false;
1023 settings[i].qstep = default_qstep();
1024 }
1025
1026 const char* tags[4] = { "nlt_truncated_fixed", "nlt_truncated_lut8",
1027 "nlt_truncated_lut16", "nlt_truncated_lut32" };
1028 for (size_t i = 0; i < 4; ++i)
1029 {
1030 const std::string filename =
1031 std::string(OUT_FILE_DIR) + tags[i] + ".j2c";
1032 encode_image(filename, img, settings[i]);
1033
1034 std::vector<ui8> data;
1035 ASSERT_TRUE(load_codestream(filename, data));
1036 remove(filename.c_str());
1037
1038 size_t marker = find_nlt_marker(data);
1039 ASSERT_LT(marker + 3, data.size());
1040 size_t marker_length = ((size_t)data[marker + 2] << 8) |
1041 (size_t)data[marker + 3];
1042 ASSERT_GE(marker_length, (size_t)6);
1043 ASSERT_LE(marker + 2 + marker_length, data.size());
1044
1045 for (size_t available = 0; available < marker_length; ++available)
1046 {
1047 SCOPED_TRACE(testing::Message() << tags[i]
1048 << ", bytes available in marker segment: " << available);
1049
1050 message_capture messages;
1051 short_read_infile file(data, marker + 2 + available);
1052 codestream cs;
1053 EXPECT_THROW(cs.read_headers(&file), std::runtime_error);
1054 EXPECT_TRUE(file.saw_short_read());
1055 EXPECT_EQ(messages.errors.num_messages, 1);
1056 // The reads that belong to one field group are all issued before the
1057 // group is checked, so the message that fires is the one naming the
1058 // group that went short.
1059 EXPECT_TRUE(messages.errors.last_code == 0x000501E1 ||
1060 messages.errors.last_code == 0x000501E2 ||
1061 messages.errors.last_code == 0x000501E3)
1062 << std::hex << "error code " << messages.errors.last_code;
1063 }
1064 }
1065}
1066
1068// A codestream that does not ask for a nonlinearity must not hold an NLT
1069// marker segment, and must not report a nonlinearity when its headers are
1070// read back.
1072TEST(NltTest, NoMarkerSegmentWithoutNlt)
1073{
1074 test_image img;
1075 make_synthetic_image(img, 64, 64);
1076
1077 nlt_setting setting;
1078 setting.type = param_nlt::OJPH_NLT_NO_NLT;
1079 setting.use_pfm_lut = true;
1080 setting.use_approx_inverse = true;
1081 setting.reversible = false;
1082 setting.qstep = default_qstep();
1083
1084 const std::string filename = OUT_FILE_DIR "nlt_none.j2c";
1085 encode_image(filename, img, setting);
1086
1087 bool has_nlt = true;
1088 test_image decoded;
1089 decode_image(filename, decoded, &has_nlt, NULL, NULL, NULL);
1090
1091 EXPECT_FALSE(has_nlt);
1092 EXPECT_FALSE(has_nlt_marker(filename));
1093}
1094
1096// With an identity look-up table the LUT style nonlinearities, 2 and 4, do
1097// not change the samples, so a round trip through either of them has to be as
1098// accurate as a round trip that does not use a nonlinearity. This isolates
1099// the nonlinearity from the accuracy of the table itself: it fails if the
1100// table is not inverted correctly, or if the samples are not exchanged in the
1101// format the nonlinearity expects.
1103TEST(NltTest, LutStyleWithIdentityLutKeepsSamples)
1104{
1105 test_image img;
1106 make_synthetic_image(img, 128, 128);
1107
1108 nlt_setting baseline;
1109 baseline.type = param_nlt::OJPH_NLT_NO_NLT;
1110 baseline.use_pfm_lut = false;
1111 baseline.use_approx_inverse = true;
1112 baseline.reversible = false;
1113 baseline.qstep = 1e-5f;
1114 comparison plain = round_trip("nlt_identity_plain", img, baseline);
1115
1116 const ui8 types[2] = { param_nlt::OJPH_NLT_LUT_STYLE_NLT,
1118 for (size_t i = 0; i < 2; ++i)
1119 {
1120 nlt_setting setting = baseline;
1121 setting.type = types[i];
1122 const std::string tag = "nlt_identity_" + std::to_string((int)types[i]);
1123 comparison nlt = round_trip(tag, img, setting);
1124
1125 // the identity table only adds the rounding of a transformation that does
1126 // nothing, so the average error has to stay in the neighbourhood of the
1127 // error of the round trip that does not use a nonlinearity
1128 EXPECT_LE(nlt.mean_abs_error, 3.0 * plain.mean_abs_error + 16.0)
1129 << "nonlinearity " << (int)types[i] << ": mean absolute error "
1130 << nlt.mean_abs_error << " against " << plain.mean_abs_error
1131 << " for a round trip without a nonlinearity";
1132 EXPECT_LT(nlt.max_rel_error, 0.01)
1133 << "nonlinearity " << (int)types[i] << " with an identity table";
1134 }
1135}
1136
1138// The entries of a look-up table need not span the range the table is indexed
1139// with. The table used here is indexed over the whole range of 32 bit
1140// patterns but its entries only cover the middle half of that range. Samples
1141// inside the range of the entries have to come back from a round trip, in
1142// particular those next to the first and the last entry. This fails if the
1143// encoder maps the ends of the table to the values of the first and last
1144// entries, instead of to the positions at which the table starts and stops.
1146TEST(NltTest, LutWithNarrowRangeOfEntriesKeepsSamples)
1147{
1148 const ui32 width = 128, height = 128;
1149 const size_t count = (size_t)width * height;
1150
1151 // types 2 and 4 index the table with the sample and with the sample after
1152 // it has been converted from binary complement to sign magnitude,
1153 // respectively; that conversion is its own inverse, so it is also how a
1154 // ramp expressed in the domain of the table is turned into samples
1155 const si64 bias = ((si64)1 << 31) + 1;
1156 const ui8 types[2] = { param_nlt::OJPH_NLT_LUT_STYLE_NLT,
1158 for (size_t t = 0; t < 2; ++t)
1159 {
1160 const bool smag = types[t] == param_nlt::OJPH_NLT_BINARY_COMPLEMENT_PLUS_LUT;
1161
1162 // a ramp over the range the entries of the table cover, -2^30 to 2^30
1163 test_image img;
1164 img.width = width;
1165 img.height = height;
1166 img.num_comps = 1;
1167 img.samples.assign(1, std::vector<si32>(count, 0));
1168 for (size_t i = 0; i < count; ++i)
1169 {
1170 si64 u = -((si64)1 << 30) + ((si64)1 << 31) * (si64)i / (si64)(count - 1);
1171 img.samples[0][i] = (si32)((smag && u < 0) ? -u - bias : u);
1172 }
1173
1174 nlt_setting setting;
1175 setting.type = types[t];
1176 setting.use_pfm_lut = false;
1177 setting.use_narrow_lut = true;
1178 setting.use_approx_inverse = true;
1179 setting.reversible = false;
1180 setting.qstep = 1e-5f;
1181
1182 const std::string tag = "nlt_narrow_lut_" + std::to_string((int)types[t]);
1183 const std::string filename = std::string(OUT_FILE_DIR) + tag + ".j2c";
1184 encode_image(filename, img, setting);
1185 test_image decoded;
1186 decode_image(filename, decoded, NULL, NULL, NULL, NULL);
1187 ASSERT_EQ(decoded.samples.size(), 1u) << tag;
1188 ASSERT_EQ(decoded.samples[0].size(), count) << tag;
1189
1190 // compare in the domain of the table, where an error of a given size
1191 // means the same thing everywhere; the tolerance is a very small part
1192 // of the range of the ramp, while the error this test looks for is a
1193 // sizeable part of it
1194 si64 max_error = 0;
1195 size_t worst = 0;
1196 for (size_t i = 0; i < count; ++i)
1197 {
1198 si64 a = img.samples[0][i], b = decoded.samples[0][i];
1199 if (smag && a < 0) a = -a - bias;
1200 if (smag && b < 0) b = -b - bias;
1201 const si64 err = a > b ? a - b : b - a;
1202 if (err > max_error)
1203 {
1204 max_error = err;
1205 worst = i;
1206 }
1207 }
1208 EXPECT_LT(max_error, (si64)1 << 20)
1209 << "nonlinearity " << (int)types[t] << ": sample " << worst
1210 << " of the ramp came back with an error of " << max_error;
1211 }
1212}
1213
1215// Type 3 is a reversible point transformation, so with the reversible (5/3)
1216// wavelet the samples of both test images come back bit for bit.
1218TEST(NltTest, BinaryComplementIsLossless)
1219{
1220 test_image images[num_images];
1221 std::string missing = load_test_images(images);
1222
1223 for (size_t i = 0; i < num_images; ++i)
1224 {
1225 nlt_setting setting;
1227 setting.use_pfm_lut = false;
1228 setting.use_approx_inverse = true;
1229 setting.reversible = true;
1230 setting.qstep = default_qstep();
1231
1232 const std::string tag = std::string("nlt_lossless_") + image_names[i];
1233 comparison result = round_trip(tag, images[i], setting);
1234
1235 EXPECT_TRUE(result.is_bit_exact()) << image_names[i] << ": "
1236 << (result.num_samples - result.num_bit_exact) << " of "
1237 << result.num_samples << " samples changed; the largest change is "
1238 << result.max_abs_error << " in a 32 bit pattern";
1239 }
1240}
1241
1243// Round trips of the test images with each nonlinearity, using the look-up
1244// table that ojph_compress installs for .pfm images. The 9/7 wavelet is
1245// lossy, so the decoded image is compared with a round trip of the same image
1246// that does not use a nonlinearity: a nonlinearity must not add much to the
1247// error of that round trip, and the samples have to come back to within a
1248// fraction of a percent of their value.
1250TEST(NltTest, RoundTripOfTestImages)
1251{
1252 test_image images[num_images];
1253 std::string missing = load_test_images(images);
1254 // if (!missing.empty())
1255 // {
1256 // GTEST_SKIP() << missing_images_message(missing);
1257 // return;
1258 // }
1259
1260 const ui8 types[3] = { param_nlt::OJPH_NLT_LUT_STYLE_NLT,
1263 for (int precise = 0; precise < 2; ++precise)
1264 {
1265 nlt_setting baseline;
1266 baseline.use_approx_inverse = (precise == 0);
1267 for (size_t n = 0; n < num_images; ++n)
1268 {
1269 baseline.type = param_nlt::OJPH_NLT_NO_NLT;
1270 baseline.use_pfm_lut = true;
1271 baseline.reversible = false;
1272 baseline.qstep = default_qstep();
1273 comparison plain = round_trip(std::string("nlt_plain_")
1274 + (precise == 0 ? "approx_" : "precise_") + image_names[n],
1275 images[n], baseline);
1276
1277 for (size_t t = 0; t < 3; ++t)
1278 {
1279 nlt_setting setting = baseline;
1280 setting.type = types[t];
1281 const std::string tag = std::string("nlt_") +
1282 std::to_string((int)types[t]) + "_" + image_names[n];
1283 comparison nlt = round_trip(tag, images[n], setting);
1284
1285 std::cout << image_names[n] << " with nonlinearity " << (int)types[t]
1286 << ": mean absolute error " << nlt.mean_abs_error
1287 << ", mean relative error " << nlt.mean_rel_error
1288 << ", largest relative error " << nlt.max_rel_error
1289 << " (without a nonlinearity the mean absolute error is "
1290 << plain.mean_abs_error << ")" << std::endl;
1291
1292 // type 3 is a reversible transformation of the samples, so it has to
1293 // behave much like no nonlinearity at all
1294 const double factor =
1295 types[t] == param_nlt::OJPH_NLT_BINARY_COMPLEMENT_NLT ? 2.0 : 5.0;
1296
1297 EXPECT_LE(nlt.mean_abs_error, factor * plain.mean_abs_error + 1024.0)
1298 << image_names[n] << " with nonlinearity " << (int)types[t]
1299 << ": mean absolute error " << nlt.mean_abs_error << " against "
1300 << plain.mean_abs_error << " for a round trip without a nonlinearity";
1301 EXPECT_LT(nlt.mean_rel_error, 0.05)
1302 << image_names[n] << " with nonlinearity " << (int)types[t];
1303 EXPECT_LT(nlt.max_rel_error, 0.30)
1304 << image_names[n] << " with nonlinearity " << (int)types[t];
1305 }
1306 }
1307 }
1308}
1309
1311// The inverse of a look-up table is a curve made of straight segments that
1312// change slope at the values of the entries of the table. By default the
1313// encoder samples that curve uniformly and interpolates between the samples,
1314// which departs from it wherever the value of an entry falls between two
1315// samples; that departure does not depend on the quantization step, so it
1316// puts a floor under the error of a round trip, however fine the
1317// quantization is. Asking for the exact inverse removes that floor. The
1318// table used here has one entry that the uniformly sampled table misses.
1320TEST(NltTest, ExactInverseOfLutWithBentCurveKeepsSamples)
1321{
1322 const ui32 width = 128, height = 128;
1323 const size_t count = (size_t)width * height;
1324
1325 // see LutWithNarrowRangeOfEntriesKeepsSamples for how types 2 and 4 index
1326 // the table, and how the ramp is turned into samples
1327 const si64 bias = ((si64)1 << 31) + 1;
1328 const ui8 types[2] = { param_nlt::OJPH_NLT_LUT_STYLE_NLT,
1330 for (size_t t = 0; t < 2; ++t)
1331 {
1332 const bool smag = types[t] == param_nlt::OJPH_NLT_BINARY_COMPLEMENT_PLUS_LUT;
1333
1334 // a ramp over the range the entries of the table cover, -2^30 to 2^30
1335 test_image img;
1336 img.width = width;
1337 img.height = height;
1338 img.num_comps = 1;
1339 img.samples.assign(1, std::vector<si32>(count, 0));
1340 for (size_t i = 0; i < count; ++i)
1341 {
1342 si64 u = -((si64)1 << 30) + ((si64)1 << 31) * (si64)i / (si64)(count - 1);
1343 img.samples[0][i] = (si32)((smag && u < 0) ? -u - bias : u);
1344 }
1345
1346 si64 error[2] = { 0, 0 }; // the largest error of each inverse
1347 for (size_t e = 0; e < 2; ++e)
1348 {
1349 nlt_setting setting;
1350 setting.type = types[t];
1351 setting.use_pfm_lut = false;
1352 setting.use_bent_lut = true;
1353 setting.use_approx_inverse = e == 0;
1354 setting.reversible = false;
1355 setting.qstep = 1e-5f;
1356
1357 const std::string tag = "nlt_bent_lut_" +
1358 std::to_string((int)types[t]) + "_" + std::to_string(e);
1359 const std::string filename = std::string(OUT_FILE_DIR) + tag + ".j2c";
1360 encode_image(filename, img, setting);
1361 test_image decoded;
1362 decode_image(filename, decoded, NULL, NULL, NULL, NULL);
1363 ASSERT_EQ(decoded.samples.size(), 1u) << tag;
1364 ASSERT_EQ(decoded.samples[0].size(), count) << tag;
1365
1366 // compare in the domain of the table, as in the test above
1367 for (size_t i = 0; i < count; ++i)
1368 {
1369 si64 a = img.samples[0][i], b = decoded.samples[0][i];
1370 if (smag && a < 0)
1371 {
1372 a = -a - bias;
1373 }
1374 if (smag && b < 0)
1375 {
1376 b = -b - bias;
1377 }
1378 const si64 err = a > b ? a - b : b - a;
1379 if (err > error[e])
1380 {
1381 error[e] = err;
1382 }
1383 }
1384 }
1385
1386 // the exact inverse leaves only the error of the arithmetic, which is a
1387 // very small part of the range of the ramp; the uniformly sampled inverse
1388 // departs from the curve of the table by a sizeable part of it
1389 EXPECT_LT(error[1], (si64)1 << 20)
1390 << "nonlinearity " << (int)types[t]
1391 << ": with the exact inverse the largest error of the ramp is "
1392 << error[1];
1393 EXPECT_GT(error[0], error[1])
1394 << "nonlinearity " << (int)types[t]
1395 << ": the uniformly sampled inverse is not less accurate than the exact "
1396 << "inverse; errors are " << error[0] << " and " << error[1];
1397 }
1398}
1399
1401// The encoder inverts the look-up table by finding, for each value, the two
1402// entries of the table that enclose it. It knows how far ahead to look: the
1403// largest number of entries that one step of the table it derives from the
1404// supplied one can cross. When the supplied table has a long run of almost
1405// flat segments, that number is more than one, and the search has to be
1406// allowed to reach that many entries ahead of the entry the value is found
1407// at. The table used here has such a run, and the encoder has to search four
1408// entries ahead; the run covers half of the entries of the table, and the
1409// values it spans fall inside the range of values a ramp over the table
1410// covers, so many samples of the ramp are inverted inside the run. Note that
1411// the encoder warns about the flat segments when this table is installed,
1412// which is expected.
1414TEST(NltTest, ExactInverseOfLutWithFlatSegmentsKeepsSamples)
1415{
1416 const ui32 width = 128, height = 128;
1417 const size_t count = (size_t)width * height;
1418
1419 // see LutWithNarrowRangeOfEntriesKeepsSamples for how types 2 and 4 index
1420 // the table, and how the ramp is turned into samples
1421 const si64 bias = ((si64)1 << 31) + 1;
1422 const ui8 types[2] = { param_nlt::OJPH_NLT_LUT_STYLE_NLT,
1424 for (size_t t = 0; t < 2; ++t)
1425 {
1426 const bool smag = types[t] == param_nlt::OJPH_NLT_BINARY_COMPLEMENT_PLUS_LUT;
1427
1428 // a ramp over the range of values the entries of the table cover, from
1429 // 1/16 of the range to 15/16 of it, which covers the run of flat segments
1430 test_image img;
1431 img.width = width;
1432 img.height = height;
1433 img.num_comps = 1;
1434 img.samples.assign(1, std::vector<si32>(count, 0));
1435 for (size_t i = 0; i < count; ++i)
1436 {
1437 si64 u = -((si64)1 << 30) + ((si64)1 << 31) * (si64)i / (si64)(count - 1);
1438 img.samples[0][i] = (si32)((smag && u < 0) ? -u - bias : u);
1439 }
1440
1441 si64 error[2] = { 0, 0 }; // the largest error of each inverse
1442 for (size_t e = 0; e < 2; ++e)
1443 {
1444 nlt_setting setting;
1445 setting.type = types[t];
1446 setting.use_clustered_lut = true;
1447 setting.use_approx_inverse = e == 0;
1448 setting.reversible = false;
1449 setting.qstep = 1e-6f;
1450
1451 const std::string tag = "nlt_clustered_lut_" +
1452 std::to_string((int)types[t]) + "_" + std::to_string(e);
1453 const std::string filename = std::string(OUT_FILE_DIR) + tag + ".j2c";
1454 encode_image(filename, img, setting);
1455 test_image decoded;
1456 decode_image(filename, decoded, NULL, NULL, NULL, NULL);
1457 ASSERT_EQ(decoded.samples.size(), 1u) << tag;
1458 ASSERT_EQ(decoded.samples[0].size(), count) << tag;
1459
1460 // compare in the domain of the table, as in the test above
1461 for (size_t i = 0; i < count; ++i)
1462 {
1463 si64 a = img.samples[0][i], b = decoded.samples[0][i];
1464 if (smag && a < 0)
1465 {
1466 a = -a - bias;
1467 }
1468 if (smag && b < 0)
1469 {
1470 b = -b - bias;
1471 }
1472 const si64 err = a > b ? a - b : b - a;
1473 if (err > error[e])
1474 {
1475 error[e] = err;
1476 }
1477 }
1478 }
1479
1480 // one entry of the table spans about 3.7 million samples of the range of
1481 // its values, and the search inside the run of flat segments has to reach
1482 // four of them; an inverse that looks one entry too few lands on the
1483 // segment before, which is eight times narrower, and at the end of the run,
1484 // where the table is steep again, that departure is worth most of an entry
1485 const si64 entry = ((si64)0xF0000000 - 0x10000000) /
1486 (clusteredLutNumPoints - 1);
1487 EXPECT_LT(error[1], entry / 8)
1488 << "nonlinearity " << (int)types[t]
1489 << ": with the exact inverse the largest error of the ramp is "
1490 << error[1] << ", and one entry of the table is " << entry;
1491 EXPECT_GT(error[0], error[1])
1492 << "nonlinearity " << (int)types[t]
1493 << ": the uniformly sampled inverse is not less accurate than the exact "
1494 << "inverse; errors are " << error[0] << " and " << error[1];
1495 }
1496}
1497
1499// Writing a codestream that combines a LUT style nonlinearity with the
1500// reversible wavelet has to fail: the nonlinearity cannot be applied with that
1501// wavelet, so the codestream would advertise a points transformation that its
1502// samples do not follow.
1504TEST(NltTest, LutStyleIsRejectedWithReversibleWavelet)
1505{
1506 test_image img;
1507 make_synthetic_image(img, 64, 64);
1508
1509 const ui8 types[2] = { param_nlt::OJPH_NLT_LUT_STYLE_NLT,
1511 for (size_t i = 0; i < 2; ++i)
1512 {
1513 nlt_setting setting;
1514 setting.type = types[i];
1515 setting.use_pfm_lut = true;
1516 setting.use_approx_inverse = true;
1517 setting.reversible = true; // the combination that is refused
1518 setting.qstep = default_qstep();
1519
1520 const std::string filename = OUT_FILE_DIR "nlt_rejected.j2c";
1521 message_capture capture;
1522 EXPECT_THROW(encode_image(filename, img, setting), std::runtime_error)
1523 << "nonlinearity " << (int)types[i] << " with the reversible wavelet";
1524 EXPECT_EQ(capture.errors.last_code, 0x000501B1)
1525 << "nonlinearity " << (int)types[i];
1526 }
1527
1528 // the same nonlinearity is accepted with the irreversible wavelet, and the
1529 // binary complement nonlinearity is accepted with either wavelet; those
1530 // combinations are covered by RoundTripOfTestImages and
1531 // BinaryComplementIsLossless
1532 nlt_setting accepted;
1533 accepted.type = param_nlt::OJPH_NLT_LUT_STYLE_NLT;
1534 accepted.use_pfm_lut = true;
1535 accepted.reversible = false;
1536 accepted.qstep = default_qstep();
1537 EXPECT_NO_THROW(encode_image(OUT_FILE_DIR "nlt_accepted.j2c", img, accepted));
1538}
1539
1541// The wavelet can be chosen per component, with a COC marker segment, so the
1542// refusal has to follow the component the nonlinearity is attached to.
1544TEST(NltTest, LutStyleIsRejectedPerComponent)
1545{
1546 test_image img;
1547 make_synthetic_image(img, 64, 64);
1548
1549 // component 1 is coded with the irreversible wavelet, so the nonlinearity is
1550 // accepted there
1551 {
1552 const std::string filename = OUT_FILE_DIR "nlt_component_accepted.j2c";
1553 EXPECT_NO_THROW(encode_two_component_image(filename, img, 1));
1554 test_image decoded;
1555 decode_image(filename, decoded, NULL, NULL, NULL, NULL);
1556 EXPECT_EQ(decoded.num_comps, 2u);
1557 }
1558
1559 // component 0 keeps the reversible wavelet, so the nonlinearity is refused
1560 // there
1561 {
1562 const std::string filename = OUT_FILE_DIR "nlt_component_rejected.j2c";
1563 message_capture capture;
1564 EXPECT_THROW(encode_two_component_image(filename, img, 0),
1565 std::runtime_error);
1566 EXPECT_EQ(capture.errors.last_code, 0x000501B1);
1567 }
1568}
1569
1571// A codestream that carries the combination, which another encoder may have
1572// produced, has to be reported when its headers are read: this library cannot
1573// invert such a nonlinearity, so it says so rather than return samples that
1574// look like image samples.
1576TEST(NltTest, LutStyleWithReversibleWaveletIsReportedWhenRead)
1577{
1578 test_image img;
1579 make_synthetic_image(img, 64, 64);
1580
1581 // a reversible codestream without a nonlinearity, into which an NLT marker
1582 // segment for a LUT style nonlinearity is inserted
1583 nlt_setting setting;
1584 setting.type = param_nlt::OJPH_NLT_NO_NLT;
1585 setting.use_pfm_lut = true;
1586 setting.use_approx_inverse = true;
1587 setting.reversible = true;
1588 setting.qstep = default_qstep();
1589
1590 const std::string filename = OUT_FILE_DIR "nlt_foreign.j2c";
1591 encode_image(filename, img, setting);
1592 ASSERT_TRUE(insert_lut_nlt_marker(filename,
1594
1595 message_capture capture;
1596 codestream cs;
1597 j2c_infile file;
1598 file.open(filename.c_str());
1599 EXPECT_THROW(cs.read_headers(&file), std::runtime_error);
1600 EXPECT_GE(capture.errors.num_messages, 1);
1601 EXPECT_EQ(capture.errors.last_code, 0x00030059);
1602
1603 ui8 bit_depth = 0, type = 0;
1604 bool is_signed = false;
1606 bit_depth, is_signed, type));
1607 EXPECT_EQ((int)type, (int)param_nlt::OJPH_NLT_LUT_STYLE_NLT);
1608 cs.close();
1609}
The object represent a codestream.
param_siz access_siz()
Returns the underlying SIZ marker segment object.
param_cod access_cod()
Returns the underlying COD marker segment object.
void restrict_input_resolution(ui32 skipped_res_for_data, ui32 skipped_res_for_recon)
This function restricts resolution decoding for a codestream. It is for a reading (decoding) codestre...
void close()
Call this function to close the underlying file; works for both encoding and decoding codestreams.
void set_planar(bool planar)
Sets the sequence of pushing or pull rows from the machinery.
line_buf * exchange(line_buf *line, ui32 &next_component)
This call is used to send image data rows to the library. We expect to send one row from a single com...
param_qcd access_qcd()
Returns the underlying QCD marker segment object.
void read_headers(infile_base *file)
This call reads the headers of a codestream. It is for a reading (or decoding) codestream,...
void write_headers(outfile_base *file, const comment_exchange *comments=NULL, ui32 num_comments=0)
Writes codestream headers when the codestream is used for writing. This function should be called aft...
void create()
This call is for a decoding (or reading) codestream. Call this function after calling restrict_input_...
param_nlt access_nlt()
Returns the underlying NLT marker segment object.
void flush()
This is the last call to a writing (encoding) codestream. This will write encoded bitstream data to t...
line_buf * pull(ui32 &comp_num)
This call is to pull one row from the codestream, being decoded. The returned line_buf object holds o...
void open(const char *filename)
void open(const char *filename)
Definition ojph_file.cpp:62
Derived from message_base to handle error messages.
Derived from message_base to handle warning messages.
void set_num_decomposition(ui32 num_decompositions)
void set_block_dims(ui32 width, ui32 height)
void set_color_transform(bool color_transform)
void set_reversible(bool reversible)
non-linearity point transformation object (implements NLT marker segment)
@ OJPH_NLT_BINARY_COMPLEMENT_NLT
@ OJPH_NLT_BINARY_COMPLEMENT_PLUS_LUT
bool get_nonlinear_transform(ui32 comp_num, ui8 &decoded_bit_depth, bool &decoded_signedness, ui8 &nl_type) const
void set_nonlinear_transform(ui32 comp_num, ui8 nl_type)
void set_irrev_quant(float delta)
Set the irreversible quantization base delta.
void set_tile_size(size s)
void set_component(ui32 comp_num, const point &downsampling, ui32 bit_depth, bool is_signed)
void set_num_components(ui32 num_comps)
void set_tile_offset(point offset)
void set_image_offset(point offset)
ui32 get_recon_height(ui32 comp_num) const
void set_image_extent(point extent)
ui32 get_recon_width(ui32 comp_num) const
ui32 get_num_components() const
OJPH_EXPORT message_warning * get_warning()
Get the warning message object, whose operator() member class is called for warning messages – See th...
OJPH_EXPORT void configure_error(message_error *error)
This overrides the default behaviour of handling error messages.
int64_t si64
Definition ojph_defs.h:57
const bool is_machine_little_endian
Definition ojph_arch.h:401
OJPH_EXPORT message_error * get_error()
Get the error message object, whose operator() member class is called for error messages – See the ma...
uint16_t ui16
Definition ojph_defs.h:52
OJPH_EXPORT void configure_warning(message_warning *warn)
This overrides the default behaviour of handling warning messages.
int32_t si32
Definition ojph_defs.h:55
message_error error
uint32_t ui32
Definition ojph_defs.h:54
uint8_t ui8
Definition ojph_defs.h:50
point(ui32 x=0, ui32 y=0)
Definition ojph_base.h:59
size(ui32 w=0, ui32 h=0)
Definition ojph_base.h:49
#define REF_FILE_DIR
#define OUT_FILE_DIR
TEST(TestExecutables, OpenJPHCompressNoArguments)
#define NLT_MARKER_HIGH
Definition test_nlt.cpp:85
#define NLT_MARKER_LOW
Definition test_nlt.cpp:86