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, &params) };
290 	zimgxx::FilterGraph tohdr_graph{ zimgxx::FilterGraph::build(linear_format, rgb_format, &params) };
291 	zimgxx::FilterGraph tosdr_graph{ zimgxx::FilterGraph::build(linear_format, sdr_format, &params) };
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