/src/libjxl/lib/extras/enc/npy.cc
Line | Count | Source |
1 | | // Copyright (c) the JPEG XL Project Authors. All rights reserved. |
2 | | // |
3 | | // Use of this source code is governed by a BSD-style |
4 | | // license that can be found in the LICENSE file. |
5 | | |
6 | | #include "lib/extras/enc/npy.h" |
7 | | |
8 | | #include <jxl/codestream_header.h> |
9 | | #include <jxl/types.h> |
10 | | |
11 | | #include <cstddef> |
12 | | #include <cstdint> |
13 | | #include <cstring> |
14 | | #include <memory> |
15 | | #include <ostream> |
16 | | #include <sstream> |
17 | | #include <string> |
18 | | #include <type_traits> |
19 | | #include <utility> |
20 | | #include <vector> |
21 | | |
22 | | #include "lib/extras/enc/encode.h" |
23 | | #include "lib/extras/packed_image.h" |
24 | | #include "lib/jxl/base/common.h" |
25 | | #include "lib/jxl/base/data_parallel.h" |
26 | | #include "lib/jxl/base/status.h" |
27 | | |
28 | | namespace jxl { |
29 | | namespace extras { |
30 | | namespace { |
31 | | |
32 | | // JSON value writing |
33 | | |
34 | | class JSONField { |
35 | | public: |
36 | 0 | virtual ~JSONField() = default; |
37 | | virtual void Write(std::ostream& o, uint32_t indent) const = 0; |
38 | | |
39 | | protected: |
40 | 0 | JSONField() = default; |
41 | | }; |
42 | | |
43 | | class JSONValue : public JSONField { |
44 | | public: |
45 | | template <typename T> |
46 | 0 | explicit JSONValue(const T& value) : value_(std::to_string(value)) {}Unexecuted instantiation: npy.cc:jxl::extras::(anonymous namespace)::JSONValue::JSONValue<float>(float const&) Unexecuted instantiation: npy.cc:jxl::extras::(anonymous namespace)::JSONValue::JSONValue<unsigned int>(unsigned int const&) Unexecuted instantiation: npy.cc:jxl::extras::(anonymous namespace)::JSONValue::JSONValue<int>(int const&) |
47 | | |
48 | 0 | explicit JSONValue(const std::string& value) : value_("\"" + value + "\"") {} |
49 | | |
50 | 0 | explicit JSONValue(bool value) : value_(value ? "true" : "false") {} |
51 | | |
52 | 0 | void Write(std::ostream& o, uint32_t indent) const override { o << value_; } |
53 | | |
54 | | private: |
55 | | std::string value_; |
56 | | }; |
57 | | |
58 | | class JSONDict : public JSONField { |
59 | | public: |
60 | 0 | JSONDict() = default; |
61 | | |
62 | | template <typename T> |
63 | 0 | T* AddEmpty(const std::string& key) { |
64 | 0 | static_assert(std::is_convertible<T*, JSONField*>::value, |
65 | 0 | "T must be a JSONField"); |
66 | 0 | T* ret = new T(); |
67 | 0 | JSONField* field = static_cast<JSONField*>(ret); |
68 | 0 | auto handle = std::unique_ptr<JSONField>(field); |
69 | 0 | values_.emplace_back(key, std::move(handle)); |
70 | 0 | return ret; |
71 | 0 | } Unexecuted instantiation: npy.cc:jxl::extras::(anonymous namespace)::JSONArray* jxl::extras::(anonymous namespace)::JSONDict::AddEmpty<jxl::extras::(anonymous namespace)::JSONArray>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&) Unexecuted instantiation: npy.cc:jxl::extras::(anonymous namespace)::JSONDict* jxl::extras::(anonymous namespace)::JSONDict::AddEmpty<jxl::extras::(anonymous namespace)::JSONDict>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&) |
72 | | |
73 | | template <typename T> |
74 | 0 | void Add(const std::string& key, const T& value) { |
75 | 0 | JSONField* field = static_cast<JSONField*>(new JSONValue(value)); |
76 | 0 | auto handle = std::unique_ptr<JSONField>(field); |
77 | 0 | values_.emplace_back(key, std::move(handle)); |
78 | 0 | } Unexecuted instantiation: npy.cc:void jxl::extras::(anonymous namespace)::JSONDict::Add<jxl::extras::(anonymous namespace)::JSONValue>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, jxl::extras::(anonymous namespace)::JSONValue const&) Unexecuted instantiation: npy.cc:void jxl::extras::(anonymous namespace)::JSONDict::Add<float>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, float const&) Unexecuted instantiation: npy.cc:void jxl::extras::(anonymous namespace)::JSONDict::Add<int>(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, int const&) |
79 | | |
80 | 0 | void Write(std::ostream& o, uint32_t indent) const override { |
81 | 0 | std::string indent_str(indent, ' '); |
82 | 0 | o << "{"; |
83 | 0 | bool is_first = true; |
84 | 0 | for (const auto& key_value : values_) { |
85 | 0 | if (!is_first) { |
86 | 0 | o << ","; |
87 | 0 | } |
88 | 0 | is_first = false; |
89 | 0 | o << "\n" << indent_str << " \"" << key_value.first << "\": "; |
90 | 0 | key_value.second->Write(o, indent + 2); |
91 | 0 | } |
92 | 0 | if (!values_.empty()) { |
93 | 0 | o << "\n" << indent_str; |
94 | 0 | } |
95 | 0 | o << "}"; |
96 | 0 | } |
97 | | |
98 | | private: |
99 | | // Dictionary with order. |
100 | | std::vector<std::pair<std::string, std::unique_ptr<JSONField>>> values_; |
101 | | }; |
102 | | |
103 | | class JSONArray : public JSONField { |
104 | | public: |
105 | 0 | JSONArray() = default; |
106 | | |
107 | | template <typename T> |
108 | 0 | T* AddEmpty() { |
109 | 0 | static_assert(std::is_convertible<T*, JSONField*>::value, |
110 | 0 | "T must be a JSONField"); |
111 | 0 | T* ret = new T(); |
112 | 0 | values_.emplace_back(ret); |
113 | 0 | return ret; |
114 | 0 | } |
115 | | |
116 | | template <typename T> |
117 | 0 | void Add(const T& value) { |
118 | 0 | values_.emplace_back(new JSONValue(value)); |
119 | 0 | } Unexecuted instantiation: npy.cc:void jxl::extras::(anonymous namespace)::JSONArray::Add<unsigned int>(unsigned int const&) Unexecuted instantiation: npy.cc:void jxl::extras::(anonymous namespace)::JSONArray::Add<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&) |
120 | | |
121 | 0 | void Write(std::ostream& o, uint32_t indent) const override { |
122 | 0 | std::string indent_str(indent, ' '); |
123 | 0 | o << "["; |
124 | 0 | bool is_first = true; |
125 | 0 | for (const auto& value : values_) { |
126 | 0 | if (!is_first) { |
127 | 0 | o << ","; |
128 | 0 | } |
129 | 0 | is_first = false; |
130 | 0 | o << "\n" << indent_str << " "; |
131 | 0 | value->Write(o, indent + 2); |
132 | 0 | } |
133 | 0 | if (!values_.empty()) { |
134 | 0 | o << "\n" << indent_str; |
135 | 0 | } |
136 | 0 | o << "]"; |
137 | 0 | } |
138 | | |
139 | | private: |
140 | | std::vector<std::unique_ptr<JSONField>> values_; |
141 | | }; |
142 | | |
143 | 0 | void GenerateMetadata(const PackedPixelFile& ppf, std::vector<uint8_t>* out) { |
144 | 0 | JSONDict meta; |
145 | | // Same order as in 18181-3 CD. |
146 | | |
147 | | // Frames. |
148 | 0 | auto* meta_frames = meta.AddEmpty<JSONArray>("frames"); |
149 | 0 | for (size_t i = 0; i < ppf.frames.size(); i++) { |
150 | 0 | auto* frame_i = meta_frames->AddEmpty<JSONDict>(); |
151 | 0 | if (ppf.info.have_animation) { |
152 | 0 | frame_i->Add("duration", |
153 | 0 | JSONValue(ppf.frames[i].frame_info.duration * 1.0f * |
154 | 0 | ppf.info.animation.tps_denominator / |
155 | 0 | ppf.info.animation.tps_numerator)); |
156 | 0 | } |
157 | |
|
158 | 0 | frame_i->Add("name", JSONValue(ppf.frames[i].name)); |
159 | |
|
160 | 0 | if (ppf.info.animation.have_timecodes) { |
161 | 0 | frame_i->Add("timecode", JSONValue(ppf.frames[i].frame_info.timecode)); |
162 | 0 | } |
163 | 0 | } |
164 | |
|
165 | 0 | #define METADATA(FIELD) meta.Add(#FIELD, ppf.info.FIELD) |
166 | |
|
167 | 0 | METADATA(intensity_target); |
168 | 0 | METADATA(min_nits); |
169 | 0 | METADATA(relative_to_max_display); |
170 | 0 | METADATA(linear_below); |
171 | |
|
172 | 0 | if (ppf.info.have_preview) { |
173 | 0 | meta.AddEmpty<JSONDict>("preview"); |
174 | | // TODO(veluca): can we have duration/name/timecode here? |
175 | 0 | } |
176 | |
|
177 | 0 | { |
178 | 0 | auto* ectype = meta.AddEmpty<JSONArray>("extra_channel_type"); |
179 | 0 | auto* bps = meta.AddEmpty<JSONArray>("bits_per_sample"); |
180 | 0 | auto* ebps = meta.AddEmpty<JSONArray>("exp_bits_per_sample"); |
181 | 0 | bps->Add(ppf.info.bits_per_sample); |
182 | 0 | ebps->Add(ppf.info.exponent_bits_per_sample); |
183 | 0 | for (const auto& eci : ppf.extra_channels_info) { |
184 | 0 | switch (eci.ec_info.type) { |
185 | 0 | case JXL_CHANNEL_ALPHA: { |
186 | 0 | ectype->Add(std::string("Alpha")); |
187 | 0 | break; |
188 | 0 | } |
189 | 0 | case JXL_CHANNEL_DEPTH: { |
190 | 0 | ectype->Add(std::string("Depth")); |
191 | 0 | break; |
192 | 0 | } |
193 | 0 | case JXL_CHANNEL_SPOT_COLOR: { |
194 | 0 | ectype->Add(std::string("SpotColor")); |
195 | 0 | break; |
196 | 0 | } |
197 | 0 | case JXL_CHANNEL_SELECTION_MASK: { |
198 | 0 | ectype->Add(std::string("SelectionMask")); |
199 | 0 | break; |
200 | 0 | } |
201 | 0 | case JXL_CHANNEL_BLACK: { |
202 | 0 | ectype->Add(std::string("Black")); |
203 | 0 | break; |
204 | 0 | } |
205 | 0 | case JXL_CHANNEL_CFA: { |
206 | 0 | ectype->Add(std::string("CFA")); |
207 | 0 | break; |
208 | 0 | } |
209 | 0 | case JXL_CHANNEL_THERMAL: { |
210 | 0 | ectype->Add(std::string("Thermal")); |
211 | 0 | break; |
212 | 0 | } |
213 | 0 | default: { |
214 | 0 | ectype->Add(std::string("UNKNOWN")); |
215 | 0 | break; |
216 | 0 | } |
217 | 0 | } |
218 | 0 | bps->Add(eci.ec_info.bits_per_sample); |
219 | 0 | ebps->Add(eci.ec_info.exponent_bits_per_sample); |
220 | 0 | } |
221 | 0 | } |
222 | | |
223 | 0 | std::ostringstream os; |
224 | 0 | meta.Write(os, 0); |
225 | 0 | out->resize(os.str().size()); |
226 | 0 | memcpy(out->data(), os.str().data(), os.str().size()); |
227 | 0 | } |
228 | | |
229 | 0 | void Append(std::vector<uint8_t>* out, const void* data, size_t size) { |
230 | 0 | size_t pos = out->size(); |
231 | 0 | out->resize(pos + size); |
232 | 0 | memcpy(out->data() + pos, data, size); |
233 | 0 | } |
234 | | |
235 | | void WriteNPYHeader(size_t xsize, size_t ysize, uint32_t num_channels, |
236 | 0 | size_t num_frames, std::vector<uint8_t>* out) { |
237 | 0 | const uint8_t header[] = "\x93NUMPY\x01\x00"; |
238 | 0 | Append(out, header, 8); |
239 | 0 | std::stringstream ss; |
240 | 0 | ss << "{'descr': '<f4', 'fortran_order': False, 'shape': (" << num_frames |
241 | 0 | << ", " << ysize << ", " << xsize << ", " << num_channels << "), }\n"; |
242 | | // 16-bit little endian header length. |
243 | 0 | uint8_t header_len[2] = {static_cast<uint8_t>(ss.str().size() % 256), |
244 | 0 | static_cast<uint8_t>(ss.str().size() / 256)}; |
245 | 0 | Append(out, header_len, 2); |
246 | 0 | Append(out, ss.str().data(), ss.str().size()); |
247 | 0 | } |
248 | | |
249 | | bool WriteFrameToNPYArray(size_t xsize, size_t ysize, const PackedFrame& frame, |
250 | 0 | std::vector<uint8_t>* out) { |
251 | 0 | const auto& color = frame.color; |
252 | 0 | if (color.xsize != xsize || color.ysize != ysize) { |
253 | 0 | return false; |
254 | 0 | } |
255 | 0 | for (const auto& ec : frame.extra_channels) { |
256 | 0 | if (ec.xsize != xsize || ec.ysize != ysize) { |
257 | 0 | return false; |
258 | 0 | } |
259 | 0 | } |
260 | | // interleave the samples from color and extra channels |
261 | 0 | for (size_t y = 0; y < ysize; ++y) { |
262 | 0 | for (size_t x = 0; x < xsize; ++x) { |
263 | 0 | { |
264 | 0 | size_t sample_size = color.pixel_stride(); |
265 | 0 | size_t offset = y * color.stride + x * sample_size; |
266 | 0 | uint8_t* pixels = reinterpret_cast<uint8_t*>(color.pixels()); |
267 | 0 | JXL_ENSURE(offset + sample_size <= color.pixels_size); |
268 | 0 | Append(out, pixels + offset, sample_size); |
269 | 0 | } |
270 | 0 | for (const auto& ec : frame.extra_channels) { |
271 | 0 | size_t sample_size = ec.pixel_stride(); |
272 | 0 | size_t offset = y * ec.stride + x * sample_size; |
273 | 0 | uint8_t* pixels = reinterpret_cast<uint8_t*>(ec.pixels()); |
274 | 0 | JXL_ENSURE(offset + sample_size <= ec.pixels_size); |
275 | 0 | Append(out, pixels + offset, sample_size); |
276 | 0 | } |
277 | 0 | } |
278 | 0 | } |
279 | 0 | return true; |
280 | 0 | } |
281 | | |
282 | | // Writes a PackedPixelFile as a numpy 4D ndarray in binary format. |
283 | 0 | bool WriteNPYArray(const PackedPixelFile& ppf, std::vector<uint8_t>* out) { |
284 | 0 | size_t xsize = ppf.info.xsize; |
285 | 0 | size_t ysize = ppf.info.ysize; |
286 | 0 | WriteNPYHeader(xsize, ysize, |
287 | 0 | ppf.info.num_color_channels + ppf.extra_channels_info.size(), |
288 | 0 | ppf.frames.size(), out); |
289 | 0 | for (const auto& frame : ppf.frames) { |
290 | 0 | if (!WriteFrameToNPYArray(xsize, ysize, frame, out)) { |
291 | 0 | return false; |
292 | 0 | } |
293 | 0 | } |
294 | 0 | return true; |
295 | 0 | } |
296 | | |
297 | | class NumPyEncoder : public Encoder { |
298 | | public: |
299 | | Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, |
300 | 0 | ThreadPool* pool) const override { |
301 | 0 | JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); |
302 | 0 | GenerateMetadata(ppf, &encoded_image->metadata); |
303 | 0 | encoded_image->bitstreams.emplace_back(); |
304 | 0 | if (!WriteNPYArray(ppf, &encoded_image->bitstreams.back())) { |
305 | 0 | return false; |
306 | 0 | } |
307 | 0 | if (ppf.preview_frame) { |
308 | 0 | size_t xsize = ppf.info.preview.xsize; |
309 | 0 | size_t ysize = ppf.info.preview.ysize; |
310 | 0 | WriteNPYHeader(xsize, ysize, ppf.info.num_color_channels, 1, |
311 | 0 | &encoded_image->preview_bitstream); |
312 | 0 | if (!WriteFrameToNPYArray(xsize, ysize, *ppf.preview_frame, |
313 | 0 | &encoded_image->preview_bitstream)) { |
314 | 0 | return false; |
315 | 0 | } |
316 | 0 | } |
317 | 0 | return true; |
318 | 0 | } |
319 | 0 | std::vector<JxlPixelFormat> AcceptedFormats() const override { |
320 | 0 | std::vector<JxlPixelFormat> formats; |
321 | 0 | for (const uint32_t num_channels : {1, 3}) { |
322 | 0 | formats.push_back(JxlPixelFormat{num_channels, JXL_TYPE_FLOAT, |
323 | 0 | JXL_LITTLE_ENDIAN, /*align=*/0}); |
324 | 0 | } |
325 | 0 | return formats; |
326 | 0 | } |
327 | 0 | bool AcceptsCmyk() const override { return true; } |
328 | | }; |
329 | | |
330 | | } // namespace |
331 | | |
332 | 0 | std::unique_ptr<Encoder> GetNumPyEncoder() { |
333 | 0 | return jxl::make_unique<NumPyEncoder>(); |
334 | 0 | } |
335 | | |
336 | | } // namespace extras |
337 | | } // namespace jxl |