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 | |
30 | using namespace glow; |
31 | |
32 | // Check if the header starts with numpy magic string. |
33 | bool checkNumpyMagicHdr(uint8_t *) { |
34 | uint8_t MAGIC[] = {0x93, 0x4E, 0x55, 0x4D, 0x50, 0x59}; |
35 | return !memcmp((uint8_t *)header, MAGIC, 6); |
36 | } |
37 | |
38 | enum class NpyType { I1, U1, I2, U2, I4, U4, I8, U8, F2, F4, F8 }; |
39 | |
40 | struct 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 | |
49 | template <typename T> |
50 | static 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 | |
58 | void 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. |
75 | void 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 | |
183 | static 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 | |
218 | static 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 | |
241 | void 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 | |
264 | void 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 | |
340 | bool 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 | |