1/**
2 * Copyright (c) Glow Contributors. See CONTRIBUTORS file.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#include "glow/Base/Image.h"
18#include "glow/Base/Tensor.h"
19
20#include <fstream>
21#include <map>
22#include <sstream>
23#include <type_traits>
24
25#define NCHW2NHWC \
26 { 0u, 2u, 3u, 1u }
27#define NHWC2NCHW \
28 { 0u, 3u, 1u, 2u }
29
30using namespace glow;
31
32// Check if the header starts with numpy magic string.
33bool checkNumpyMagicHdr(uint8_t *header) {
34 uint8_t MAGIC[] = {0x93, 0x4E, 0x55, 0x4D, 0x50, 0x59};
35 return !memcmp((uint8_t *)header, MAGIC, 6);
36}
37
38enum class NpyType { I1, U1, I2, U2, I4, U4, I8, U8, F2, F4, F8 };
39
40struct NpyData {
41 std::vector<char> data;
42 std::vector<dim_t> shape;
43 NpyType type;
44 size_t nvals;
45 template <typename T> T *getData() { return (T *)data.data(); }
46 size_t elemSize;
47};
48
49template <typename T>
50static void convertNumpyToFloatImpl(NpyData &dataNpy,
51 std::vector<float> &data) {
52 T *databuf = dataNpy.getData<T>();
53 for (size_t i = 0; i < dataNpy.nvals; i++) {
54 data[i] = databuf[i];
55 }
56}
57
58void convertNumpyToFloat(NpyData &dataNpy, std::vector<float> &data) {
59 if (dataNpy.type == NpyType::F4) {
60 convertNumpyToFloatImpl<float>(dataNpy, data);
61 } else if (dataNpy.type == NpyType::U1) {
62 convertNumpyToFloatImpl<uint8_t>(dataNpy, data);
63 } else if (dataNpy.type == NpyType::I1) {
64 convertNumpyToFloatImpl<int8_t>(dataNpy, data);
65 } else if (dataNpy.type == NpyType::I2) {
66 convertNumpyToFloatImpl<int16_t>(dataNpy, data);
67 } else if (dataNpy.type == NpyType::U2) {
68 convertNumpyToFloatImpl<uint16_t>(dataNpy, data);
69 } else {
70 LOG(FATAL) << " Datatype not supported: " << (int)dataNpy.type;
71 }
72}
73
74/// Read npy file \p filename and store tensor info into \p npyData.
75void numpyReader(const std::string &filename, NpyData &npyData) {
76
77 std::map<std::string, NpyType> npyType = {
78 {"i1", NpyType::I1}, {"u1", NpyType::U1}, {"i2", NpyType::I2},
79 {"u2", NpyType::U2}, {"i4", NpyType::I4}, {"u4", NpyType::U4},
80 {"i8", NpyType::I8}, {"u8", NpyType::U8}, {"f2", NpyType::F2},
81 {"f4", NpyType::F4}, {"f8", NpyType::F8}};
82
83 // Wrapper for string find() method, that checks for return value.
84 auto findStr = [](std::string &str, const char *name, size_t *loc = nullptr) {
85 size_t newloc = loc ? str.find(name, *loc) : str.find(name);
86 if (newloc == std::string::npos) {
87 LOG(FATAL) << "NPY loader: Couldn't find string " << name;
88 }
89 return newloc;
90 };
91
92 // Wrapper for string findFirstOf method, that checks for return value.
93 auto findFirstOf = [](std::string &str, const char *find, size_t loc) {
94 loc = str.find_first_of(find, loc);
95 if (loc == std::string::npos) {
96 LOG(FATAL) << "NPY loader: Couldn't find string " << find;
97 }
98 return loc;
99 };
100
101 // temp vars.
102 size_t loc, locend;
103
104 // get file content
105 std::ifstream fs(filename.data(), std::ifstream::binary);
106 CHECK(fs) << "NPY loader: Couldn't open file: " << filename << "\n";
107 fs.seekg(0, fs.end);
108 int len = fs.tellg();
109 CHECK(len > 0) << "NPY loader: File too short: " << filename;
110 fs.seekg(0, fs.beg);
111 char *buffer = new char[len];
112 fs.read(buffer, len);
113 CHECK(fs) << "NPY loader: Reading file failed: " << filename;
114 fs.close();
115
116 // verify that the header contains numpy "magic" string.
117 if (!checkNumpyMagicHdr((uint8_t *)buffer)) {
118 LOG(FATAL) << "NPY loader: Magic number not found";
119 }
120
121 // version
122 uint8_t version = buffer[6];
123 if (version != 1) {
124 LOG(FATAL) << "NPY loader: Version is invalid:" << version;
125 }
126
127 // get header.
128 uint32_t hdrlenLen = version > 1 ? 4 : 2;
129 uint32_t hdrlen =
130 (hdrlenLen == 2) ? *(uint16_t *)&buffer[8] : *(uint32_t *)&buffer[8];
131 std::string hdr(&buffer[8 + hdrlenLen], hdrlen);
132
133 // Find type: Search for little endian identifers, until the end of the value.
134 // Type string is 2 chars following endianess identifer.
135 loc = findStr(hdr, "descr");
136 loc = findStr(hdr, ":", &loc);
137 loc = findFirstOf(hdr, "<|,", loc);
138 if (hdr[loc] != '<' && hdr[loc] != '|') {
139 LOG(FATAL) << "NPY loader: Little-endian supported only got: " << hdr[loc];
140 }
141 std::string typestr = hdr.substr(loc + 1, 2);
142 CHECK(npyType.count(typestr))
143 << "NPY loader: Unknown type: " << typestr << "\n";
144
145 npyData.type = npyType[typestr];
146
147 npyData.elemSize = strtol(&typestr[1], NULL, 10);
148 CHECK(npyData.elemSize > 0 && npyData.elemSize <= 8)
149 << "NPY loader: Element size wrong: " << npyData.elemSize;
150
151 // find fortran_order : [True | False]
152 loc = findStr(hdr, "fortran_order");
153 loc = findStr(hdr, ":", &loc);
154 loc = hdr.find_first_not_of(" '", loc + 1);
155 locend = findFirstOf(hdr, "', ", loc + 1);
156 std::string order = hdr.substr(loc, locend - loc);
157 CHECK_EQ(order, "False") << "NPY loader: fortran_order must be False";
158
159 // find shape
160 loc = findStr(hdr, "shape");
161 loc = findStr(hdr, "(", &loc);
162 locend = findStr(hdr, ")", &loc);
163 std::string shapestr = hdr.substr(loc + 1, locend - loc - 1);
164 std::stringstream ss(shapestr);
165 std::string token;
166 npyData.nvals = 1;
167 while (std::getline(ss, token, ',')) {
168 size_t val = strtol(&token[0], NULL, 10);
169 CHECK(val > 0) << "NPY loader: Element size wrong: " << val;
170 npyData.shape.push_back(val);
171 npyData.nvals *= val;
172 }
173
174 // move file ptr to data
175 int data_pos = 8 + hdrlen + hdrlenLen;
176 char *databuf = &buffer[data_pos];
177
178 npyData.data.resize(npyData.nvals * npyData.elemSize);
179 memcpy(npyData.data.data(), databuf, npyData.nvals * npyData.elemSize);
180 delete[] buffer;
181}
182
183static void normalizeData(ImageLayout imageLayout, llvm::ArrayRef<float> mean,
184 llvm::ArrayRef<float> stddev,
185 ImageNormalizationMode imageNormMode,
186 std::vector<float> &data, std::vector<dim_t> &shape,
187 ImgDataRange pixelRange) {
188 auto inputRange =
189 glow::getPixelValMax(pixelRange) - glow::getPixelValMin(pixelRange);
190 auto range = normModeToRange(imageNormMode, pixelRange);
191
192 float scale = (range.second - range.first) / inputRange;
193 float bias = range.first;
194 float offset = glow::getPixelValMin(pixelRange);
195
196 dim_t numCh = (imageLayout == ImageLayout::Unspecified) ? 1
197 : (imageLayout == ImageLayout::NHWC) ? shape[3]
198 : shape[1];
199 std::vector<float> zeroMean(numCh, 0.f);
200 std::vector<float> oneStd(numCh, 1.f);
201 std::vector<float> meanVal(mean.size() ? mean : llvm::makeArrayRef(zeroMean));
202 std::vector<float> stddevVal(stddev.size() ? stddev
203 : llvm::makeArrayRef(oneStd));
204 CHECK_EQ(numCh, meanVal.size()) << "NPY loader: mean argument size should "
205 "match the number of channels: "
206 << numCh;
207 CHECK_EQ(numCh, stddevVal.size()) << "NPY loader: stddev argument size "
208 "should match the number of channels: "
209 << numCh;
210 size_t chStride = imageLayout == ImageLayout::NCHW ? shape[2] * shape[3] : 1;
211 for (size_t i = 0; i < data.size(); i++) {
212 size_t chIdx = (i / chStride) % numCh;
213 data[i] = (data[i] - meanVal[chIdx]) / stddevVal[chIdx];
214 data[i] = (data[i] - offset) * scale + bias;
215 }
216}
217
218static void setPixelRange(NpyType type, ImgDataRange &pixelRange) {
219 switch (type) {
220 case (NpyType::I1):
221 pixelRange = ImgDataRange::S8;
222 break;
223 case (NpyType::U1):
224 pixelRange = ImgDataRange::U8;
225 break;
226 case (NpyType::I2):
227 pixelRange = ImgDataRange::S16;
228 break;
229 case (NpyType::U2):
230 pixelRange = ImgDataRange::U16;
231 break;
232 case (NpyType::F4):
233 // accept whathever is already set.
234 break;
235 default:
236 LOG(FATAL) << "Wrong image type: " << int(type);
237 break;
238 }
239}
240
241void loadUnspecifiedImageAndPreprocess(
242 const llvm::ArrayRef<std::string> &filenames, Tensor &inputData,
243 ImageNormalizationMode imageNormMode, llvm::ArrayRef<float> mean,
244 llvm::ArrayRef<float> stddev, ImgDataRange &pixelRange) {
245
246 CHECK_EQ(filenames.size(), 1) << "NPY raw image loader: expect single file.";
247 CHECK_LE(mean.size(), 1) << "NPY raw image loader: expect single mean value.";
248 CHECK_LE(stddev.size(), 1)
249 << "NPY raw image loader: expect single stddev value.";
250
251 NpyData dataNpy;
252 numpyReader(filenames[0], dataNpy);
253 std::vector<float> data(dataNpy.nvals);
254
255 convertNumpyToFloat(dataNpy, data);
256 setPixelRange(dataNpy.type, pixelRange);
257 normalizeData(ImageLayout::Unspecified, mean, stddev, imageNormMode, data,
258 dataNpy.shape, pixelRange);
259
260 inputData.reset(ElemKind::FloatTy, dataNpy.shape);
261 inputData.getHandle<>() = data;
262}
263
264void glow::loadNumpyImagesAndPreprocess(
265 const llvm::ArrayRef<std::string> &filenames, Tensor &inputData,
266 ImageNormalizationMode imageNormMode, ImageChannelOrder &imageChannelOrder,
267 ImageLayout imageLayout, ImageLayout inputLayout,
268 llvm::ArrayRef<float> mean, llvm::ArrayRef<float> stddev,
269 ImgDataRange &pixelRange) {
270
271 DCHECK(!filenames.empty())
272 << "NPY loader: There must be at least one filename in filenames.";
273
274 imageChannelOrder = ImageChannelOrder::Unspecified;
275
276 if (imageLayout == ImageLayout::Unspecified) {
277 return loadUnspecifiedImageAndPreprocess(
278 filenames, inputData, imageNormMode, mean, stddev, pixelRange);
279 }
280
281 dim_t numImg = filenames.size();
282
283 // Read each tensor file into a vector of tensors.
284 std::vector<Tensor> tensors(numImg);
285 dim_t batchSize = 0;
286 for (dim_t n = 0; n < numImg; n++) {
287
288 NpyData dataNpy;
289 numpyReader(filenames[n], dataNpy);
290 std::vector<float> data(dataNpy.nvals);
291 convertNumpyToFloat(dataNpy, data);
292 // Expand 3D to 4D. Supporting 4D only.
293 if (dataNpy.shape.size() == 3) {
294 dataNpy.shape.insert(dataNpy.shape.begin(), 1);
295 }
296 CHECK_EQ(dataNpy.shape.size(), 4)
297 << "NPY loader: Supporting only 3 or 4 dimensions.";
298 normalizeData(imageLayout, mean, stddev, imageNormMode, data, dataNpy.shape,
299 pixelRange);
300 // Load tensor from the vector obtained from the npy loader.
301 tensors[n].reset(glow::ElemKind::FloatTy, dataNpy.shape);
302 tensors[n].getHandle<>() = data;
303 auto dims0 = tensors[0].dims();
304 auto dims = tensors[n].dims();
305 if (n > 0) {
306 CHECK_EQ(dims0.size(), dims.size())
307 << "NPY loader: Number of dimensions must match.";
308 for (dim_t i = 1; i < dims.size(); i++) {
309 CHECK_EQ(dims0[i], dims[i])
310 << "NPY loader: Non-batch dimensions must match.";
311 }
312 }
313 // Accumulate batch dimension after each dump is loaded.
314 batchSize += dims[0];
315 }
316 // Input tensor dimensions are now fully known.
317 inputData.reset(ElemKind::FloatTy,
318 {batchSize, tensors[0].dims()[1], tensors[0].dims()[2],
319 tensors[0].dims()[3]});
320 auto IIDH = inputData.getHandle<>();
321 // Insert each loaded file (in tensors[] tensors) as the input tensor slices.
322 for (dim_t n = 0, batch = 0, e = tensors.size(); n < e; n++) {
323 Handle<float> H = tensors[n].getHandle<>();
324 IIDH.insertTensors(H, {batch, 0, 0, 0});
325 batch += tensors[n].dims()[0];
326 }
327
328 // Convert to the requested layout.
329 if (inputLayout != imageLayout && inputLayout != ImageLayout::Unspecified) {
330 glow::Tensor transposed;
331 if (imageLayout == ImageLayout::NCHW) {
332 inputData.transpose(&transposed, NHWC2NCHW);
333 } else {
334 inputData.transpose(&transposed, NCHW2NHWC);
335 }
336 inputData = std::move(transposed);
337 }
338}
339
340bool glow::isNumpyNpyFormat(const std::string &filename) {
341 std::ifstream fs(filename.data(), std::ifstream::binary);
342 CHECK(fs) << "NPY loader: Couldn't open file: " << filename << "\n";
343 uint8_t prefix[8];
344 fs.read((char *)prefix, 8);
345 CHECK(fs) << "NPY loader: Reading file failed: " << filename;
346 // check if the header contains numpy magic string.
347 return checkNumpyMagicHdr(prefix);
348}
349