1 // z.lib example code for HDR API.
2 //
3 // Example code demonstrates the use of z.lib to operate on HDR images. In the
4 // example, an HDR10 image is decomposed into SDR and HDR components.
5
6 #include <algorithm>
7 #include <cmath>
8 #include <cstddef>
9 #include <cstdint>
10 #include <cstring>
11 #include <iostream>
12 #include <memory>
13 #include <stdexcept>
14 #include <string>
15 #include <system_error>
16
17 #include <zimg++.hpp>
18 #if ZIMG_API_VERSION < ZIMG_MAKE_API_VERSION(2, 2)
19 #error API 2.2 required for HDR
20 #endif
21
22 #include "aligned_malloc.h"
23 #include "argparse.h"
24 #include "mmap.h"
25 #include "win32_bitmap.h"
26
27 namespace {
28
decode_mask_key(const struct ArgparseOption *,void * out,const char * param,int)29 int decode_mask_key(const struct ArgparseOption *, void *out, const char *param, int)
30 {
31 const char HEX_DIGITS[] = "0123456789abcdefABCDEF";
32
33 uint8_t *mask_key = static_cast<uint8_t *>(out);
34
35 try {
36 std::string s{ param };
37 if (s.size() != 6 || s.find_first_not_of(HEX_DIGITS) != std::string::npos)
38 throw std::runtime_error{ "bad hex string" };
39
40 mask_key[0] = static_cast<uint8_t>(std::stoi(s.substr(0, 2), nullptr, 16));
41 mask_key[1] = static_cast<uint8_t>(std::stoi(s.substr(2, 2), nullptr, 16));
42 mask_key[2] = static_cast<uint8_t>(std::stoi(s.substr(4, 2), nullptr, 16));
43 } catch (const std::exception &e) {
44 std::cerr << e.what() << '\n';
45 return -1;
46 }
47
48 return 0;
49 }
50
51
52 struct Arguments {
53 const char *inpath;
54 const char *sdrpath;
55 const char *hdrpath;
56 unsigned width;
57 unsigned height;
58 double luminance;
59 uint8_t mask_key[3];
60 char fast;
61 };
62
63 const ArgparseOption program_switches[] = {
64 { OPTION_FLAG, "f", "fast", offsetof(Arguments, fast), nullptr, "use fast gamma functions" },
65 { OPTION_FLOAT, "l", "luminance", offsetof(Arguments, luminance), nullptr, "legacy peak brightness (cd/m^2)" },
66 { OPTION_USER1, "k", "key", offsetof(Arguments, mask_key), decode_mask_key, "HDR color key (RRGGBB hex string)" },
67 { OPTION_STRING, "m", "mask", offsetof(Arguments, hdrpath), nullptr, "HDR difference mask" },
68 { OPTION_NULL }
69 };
70
71 const ArgparseOption program_positional[] = {
72 { OPTION_STRING, nullptr, "inpath", offsetof(Arguments, inpath), nullptr, "input path" },
73 { OPTION_STRING, nullptr, "outpath", offsetof(Arguments, sdrpath), nullptr, "output path" },
74 { OPTION_UINT, "w", "width", offsetof(Arguments, width), nullptr, "image width" },
75 { OPTION_UINT, "h", "height", offsetof(Arguments, height), nullptr, "image height" },
76 { OPTION_NULL }
77 };
78
79 const ArgparseCommandLine program_def = {
80 program_switches,
81 program_positional,
82 "hdr_example",
83 "show legacy and HDR portion of HDR10 images",
84 "Input must be HDR10 (YUV 4:2:0, 10 bpc), SDR output is BMP, HDR output is planar HDR10 RGB"
85 };
86
87
88 struct ImageBuffer {
89 std::shared_ptr<void> handle;
90 zimgxx::zimage_buffer buffer;
91 };
92
align(size_t n)93 size_t align(size_t n)
94 {
95 return (n + 31) & ~31;
96 }
97
allocate_buffer(unsigned width,unsigned height,unsigned subsample_w,unsigned subsample_h,size_t bytes_per_pel)98 ImageBuffer allocate_buffer(unsigned width, unsigned height, unsigned subsample_w, unsigned subsample_h, size_t bytes_per_pel)
99 {
100 ImageBuffer buf;
101
102 buf.buffer.stride(0) = align(width * bytes_per_pel);
103 buf.buffer.stride(1) = align((width >> subsample_h) * bytes_per_pel);
104 buf.buffer.stride(2) = align((width >> subsample_w) * bytes_per_pel);
105 buf.buffer.mask(0) = ZIMG_BUFFER_MAX;
106 buf.buffer.mask(1) = ZIMG_BUFFER_MAX;
107 buf.buffer.mask(2) = ZIMG_BUFFER_MAX;
108
109 size_t buffer_size = buf.buffer.stride(0) * height + 2 * buf.buffer.stride(1) * (height >> subsample_h);
110 buf.handle = std::shared_ptr<void>(aligned_malloc(buffer_size, 32), aligned_free);
111
112 uint8_t *ptr = static_cast<uint8_t *>(buf.handle.get());
113 buf.buffer.data(0) = ptr;
114 buf.buffer.data(1) = ptr + buf.buffer.stride(0) * height;
115 buf.buffer.data(2) = ptr + buf.buffer.stride(0) * height + buf.buffer.stride(1) * (height >> subsample_h);
116
117 return buf;
118 }
119
read_from_file(const char * path,unsigned width,unsigned height,unsigned subsample_w,unsigned subsample_h,size_t bytes_per_pel)120 ImageBuffer read_from_file(const char *path, unsigned width, unsigned height, unsigned subsample_w, unsigned subsample_h, size_t bytes_per_pel)
121 {
122 ImageBuffer buf = allocate_buffer(width, height, subsample_w, subsample_h, bytes_per_pel);
123 MemoryMappedFile mmap{ path, MemoryMappedFile::READ_TAG };
124
125 size_t file_size = bytes_per_pel * width * height + 2 * bytes_per_pel * (width >> subsample_w) * (height >> subsample_h);
126 if (mmap.size() != file_size)
127 throw std::runtime_error{ "bad file size" };
128
129 const uint8_t *src_p = static_cast<const uint8_t *>(mmap.read_ptr());
130
131 for (unsigned p = 0; p < 3; ++p) {
132 size_t rowsize = (width >> (p ? subsample_w : 0)) * bytes_per_pel;
133
134 for (unsigned i = 0; i < height >> (p ? subsample_h : 0); ++i) {
135 memcpy(buf.buffer.line_at(i, p), src_p, rowsize);
136 src_p += rowsize;
137 }
138 }
139
140 return buf;
141 }
142
write_to_file(const ImageBuffer & buf,const char * path,unsigned width,unsigned height,unsigned subsample_w,unsigned subsample_h,size_t bytes_per_pel)143 void write_to_file(const ImageBuffer &buf, const char *path, unsigned width, unsigned height, unsigned subsample_w, unsigned subsample_h, size_t bytes_per_pel)
144 {
145 size_t file_size = bytes_per_pel * width * height + 2 * bytes_per_pel * (width >> subsample_w) * (height >> subsample_h);
146 MemoryMappedFile mmap{ path, file_size, MemoryMappedFile::CREATE_TAG };
147
148 uint8_t *dst_p = static_cast<uint8_t *>(mmap.write_ptr());
149
150 for (unsigned p = 0; p < 3; ++p) {
151 size_t rowsize = (width >> (p ? subsample_w : 0)) * bytes_per_pel;
152
153 for (unsigned i = 0; i < height >> (p ? subsample_h : 0); ++i) {
154 memcpy(dst_p, buf.buffer.line_at(i, p), rowsize);
155 dst_p += rowsize;
156 }
157 }
158 }
159
write_to_bmp(const ImageBuffer & buf,const char * path,unsigned width,unsigned height)160 void write_to_bmp(const ImageBuffer &buf, const char *path, unsigned width, unsigned height)
161 {
162 WindowsBitmap bmp{ path, static_cast<int>(width), static_cast<int>(height), 24 };
163 unsigned char *dst_p = bmp.write_ptr();
164
165 for (unsigned i = 0; i < height; ++i) {
166 const uint8_t *src_r = static_cast<const uint8_t *>(buf.buffer.line_at(i, 0));
167 const uint8_t *src_g = static_cast<const uint8_t *>(buf.buffer.line_at(i, 1));
168 const uint8_t *src_b = static_cast<const uint8_t *>(buf.buffer.line_at(i, 2));
169
170 for (unsigned j = 0; j < width; ++j) {
171 dst_p[j * 3 + 0] = src_b[j];
172 dst_p[j * 3 + 1] = src_g[j];
173 dst_p[j * 3 + 2] = src_r[j];
174 }
175
176 dst_p += bmp.stride();
177 }
178 }
179
undo_gamma(float x)180 float undo_gamma(float x)
181 {
182 if (x < 4.5f * 0.018053968510807f)
183 x = x / 4.5f;
184 else
185 x = std::pow((x + (1.09929682680944f - 1.0f)) / 1.09929682680944f, 1.0f / 0.45f);
186
187 return x;
188 }
189
mask_pixels(const zimgxx::zimage_buffer & src_buf,const zimgxx::zimage_buffer & mask_buf,unsigned width,unsigned height,const uint8_t * mask_val)190 void mask_pixels(const zimgxx::zimage_buffer& src_buf, const zimgxx::zimage_buffer& mask_buf, unsigned width, unsigned height, const uint8_t *mask_val)
191 {
192 float r_mask = undo_gamma(mask_val[0] / 255.0f);
193 float g_mask = undo_gamma(mask_val[1] / 255.0f);
194 float b_mask = undo_gamma(mask_val[2] / 255.0f);
195
196 for (unsigned i = 0; i < height; ++i) {
197 float *src_r = static_cast<float *>(src_buf.line_at(i, 0));
198 float *src_g = static_cast<float *>(src_buf.line_at(i, 1));
199 float *src_b = static_cast<float *>(src_buf.line_at(i, 2));
200
201 float *dst_r = static_cast<float *>(mask_buf.line_at(i, 0));
202 float *dst_g = static_cast<float *>(mask_buf.line_at(i, 1));
203 float *dst_b = static_cast<float *>(mask_buf.line_at(i, 2));
204
205 for (unsigned j = 0; j < width; ++j) {
206 float r = src_r[j];
207 float g = src_g[j];
208 float b = src_b[j];
209
210 if (r < 0.0f || g < 0.0f || b < 0.0f || r > 1.0f || g > 1.0f || b > 1.0f) {
211 src_r[j] = r_mask;
212 src_g[j] = g_mask;
213 src_b[j] = b_mask;
214
215 dst_r[j] = r;
216 dst_g[j] = g;
217 dst_b[j] = b;
218 } else {
219 dst_r[j] = 0.0f;
220 dst_g[j] = 0.0f;
221 dst_b[j] = 0.0f;
222 }
223 }
224 }
225 }
226
execute(const Arguments & args)227 void execute(const Arguments &args)
228 {
229 ImageBuffer in = read_from_file(args.inpath, args.width, args.height, 1, 1, sizeof(uint16_t));
230 ImageBuffer linear = allocate_buffer(args.width, args.height, 0, 0, sizeof(float));
231 ImageBuffer linear_mask = allocate_buffer(args.width, args.height, 0, 0, sizeof(float));
232 ImageBuffer sdr = allocate_buffer(args.width, args.height, 0, 0, sizeof(uint8_t));
233 ImageBuffer mask = allocate_buffer(args.width, args.height, 0, 0, sizeof(uint16_t));
234
235 // If allow_approximate_gamma is set, out-of-range pixels may be clipped,
236 // which could interfere with further processing of image highlights.
237 zimgxx::zfilter_graph_builder_params params;
238 params.nominal_peak_luminance = args.luminance;
239 params.allow_approximate_gamma = !!args.fast;
240
241 // HDR10 specification.
242 zimgxx::zimage_format src_format;
243 src_format.width = args.width;
244 src_format.height = args.height;
245 src_format.pixel_type = ZIMG_PIXEL_WORD;
246 src_format.subsample_w = 1;
247 src_format.subsample_h = 1;
248 src_format.color_family = ZIMG_COLOR_YUV;
249 src_format.matrix_coefficients = ZIMG_MATRIX_BT2020_NCL;
250 src_format.transfer_characteristics = ZIMG_TRANSFER_ST2084;
251 src_format.color_primaries = ZIMG_PRIMARIES_BT2020;
252 src_format.depth = 10;
253 src_format.pixel_range = ZIMG_RANGE_LIMITED;
254
255 // Linear Rec.709 RGB corresponding to above.
256 zimgxx::zimage_format linear_format;
257 linear_format.width = args.width;
258 linear_format.height = args.height;
259 linear_format.pixel_type = ZIMG_PIXEL_FLOAT;
260 linear_format.color_family = ZIMG_COLOR_RGB;
261 linear_format.matrix_coefficients = ZIMG_MATRIX_RGB;
262 linear_format.transfer_characteristics = ZIMG_TRANSFER_LINEAR;
263 linear_format.color_primaries = ZIMG_PRIMARIES_BT709;
264
265 // HDR10 RGB corresponding to above.
266 zimgxx::zimage_format rgb_format;
267 rgb_format.width = args.width;
268 rgb_format.height = args.height;
269 rgb_format.pixel_type = ZIMG_PIXEL_WORD;
270 rgb_format.color_family = ZIMG_COLOR_RGB;
271 rgb_format.matrix_coefficients = ZIMG_MATRIX_RGB;
272 rgb_format.transfer_characteristics = ZIMG_TRANSFER_ST2084;
273 rgb_format.color_primaries = ZIMG_PRIMARIES_BT2020;
274 rgb_format.depth = 10;
275 rgb_format.pixel_range = ZIMG_RANGE_FULL;
276
277 // Rec.709 RGB corresponding to above.
278 zimgxx::zimage_format sdr_format;
279 sdr_format.width = args.width;
280 sdr_format.height = args.height;
281 sdr_format.pixel_type = ZIMG_PIXEL_BYTE;
282 sdr_format.color_family = ZIMG_COLOR_RGB;
283 sdr_format.matrix_coefficients = ZIMG_MATRIX_RGB;
284 sdr_format.transfer_characteristics = ZIMG_TRANSFER_BT709;
285 sdr_format.color_primaries = ZIMG_PRIMARIES_BT709;
286 sdr_format.depth = 8;
287 sdr_format.pixel_range = ZIMG_RANGE_FULL;
288
289 zimgxx::FilterGraph tolinear_graph{ zimgxx::FilterGraph::build(src_format, linear_format, ¶ms) };
290 zimgxx::FilterGraph tohdr_graph{ zimgxx::FilterGraph::build(linear_format, rgb_format, ¶ms) };
291 zimgxx::FilterGraph tosdr_graph{ zimgxx::FilterGraph::build(linear_format, sdr_format, ¶ms) };
292
293 size_t tmp_size = std::max({ tolinear_graph.get_tmp_size(), tohdr_graph.get_tmp_size(), tosdr_graph.get_tmp_size() });
294 std::shared_ptr<void> tmp_buf{ aligned_malloc(tmp_size, 32), aligned_free };
295
296 // Convert from HDR10 to linear Rec.709.
297 tolinear_graph.process(in.buffer.as_const(), linear.buffer, tmp_buf.get());
298
299 // Search for out of range pixels and replace with color key.
300 mask_pixels(linear.buffer, linear_mask.buffer, args.width, args.height, args.mask_key);
301
302 // Convert linear image to Rec.709 for export.
303 tosdr_graph.process(linear.buffer.as_const(), sdr.buffer, tmp_buf.get());
304
305 // Convert mask image to HDR10 RGB for export.
306 tohdr_graph.process(linear_mask.buffer.as_const(), mask.buffer, tmp_buf.get());
307
308 write_to_bmp(sdr, args.sdrpath, args.width, args.height);
309 if (args.hdrpath)
310 write_to_file(mask, args.hdrpath, args.width, args.height, 0, 0, sizeof(uint16_t));
311 }
312
313 } // namespace
314
315
main(int argc,char ** argv)316 int main(int argc, char **argv)
317 {
318 Arguments args{};
319 int ret;
320
321 args.luminance = NAN;
322
323 if ((ret = argparse_parse(&program_def, &args, argc, argv)) < 0)
324 return ret == ARGPARSE_HELP_MESSAGE ? 0 : ret;
325
326 try {
327 execute(args);
328 } catch (const std::system_error &e) {
329 std::cerr << "system_error " << e.code() << ": " << e.what() << '\n';
330 return 2;
331 } catch (const zimgxx::zerror &e) {
332 std::cerr << "zimg error " << e.code << ": " << e.msg << '\n';
333 return 2;
334 } catch (const std::runtime_error &e) {
335 std::cerr << "runtime_error: " << e.what() << '\n';
336 return 2;
337 } catch (const std::logic_error &e) {
338 std::cerr << "logic_error: " << e.what() << '\n';
339 return 2;
340 }
341
342 return 0;
343 }
344