1 | //////////////////////////////////////////////////////////////////////////////////////// |
2 | // |
3 | // IMPORTANT: READ BEFORE DOWNLOADING, COPYING, INSTALLING OR USING. |
4 | // |
5 | // By downloading, copying, installing or using the software you agree to this license. |
6 | // If you do not agree to this license, do not download, install, |
7 | // copy or use the software. |
8 | // |
9 | // |
10 | // License Agreement |
11 | // For Open Source Computer Vision Library |
12 | // |
13 | // Copyright (C) 2000-2008, Intel Corporation, all rights reserved. |
14 | // Copyright (C) 2009, Willow Garage Inc., all rights reserved. |
15 | // Copyright (C) 2013, OpenCV Foundation, all rights reserved. |
16 | // Third party copyrights are property of their respective owners. |
17 | // |
18 | // Redistribution and use in source and binary forms, with or without modification, |
19 | // are permitted provided that the following conditions are met: |
20 | // |
21 | // * Redistribution's of source code must retain the above copyright notice, |
22 | // this list of conditions and the following disclaimer. |
23 | // |
24 | // * Redistribution's in binary form must reproduce the above copyright notice, |
25 | // this list of conditions and the following disclaimer in the documentation |
26 | // and/or other materials provided with the distribution. |
27 | // |
28 | // * The name of the copyright holders may not be used to endorse or promote products |
29 | // derived from this software without specific prior written permission. |
30 | // |
31 | // This software is provided by the copyright holders and contributors "as is" and |
32 | // any express or implied warranties, including, but not limited to, the implied |
33 | // warranties of merchantability and fitness for a particular purpose are disclaimed. |
34 | // In no event shall the Intel Corporation or contributors be liable for any direct, |
35 | // indirect, incidental, special, exemplary, or consequential damages |
36 | // (including, but not limited to, procurement of substitute goods or services; |
37 | // loss of use, data, or profits; or business interruption) however caused |
38 | // and on any theory of liability, whether in contract, strict liability, |
39 | // or tort (including negligence or otherwise) arising in any way out of |
40 | // the use of this software, even if advised of the possibility of such damage. |
41 | // |
42 | //////////////////////////////////////////////////////////////////////////////////////// |
43 | |
44 | /***************************************************************************************************** |
45 | |
46 | Software for visualising cascade classifier models trained by OpenCV and to get a better |
47 | understanding of the used features. |
48 | |
49 | USAGE: |
50 | ./opencv_visualisation --model=<model.xml> --image=<ref.png> --data=<video output folder> |
51 | |
52 | Created by: Puttemans Steven - April 2016 |
53 | *****************************************************************************************************/ |
54 | |
55 | #include <opencv2/core.hpp> |
56 | #include <opencv2/highgui.hpp> |
57 | #include <opencv2/imgproc.hpp> |
58 | #include <opencv2/imgcodecs.hpp> |
59 | #include <opencv2/videoio.hpp> |
60 | |
61 | #include <fstream> |
62 | #include <iostream> |
63 | #include <sstream> |
64 | |
65 | using namespace std; |
66 | using namespace cv; |
67 | |
68 | struct rect_data{ |
69 | int x; |
70 | int y; |
71 | int w; |
72 | int h; |
73 | float weight; |
74 | }; |
75 | |
76 | static void printLimits(){ |
77 | cerr << "Limits of the current interface:" << endl; |
78 | cerr << " - Only handles cascade classifier models, trained with the opencv_traincascade tool, containing stumps as decision trees [default settings]." << endl; |
79 | cerr << " - The image provided needs to be a sample window with the original model dimensions, passed to the --image parameter." << endl; |
80 | cerr << " - ONLY handles HAAR and LBP features." << endl; |
81 | } |
82 | |
83 | int main( int argc, const char** argv ) |
84 | { |
85 | CommandLineParser parser(argc, argv, |
86 | "{ help h usage ? | | show this message }" |
87 | "{ image i | | (required) path to reference image }" |
88 | "{ model m | | (required) path to cascade xml file }" |
89 | "{ data d | | (optional) path to video output folder }" |
90 | "{ ext | avi | (optional) output video file extension e.g. avi (default) or mp4 }" |
91 | "{ fourcc | XVID | (optional) output video file's 4-character codec e.g. XVID (default) or H264 }" |
92 | "{ fps | 15 | (optional) output video file's frames-per-second rate }" |
93 | ); |
94 | // Read in the input arguments |
95 | if (parser.has(name: "help" )){ |
96 | parser.printMessage(); |
97 | printLimits(); |
98 | return 0; |
99 | } |
100 | string model(parser.get<string>(name: "model" )); |
101 | string output_folder(parser.get<string>(name: "data" )); |
102 | string image_ref = (parser.get<string>(name: "image" )); |
103 | string fourcc = (parser.get<string>(name: "fourcc" )); |
104 | int fps = parser.get<int>(name: "fps" ); |
105 | if (model.empty() || image_ref.empty() || fourcc.size()!=4 || fps<1){ |
106 | parser.printMessage(); |
107 | printLimits(); |
108 | return -1; |
109 | } |
110 | |
111 | // Value for timing |
112 | // You can increase this to have a better visualisation during the generation |
113 | int timing = 1; |
114 | |
115 | // Value for cols of storing elements |
116 | int cols_prefered = 5; |
117 | |
118 | // Open the XML model |
119 | FileStorage fs; |
120 | bool model_ok = fs.open(filename: model, flags: FileStorage::READ); |
121 | if (!model_ok){ |
122 | cerr << "the cascade file '" << model << "' could not be loaded." << endl; |
123 | return -1; |
124 | } |
125 | // Get a the required information |
126 | // First decide which feature type we are using |
127 | FileNode cascade = fs["cascade" ]; |
128 | string feature_type = cascade["featureType" ]; |
129 | bool haar = false, lbp = false; |
130 | if (feature_type.compare(s: "HAAR" ) == 0){ |
131 | haar = true; |
132 | } |
133 | if (feature_type.compare(s: "LBP" ) == 0){ |
134 | lbp = true; |
135 | } |
136 | if ( feature_type.compare(s: "HAAR" ) != 0 && feature_type.compare(s: "LBP" )){ |
137 | cerr << "The model is not an HAAR or LBP feature based model!" << endl; |
138 | cerr << "Please select a model that can be visualized by the software." << endl; |
139 | return -1; |
140 | } |
141 | |
142 | // We make a visualisation mask - which increases the window to make it at least a bit more visible |
143 | int resize_factor = 10; |
144 | int resize_storage_factor = 10; |
145 | Mat reference_image = imread(filename: image_ref, flags: IMREAD_GRAYSCALE ); |
146 | if (reference_image.empty()){ |
147 | cerr << "the reference image '" << image_ref << "'' could not be loaded." << endl; |
148 | return -1; |
149 | } |
150 | Mat visualization; |
151 | resize(src: reference_image, dst: visualization, dsize: Size(reference_image.cols * resize_factor, reference_image.rows * resize_factor), fx: 0, fy: 0, interpolation: INTER_LINEAR_EXACT); |
152 | |
153 | // First recover for each stage the number of weak features and their index |
154 | // Important since it is NOT sequential when using LBP features |
155 | vector< vector<int> > stage_features; |
156 | FileNode stages = cascade["stages" ]; |
157 | FileNodeIterator it_stages = stages.begin(), it_stages_end = stages.end(); |
158 | int idx = 0; |
159 | for( ; it_stages != it_stages_end; it_stages++, idx++ ){ |
160 | vector<int> current_feature_indexes; |
161 | FileNode weak_classifiers = (*it_stages)["weakClassifiers" ]; |
162 | FileNodeIterator it_weak = weak_classifiers.begin(), it_weak_end = weak_classifiers.end(); |
163 | vector<int> values; |
164 | for(int idy = 0; it_weak != it_weak_end; it_weak++, idy++ ){ |
165 | (*it_weak)["internalNodes" ] >> values; |
166 | current_feature_indexes.push_back( x: (int)values[2] ); |
167 | } |
168 | stage_features.push_back(x: current_feature_indexes); |
169 | } |
170 | |
171 | // If the output option has been chosen than we will store a combined image plane for |
172 | // each stage, containing all weak classifiers for that stage. |
173 | bool draw_planes = false; |
174 | stringstream output_video; |
175 | output_video << output_folder << "model_visualization." << parser.get<string>(name: "ext" ); |
176 | VideoWriter result_video; |
177 | if( output_folder.compare(s: "" ) != 0 ){ |
178 | draw_planes = true; |
179 | result_video.open(filename: output_video.str(), fourcc: VideoWriter::fourcc(c1: fourcc[0],c2: fourcc[1],c3: fourcc[2],c4: fourcc[3]), fps, frameSize: visualization.size(), isColor: false); |
180 | if (!result_video.isOpened()){ |
181 | cerr << "the output video '" << output_video.str() << "' could not be opened." |
182 | << " fourcc=" << fourcc |
183 | << " fps=" << fps |
184 | << " frameSize=" << visualization.size() |
185 | << endl; |
186 | return -1; |
187 | } |
188 | } |
189 | |
190 | if(haar){ |
191 | // Grab the corresponding features dimensions and weights |
192 | FileNode features = cascade["features" ]; |
193 | vector< vector< rect_data > > feature_data; |
194 | FileNodeIterator it_features = features.begin(), it_features_end = features.end(); |
195 | for(int idf = 0; it_features != it_features_end; it_features++, idf++ ){ |
196 | vector< rect_data > current_feature_rectangles; |
197 | FileNode rectangles = (*it_features)["rects" ]; |
198 | int nrects = (int)rectangles.size(); |
199 | for(int k = 0; k < nrects; k++){ |
200 | rect_data current_data; |
201 | FileNode single_rect = rectangles[k]; |
202 | current_data.x = (int)single_rect[0]; |
203 | current_data.y = (int)single_rect[1]; |
204 | current_data.w = (int)single_rect[2]; |
205 | current_data.h = (int)single_rect[3]; |
206 | current_data.weight = (float)single_rect[4]; |
207 | current_feature_rectangles.push_back(x: current_data); |
208 | } |
209 | feature_data.push_back(x: current_feature_rectangles); |
210 | } |
211 | |
212 | // Loop over each possible feature on its index, visualise on the mask and wait a bit, |
213 | // then continue to the next feature. |
214 | // If visualisations should be stored then do the in between calculations |
215 | Mat image_plane; |
216 | Mat metadata = Mat::zeros(rows: 150, cols: 1000, CV_8UC1); |
217 | vector< rect_data > current_rects; |
218 | for(int sid = 0; sid < (int)stage_features.size(); sid ++){ |
219 | if(draw_planes){ |
220 | int features_nmbr = (int)stage_features[sid].size(); |
221 | int cols = cols_prefered; |
222 | int rows = features_nmbr / cols; |
223 | if( (features_nmbr % cols) > 0){ |
224 | rows++; |
225 | } |
226 | image_plane = Mat::zeros(rows: reference_image.rows * resize_storage_factor * rows, cols: reference_image.cols * resize_storage_factor * cols, CV_8UC1); |
227 | } |
228 | for(int fid = 0; fid < (int)stage_features[sid].size(); fid++){ |
229 | stringstream meta1, meta2; |
230 | meta1 << "Stage " << sid << " / Feature " << fid; |
231 | meta2 << "Rectangles: " ; |
232 | Mat temp_window = visualization.clone(); |
233 | Mat temp_metadata = metadata.clone(); |
234 | int current_feature_index = stage_features[sid][fid]; |
235 | current_rects = feature_data[current_feature_index]; |
236 | Mat single_feature = reference_image.clone(); |
237 | resize(src: single_feature, dst: single_feature, dsize: Size(), fx: resize_storage_factor, fy: resize_storage_factor, interpolation: INTER_LINEAR_EXACT); |
238 | for(int i = 0; i < (int)current_rects.size(); i++){ |
239 | rect_data local = current_rects[i]; |
240 | if(draw_planes){ |
241 | if(local.weight >= 0){ |
242 | rectangle(img: single_feature, rec: Rect(local.x * resize_storage_factor, local.y * resize_storage_factor, local.w * resize_storage_factor, local.h * resize_storage_factor), color: Scalar(0), thickness: FILLED); |
243 | }else{ |
244 | rectangle(img: single_feature, rec: Rect(local.x * resize_storage_factor, local.y * resize_storage_factor, local.w * resize_storage_factor, local.h * resize_storage_factor), color: Scalar(255), thickness: FILLED); |
245 | } |
246 | } |
247 | Rect part(local.x * resize_factor, local.y * resize_factor, local.w * resize_factor, local.h * resize_factor); |
248 | meta2 << part << " (w " << local.weight << ") " ; |
249 | if(local.weight >= 0){ |
250 | rectangle(img: temp_window, rec: part, color: Scalar(0), thickness: FILLED); |
251 | }else{ |
252 | rectangle(img: temp_window, rec: part, color: Scalar(255), thickness: FILLED); |
253 | } |
254 | } |
255 | imshow(winname: "features" , mat: temp_window); |
256 | putText(img: temp_window, text: meta1.str(), org: Point(15,15), fontFace: FONT_HERSHEY_SIMPLEX, fontScale: 0.5, color: Scalar(255)); |
257 | result_video.write(image: temp_window); |
258 | // Copy the feature image if needed |
259 | if(draw_planes){ |
260 | single_feature.copyTo(m: image_plane(Rect(0 + (fid%cols_prefered)*single_feature.cols, 0 + (fid/cols_prefered) * single_feature.rows, single_feature.cols, single_feature.rows))); |
261 | } |
262 | putText(img: temp_metadata, text: meta1.str(), org: Point(15,15), fontFace: FONT_HERSHEY_SIMPLEX, fontScale: 0.5, color: Scalar(255)); |
263 | putText(img: temp_metadata, text: meta2.str(), org: Point(15,40), fontFace: FONT_HERSHEY_SIMPLEX, fontScale: 0.5, color: Scalar(255)); |
264 | imshow(winname: "metadata" , mat: temp_metadata); |
265 | waitKey(delay: timing); |
266 | } |
267 | //Store the stage image if needed |
268 | if(draw_planes){ |
269 | stringstream save_location; |
270 | save_location << output_folder << "stage_" << sid << ".png" ; |
271 | imwrite(filename: save_location.str(), img: image_plane); |
272 | } |
273 | } |
274 | } |
275 | |
276 | if(lbp){ |
277 | // Grab the corresponding features dimensions and weights |
278 | FileNode features = cascade["features" ]; |
279 | vector<Rect> feature_data; |
280 | FileNodeIterator it_features = features.begin(), it_features_end = features.end(); |
281 | for(int idf = 0; it_features != it_features_end; it_features++, idf++ ){ |
282 | FileNode rectangle = (*it_features)["rect" ]; |
283 | Rect current_feature ((int)rectangle[0], (int)rectangle[1], (int)rectangle[2], (int)rectangle[3]); |
284 | feature_data.push_back(x: current_feature); |
285 | } |
286 | |
287 | // Loop over each possible feature on its index, visualise on the mask and wait a bit, |
288 | // then continue to the next feature. |
289 | Mat image_plane; |
290 | Mat metadata = Mat::zeros(rows: 150, cols: 1000, CV_8UC1); |
291 | for(int sid = 0; sid < (int)stage_features.size(); sid ++){ |
292 | if(draw_planes){ |
293 | int features_nmbr = (int)stage_features[sid].size(); |
294 | int cols = cols_prefered; |
295 | int rows = features_nmbr / cols; |
296 | if( (features_nmbr % cols) > 0){ |
297 | rows++; |
298 | } |
299 | image_plane = Mat::zeros(rows: reference_image.rows * resize_storage_factor * rows, cols: reference_image.cols * resize_storage_factor * cols, CV_8UC1); |
300 | } |
301 | for(int fid = 0; fid < (int)stage_features[sid].size(); fid++){ |
302 | stringstream meta1, meta2; |
303 | meta1 << "Stage " << sid << " / Feature " << fid; |
304 | meta2 << "Rectangle: " ; |
305 | Mat temp_window = visualization.clone(); |
306 | Mat temp_metadata = metadata.clone(); |
307 | int current_feature_index = stage_features[sid][fid]; |
308 | Rect current_rect = feature_data[current_feature_index]; |
309 | Mat single_feature = reference_image.clone(); |
310 | resize(src: single_feature, dst: single_feature, dsize: Size(), fx: resize_storage_factor, fy: resize_storage_factor, interpolation: INTER_LINEAR_EXACT); |
311 | |
312 | // VISUALISATION |
313 | // The rectangle is the top left one of a 3x3 block LBP constructor |
314 | Rect resized(current_rect.x * resize_factor, current_rect.y * resize_factor, current_rect.width * resize_factor, current_rect.height * resize_factor); |
315 | meta2 << resized; |
316 | // Top left |
317 | rectangle(img: temp_window, rec: resized, color: Scalar(255), thickness: 1); |
318 | // Top middle |
319 | rectangle(img: temp_window, rec: Rect(resized.x + resized.width, resized.y, resized.width, resized.height), color: Scalar(255), thickness: 1); |
320 | // Top right |
321 | rectangle(img: temp_window, rec: Rect(resized.x + 2*resized.width, resized.y, resized.width, resized.height), color: Scalar(255), thickness: 1); |
322 | // Middle left |
323 | rectangle(img: temp_window, rec: Rect(resized.x, resized.y + resized.height, resized.width, resized.height), color: Scalar(255), thickness: 1); |
324 | // Middle middle |
325 | rectangle(img: temp_window, rec: Rect(resized.x + resized.width, resized.y + resized.height, resized.width, resized.height), color: Scalar(255), thickness: FILLED); |
326 | // Middle right |
327 | rectangle(img: temp_window, rec: Rect(resized.x + 2*resized.width, resized.y + resized.height, resized.width, resized.height), color: Scalar(255), thickness: 1); |
328 | // Bottom left |
329 | rectangle(img: temp_window, rec: Rect(resized.x, resized.y + 2*resized.height, resized.width, resized.height), color: Scalar(255), thickness: 1); |
330 | // Bottom middle |
331 | rectangle(img: temp_window, rec: Rect(resized.x + resized.width, resized.y + 2*resized.height, resized.width, resized.height), color: Scalar(255), thickness: 1); |
332 | // Bottom right |
333 | rectangle(img: temp_window, rec: Rect(resized.x + 2*resized.width, resized.y + 2*resized.height, resized.width, resized.height), color: Scalar(255), thickness: 1); |
334 | |
335 | if(draw_planes){ |
336 | Rect resized_inner(current_rect.x * resize_storage_factor, current_rect.y * resize_storage_factor, current_rect.width * resize_storage_factor, current_rect.height * resize_storage_factor); |
337 | // Top left |
338 | rectangle(img: single_feature, rec: resized_inner, color: Scalar(255), thickness: 1); |
339 | // Top middle |
340 | rectangle(img: single_feature, rec: Rect(resized_inner.x + resized_inner.width, resized_inner.y, resized_inner.width, resized_inner.height), color: Scalar(255), thickness: 1); |
341 | // Top right |
342 | rectangle(img: single_feature, rec: Rect(resized_inner.x + 2*resized_inner.width, resized_inner.y, resized_inner.width, resized_inner.height), color: Scalar(255), thickness: 1); |
343 | // Middle left |
344 | rectangle(img: single_feature, rec: Rect(resized_inner.x, resized_inner.y + resized_inner.height, resized_inner.width, resized_inner.height), color: Scalar(255), thickness: 1); |
345 | // Middle middle |
346 | rectangle(img: single_feature, rec: Rect(resized_inner.x + resized_inner.width, resized_inner.y + resized_inner.height, resized_inner.width, resized_inner.height), color: Scalar(255), thickness: FILLED); |
347 | // Middle right |
348 | rectangle(img: single_feature, rec: Rect(resized_inner.x + 2*resized_inner.width, resized_inner.y + resized_inner.height, resized_inner.width, resized_inner.height), color: Scalar(255), thickness: 1); |
349 | // Bottom left |
350 | rectangle(img: single_feature, rec: Rect(resized_inner.x, resized_inner.y + 2*resized_inner.height, resized_inner.width, resized_inner.height), color: Scalar(255), thickness: 1); |
351 | // Bottom middle |
352 | rectangle(img: single_feature, rec: Rect(resized_inner.x + resized_inner.width, resized_inner.y + 2*resized_inner.height, resized_inner.width, resized_inner.height), color: Scalar(255), thickness: 1); |
353 | // Bottom right |
354 | rectangle(img: single_feature, rec: Rect(resized_inner.x + 2*resized_inner.width, resized_inner.y + 2*resized_inner.height, resized_inner.width, resized_inner.height), color: Scalar(255), thickness: 1); |
355 | |
356 | single_feature.copyTo(m: image_plane(Rect(0 + (fid%cols_prefered)*single_feature.cols, 0 + (fid/cols_prefered) * single_feature.rows, single_feature.cols, single_feature.rows))); |
357 | } |
358 | |
359 | putText(img: temp_metadata, text: meta1.str(), org: Point(15,15), fontFace: FONT_HERSHEY_SIMPLEX, fontScale: 0.5, color: Scalar(255)); |
360 | putText(img: temp_metadata, text: meta2.str(), org: Point(15,40), fontFace: FONT_HERSHEY_SIMPLEX, fontScale: 0.5, color: Scalar(255)); |
361 | imshow(winname: "metadata" , mat: temp_metadata); |
362 | imshow(winname: "features" , mat: temp_window); |
363 | putText(img: temp_window, text: meta1.str(), org: Point(15,15), fontFace: FONT_HERSHEY_SIMPLEX, fontScale: 0.5, color: Scalar(255)); |
364 | result_video.write(image: temp_window); |
365 | |
366 | waitKey(delay: timing); |
367 | } |
368 | |
369 | //Store the stage image if needed |
370 | if(draw_planes){ |
371 | stringstream save_location; |
372 | save_location << output_folder << "stage_" << sid << ".png" ; |
373 | imwrite(filename: save_location.str(), img: image_plane); |
374 | } |
375 | } |
376 | } |
377 | return 0; |
378 | } |
379 | |