├── LICENSE ├── README.md └── inversehaar.py /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 matthewearl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Haar Cascade Inverter 2 | ===================== 3 | 4 | Script to invert an OpenCV Haar cascade. For example, when passed a cascade 5 | file that would normally detect human faces in an image, it will instead 6 | generate an image of a human face. 7 | 8 | See [this blog post](http://matthewearl.github.io/2016/01/14/inverse-haar/) for 9 | an overview of how the code works. 10 | 11 | -------------------------------------------------------------------------------- /inversehaar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2015 Matthew Earl 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included 12 | # in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17 | # NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20 | # USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | 23 | """ 24 | Invert OpenCV haar cascades. 25 | 26 | See http://matthewearl.github.io/2016/01/14/inverse-haar/ for an overview of 27 | how the code works. 28 | 29 | """ 30 | 31 | 32 | __all__ = ( 33 | 'Cascade', 34 | 'inverse_haar', 35 | ) 36 | 37 | 38 | import collections 39 | import sys 40 | import xml.etree.ElementTree 41 | 42 | import cv2 43 | import numpy 44 | 45 | from docplex.mp.context import DOcloudContext 46 | from docplex.mp.environment import Environment 47 | from docplex.mp.model import Model 48 | 49 | 50 | # Constants 51 | 52 | # URL to connect to DoCloud with. This may need changing to your particular 53 | # URL. 54 | DOCLOUD_URL = 'https://api-oaas.docloud.ibmcloud.com/job_manager/rest/v1/' 55 | 56 | # OpenCV preprocesses analysed regions by dividing by the standard deviation. 57 | # Unfortunately this step cannot be modelled with LP constraints, so we just 58 | # allow a reasonably high pixel value. This value should be at least 2, seeing 59 | # as the maximum standard deviation of a set of values between 0 and 1 is 0.5. 60 | MAX_PIXEL_VALUE = 2.0 61 | 62 | 63 | # Grid classes 64 | 65 | class Grid(object): 66 | """ 67 | A division of an image area into cells. 68 | 69 | For example, `SquareGrid` divides the image into pixels. 70 | 71 | Cell values are represented with "cell vectors", so for example, 72 | `Grid.render_cell_vec` will take a cell vector and produce an image. 73 | 74 | """ 75 | 76 | @property 77 | def num_cells(self): 78 | """The number of cells in this grid""" 79 | raise NotImplementedError 80 | 81 | def rect_to_cell_vec(self, r): 82 | """ 83 | Return a boolean cell vector corresponding with the input rectangle. 84 | 85 | Elements of the returned vector are True if and only if the 86 | corresponding cells fall within the input rectangle 87 | 88 | """ 89 | raise NotImplementedError 90 | 91 | def render_cell_vec(self, vec, im_width, im_height): 92 | """Render an image, using a cell vector and image dimensions.""" 93 | raise NotImplementedError 94 | 95 | 96 | class SquareGrid(Grid): 97 | """ 98 | A grid where cells correspond with pixels. 99 | 100 | This grid type is used for cascades which do not contain diagonal features. 101 | 102 | """ 103 | def __init__(self, width, height): 104 | self._width = width 105 | self._height = height 106 | self.cell_names = ["pixel_{}_{}".format(x, y) 107 | for y in range(height) for x in range(width)] 108 | 109 | @property 110 | def num_cells(self): 111 | return self._width * self._height 112 | 113 | def rect_to_cell_vec(self, r): 114 | assert not r.tilted 115 | out = numpy.zeros((self._width, self._height), dtype=numpy.bool) 116 | out[r.y:r.y + r.h, r.x:r.x + r.w] = True 117 | return out.flatten() 118 | 119 | def render_cell_vec(self, vec, im_width, im_height): 120 | im = vec.reshape(self._height, self._width) 121 | return cv2.resize(im, (im_width, im_height), 122 | interpolation=cv2.INTER_NEAREST) 123 | 124 | 125 | class TiltedGrid(Grid): 126 | """ 127 | A square grid, but each square consists of 4 cells. 128 | 129 | The squares are cut diagonally, resulting in a north, east, south and west 130 | triangle for each cell. 131 | 132 | This grid type is used for cascades which contain diagonal features: The 133 | idea is that the area which a diagonal feature should be integrated can be 134 | represented exactly by this structure. 135 | 136 | Unfortunately, this is not quite accurate: OpenCV's trainer and detector 137 | always resizes its images so that pixels correspond with one grid cell. As 138 | such cascades which contain diagonal features will not be accurately 139 | inverted by this script, however, they will have more detail as a result of 140 | the grid square subdivision. 141 | 142 | """ 143 | def __init__(self, width, height): 144 | self._width = width 145 | self._height = height 146 | 147 | self._cell_indices = {(d, x, y): 4 * ((width * y) + x) + d 148 | for y in range(height) 149 | for x in range(width) 150 | for d in range(4)} 151 | self.cell_names = ['cell_{}_{}_{}'.format(x, y, "NESW"[d]) 152 | for y in range(height) 153 | for x in range(width) 154 | for d in range(4)] 155 | self._cell_points = numpy.zeros((width * height * 4, 2)) 156 | 157 | for y in range(height): 158 | for x in range(width): 159 | self._cell_points[self._cell_indices[0, x, y], :] = \ 160 | numpy.array([x + 0.5, y + 0.25]) 161 | self._cell_points[self._cell_indices[1, x, y], :] = \ 162 | numpy.array([x + 0.75, y + 0.5]) 163 | self._cell_points[self._cell_indices[2, x, y], :] = \ 164 | numpy.array([x + 0.5, y + 0.75]) 165 | self._cell_points[self._cell_indices[3, x, y], :] = \ 166 | numpy.array([x + 0.25, y + 0.5]) 167 | 168 | @property 169 | def num_cells(self): 170 | return self._width * self._height * 4 171 | 172 | def _rect_to_bounds(self, r): 173 | if not r.tilted: 174 | dirs = numpy.matrix([[0, 1], [-1, 0], [0, -1], [1, 0]]) 175 | limits = numpy.matrix([[r.y, -(r.x + r.w), -(r.y + r.h), r.x]]).T 176 | else: 177 | dirs = numpy.matrix([[-1, 1], [-1, -1], [1, -1], [1, 1]]) 178 | limits = numpy.matrix([[r.y - r.x, 179 | 2 + -r.x -r.y - 2 * r.w, 180 | r.x - r.y - 2 * r.h, 181 | -2 + r.x + r.y]]).T 182 | 183 | return dirs, limits 184 | 185 | def rect_to_cell_vec(self, r): 186 | dirs, limits = self._rect_to_bounds(r) 187 | out = numpy.all(numpy.array(dirs * numpy.matrix(self._cell_points).T) 188 | >= limits, 189 | axis=0) 190 | return numpy.array(out)[0] 191 | 192 | def render_cell_vec(self, vec, im_width, im_height): 193 | out_im = numpy.zeros((im_height, im_width), dtype=vec.dtype) 194 | 195 | tris = numpy.array([[[0, 0], [1, 0], [0.5, 0.5]], 196 | [[1, 0], [1, 1], [0.5, 0.5]], 197 | [[1, 1], [0, 1], [0.5, 0.5]], 198 | [[0, 1], [0, 0], [0.5, 0.5]]]) 199 | 200 | scale_factor = numpy.array([im_width / self._width, 201 | im_height / self._height]) 202 | for y in reversed(range(self._height)): 203 | for x in range(self._width): 204 | for d in (2, 3, 1, 0): 205 | points = (tris[d] + numpy.array([x, y])) * scale_factor 206 | cv2.fillConvexPoly( 207 | img=out_im, 208 | points=points.astype(numpy.int32), 209 | color=vec[self._cell_indices[d, x, y]]) 210 | return out_im 211 | 212 | 213 | # Cascade definition 214 | 215 | class Stage(collections.namedtuple('_StageBase', 216 | ['threshold', 'weak_classifiers'])): 217 | """ 218 | A stage in an OpenCV cascade. 219 | 220 | .. attribute:: weak_classifiers 221 | 222 | A list of weak classifiers in this stage. 223 | 224 | .. attribute:: threshold 225 | 226 | The value that the weak classifiers must exceed for this stage to pass. 227 | 228 | """ 229 | 230 | 231 | class WeakClassifier(collections.namedtuple('_WeakClassifierBase', 232 | ['feature_idx', 'threshold', 'fail_val', 'pass_val'])): 233 | """ 234 | A weak classifier in an OpenCV cascade. 235 | 236 | .. attribute:: feature_idx 237 | 238 | Feature associated with this classifier. 239 | 240 | .. attribute:: threshold 241 | 242 | The value that this feature dotted with the input image must exceed for the 243 | feature to have passed. 244 | 245 | .. attribute:: fail_val 246 | 247 | The value contributed to the stage threshold if this classifier fails. 248 | 249 | .. attribute:: pass_val 250 | 251 | The value contributed to the stage threshold if this classifier passes. 252 | 253 | """ 254 | 255 | 256 | class Rect(collections.namedtuple('_RectBase', 257 | ['x', 'y', 'w', 'h', 'tilted', 'weight'])): 258 | """ 259 | A rectangle in an OpenCV cascade. 260 | 261 | Two or more of these make up a feature. 262 | 263 | .. attribute:: x, y 264 | 265 | Coordinates of the rectangle. 266 | 267 | .. attribute:: w, h 268 | 269 | Width and height of the rectangle, respectively. 270 | 271 | .. attribute:: tilted 272 | 273 | If true, the rectangle is to be considered rotated 45 degrees clockwise 274 | about its top-left corner. (+X is right, +Y is down.) 275 | 276 | .. attribute:: weight 277 | 278 | The value this rectangle contributes to the feature. 279 | 280 | """ 281 | 282 | 283 | class Cascade(collections.namedtuple('_CascadeBase', 284 | ['width', 'height', 'stages', 'features', 'tilted', 'grid'])): 285 | """ 286 | Pythonic interface to an OpenCV cascade file. 287 | 288 | .. attribute:: width 289 | 290 | Width of the cascade grid. 291 | 292 | .. attribute:: height 293 | 294 | Height of the cascade grid. 295 | 296 | .. attribute:: stages 297 | 298 | List of :class:`.Stage` objects. 299 | 300 | .. attribute:: features 301 | 302 | List of features. Each feature is in turn a list of :class:`.Rect`s. 303 | 304 | .. attribute:: tilted 305 | 306 | True if any of the features are tilted. 307 | 308 | .. attribute:: grid 309 | 310 | A :class:`.Grid` object suitable for use with the cascade. 311 | 312 | """ 313 | @staticmethod 314 | def _split_text_content(n): 315 | return n.text.strip().split(' ') 316 | 317 | @classmethod 318 | def load(cls, fname): 319 | """ 320 | Parse an OpenCV haar cascade XML file. 321 | 322 | """ 323 | root = xml.etree.ElementTree.parse(fname) 324 | 325 | width = int(root.find('./cascade/width').text.strip()) 326 | height = int(root.find('./cascade/height').text.strip()) 327 | 328 | stages = [] 329 | for stage_node in root.findall('./cascade/stages/_'): 330 | stage_threshold = float( 331 | stage_node.find('./stageThreshold').text.strip()) 332 | weak_classifiers = [] 333 | for classifier_node in stage_node.findall('weakClassifiers/_'): 334 | sp = cls._split_text_content( 335 | classifier_node.find('./internalNodes')) 336 | if sp[0] != "0" or sp[1] != "-1": 337 | raise Exception("Only simple cascade files are supported") 338 | feature_idx = int(sp[2]) 339 | threshold = float(sp[3]) 340 | 341 | sp = cls._split_text_content( 342 | classifier_node.find('./leafValues')) 343 | fail_val = float(sp[0]) 344 | pass_val = float(sp[1]) 345 | weak_classifiers.append( 346 | WeakClassifier(feature_idx, threshold, fail_val, pass_val)) 347 | stages.append(Stage(stage_threshold, weak_classifiers)) 348 | 349 | features = [] 350 | for feature_node in root.findall('./cascade/features/_'): 351 | feature = [] 352 | tilted_node = feature_node.find('./tilted') 353 | if tilted_node is not None: 354 | tilted = bool(int(tilted_node.text)) 355 | else: 356 | tilted = False 357 | for rect_node in feature_node.findall('./rects/_'): 358 | sp = cls._split_text_content(rect_node) 359 | x, y, w, h = (int(x) for x in sp[:4]) 360 | weight = float(sp[4]) 361 | feature.append(Rect(x, y, w, h, tilted, weight)) 362 | features.append(feature) 363 | 364 | tilted = any(r.tilted for f in features for r in f) 365 | 366 | if tilted: 367 | grid = TiltedGrid(width, height) 368 | else: 369 | grid = SquareGrid(width, height) 370 | 371 | stages = stages[:] 372 | 373 | return cls(width, height, stages, features, tilted, grid) 374 | 375 | def detect(self, im, epsilon=0.00001, scale_by_std_dev=False): 376 | """ 377 | Apply the cascade forwards on a potential face image. 378 | 379 | The algorithm is relatively slow compared to the integral image 380 | implementation, but is relatively terse and consequently useful for 381 | debugging. 382 | 383 | :param im: 384 | 385 | Image to apply the detector to. 386 | 387 | :param epsilon: 388 | 389 | Maximum rounding error to account for. This biases the classifier 390 | and stage thresholds towards passing. As a result, passing too 391 | large a value may result in false positive detections. 392 | 393 | :param scale_by_std_dev: 394 | 395 | If true, divide the input image by its standard deviation before 396 | processing. This simulates OpenCV's algorithm, however the reverse 397 | haar mapping implemented by this script does not account for the 398 | standard deviation divide, so to get the forward version of 399 | `inverse_haar`, pass False. 400 | 401 | """ 402 | im = im.astype(numpy.float64) 403 | 404 | im = cv2.resize(im, (self.width, self.height), 405 | interpolation=cv2.INTER_AREA) 406 | 407 | scale_factor = numpy.std(im) if scale_by_std_dev else 256. 408 | im /= scale_factor * (im.shape[1] * im.shape[0]) 409 | 410 | for stage_idx, stage in enumerate(self.stages): 411 | total = 0 412 | for classifier in stage.weak_classifiers: 413 | feature_array = self.grid.render_cell_vec( 414 | sum(self.grid.rect_to_cell_vec(r) * r.weight 415 | for r in self.features[classifier.feature_idx]), 416 | im.shape[1], im.shape[0]) 417 | 418 | if classifier.pass_val > classifier.fail_val: 419 | thr = classifier.threshold - epsilon 420 | else: 421 | thr = classifier.threshold + epsilon 422 | if numpy.sum(feature_array * im) >= thr: 423 | total += classifier.pass_val 424 | else: 425 | total += classifier.fail_val 426 | 427 | if total < stage.threshold - epsilon: 428 | return -stage_idx 429 | return 1 430 | 431 | 432 | class CascadeModel(Model): 433 | """ 434 | Model of the variables and constraints associated with a Haar cascade. 435 | 436 | This is in fact a wrapper around a docplex model. 437 | 438 | .. attribute:: cell_vars 439 | 440 | List of variables corresponding with the cells in the cascade's grid. 441 | 442 | .. attribute:: feature_vars 443 | 444 | Dict of feature indices to binary variables. Each variable represents 445 | whether the corresponding feature is present. 446 | 447 | .. attribute:: cascade 448 | 449 | The underlying :class:`.Cascade`. 450 | 451 | """ 452 | def __init__(self, cascade, docloud_context): 453 | """Make a model from a :class:`.Cascade`.""" 454 | super(CascadeModel, self).__init__("Inverse haar cascade", 455 | docloud_context=docloud_context) 456 | 457 | cell_vars = [self.continuous_var( 458 | name=cascade.grid.cell_names[i], 459 | lb=0., ub=MAX_PIXEL_VALUE) 460 | for i in range(cascade.grid.num_cells)] 461 | feature_vars = {idx: self.binary_var(name="feature_{}".format(idx)) 462 | for idx in range(len(cascade.features))} 463 | 464 | for stage in cascade.stages: 465 | # Add constraints for the feature vars. 466 | # 467 | # If the classifier's pass value is greater than its fail value, 468 | # then add a constraint equivalent to the following: 469 | # 470 | # feature var set => corresponding feature is present in image 471 | # 472 | # Conversely, if the classifier's pass vlaue is less than its fail 473 | # value, add a constraint equivalent to: 474 | # 475 | # corresponding feature is present in image => feature var set 476 | for classifier in stage.weak_classifiers: 477 | feature_vec = numpy.sum( 478 | cascade.grid.rect_to_cell_vec(r) * r.weight 479 | for r in cascade.features[classifier.feature_idx]) 480 | feature_vec /= (cascade.width * cascade.height) 481 | thr = classifier.threshold 482 | feature_var = feature_vars[classifier.feature_idx] 483 | feature_val = sum(cell_vars[i] * feature_vec[i] 484 | for i in numpy.argwhere( 485 | feature_vec != 0.).flatten()) 486 | if classifier.pass_val >= classifier.fail_val: 487 | big_num = 0.1 + thr - numpy.sum(numpy.min( 488 | [feature_vec, numpy.zeros(feature_vec.shape)], 489 | axis=0)) 490 | self.add_constraint(feature_val - feature_var * big_num >= 491 | thr - big_num) 492 | else: 493 | big_num = 0.1 + numpy.sum(numpy.max( 494 | [feature_vec, numpy.zeros(feature_vec.shape)], 495 | axis=0)) - thr 496 | self.add_constraint(feature_val - feature_var * big_num <= 497 | thr) 498 | 499 | # Enforce that the sum of features present in this stage exceeds 500 | # the stage threshold. 501 | fail_val_total = sum(c.fail_val for c in stage.weak_classifiers) 502 | adjusted_stage_threshold = stage.threshold 503 | self.add_constraint(sum((c.pass_val - c.fail_val) * 504 | feature_vars[c.feature_idx] 505 | for c in stage.weak_classifiers) >= 506 | adjusted_stage_threshold - fail_val_total) 507 | 508 | self.cascade = cascade 509 | self.cell_vars = cell_vars 510 | self.feature_vars = feature_vars 511 | 512 | def set_best_objective(self, minimize=False): 513 | """ 514 | Amend the model with an objective. 515 | 516 | The objective used is to maximise the score from each stage of the 517 | cascade. 518 | 519 | """ 520 | self.set_objective("min" if minimize else "max", 521 | sum((c.pass_val - c.fail_val) * 522 | self.feature_vars[c.feature_idx] 523 | for s in self.cascade.stages 524 | for c in s.weak_classifiers)) 525 | 526 | 527 | def inverse_haar(cascade, min_optimize=False, max_optimize=False, 528 | time_limit=None, docloud_context=None, lp_path=None): 529 | """ 530 | Invert a haar cascade. 531 | 532 | :param cascade: 533 | A :class:`.Cascade` to invert. 534 | 535 | :param min_optimize: 536 | Attempt to the solution which exceeds the stage constraints as little 537 | as possible. 538 | 539 | :param max_optimize: 540 | Attempt to the solution which exceeds the stage constraints as much 541 | as possible. 542 | 543 | :param time_limit: 544 | Maximum time to allow the solver to work, in seconds. 545 | 546 | :param docloud_context: 547 | :class:`docplex.mp.context.DOcloudContext` to use for solving. 548 | 549 | :param lp_path: 550 | File to write the LP constraints to. Useful for debugging. (Optional). 551 | 552 | """ 553 | if min_optimize and max_optimize: 554 | raise ValueError("Cannot pass both min_optimize and max_optimize") 555 | cascade_model = CascadeModel(cascade, docloud_context) 556 | if min_optimize or max_optimize: 557 | cascade_model.set_best_objective(minimize=min_optimize) 558 | if time_limit is not None: 559 | cascade_model.set_time_limit(time_limit) 560 | 561 | cascade_model.print_information() 562 | if lp_path: 563 | cascade_model.export_as_lp(path=lp_path) 564 | 565 | if not cascade_model.solve(): 566 | raise Exception("Failed to find solution") 567 | sol_vec = numpy.array([v.solution_value / MAX_PIXEL_VALUE 568 | for v in cascade_model.cell_vars]) 569 | im = cascade_model.cascade.grid.render_cell_vec(sol_vec, 570 | 10 * cascade.width, 571 | 10 * cascade.height) 572 | im = (im * 255.).astype(numpy.uint8) 573 | 574 | return im 575 | 576 | 577 | if __name__ == "__main__": 578 | import argparse 579 | 580 | parser = argparse.ArgumentParser(description= 581 | 'Inverse haar feature object detection') 582 | parser.add_argument('-c', '--cascade', type=str, required=True, 583 | help='OpenCV cascade file to be reversed.') 584 | parser.add_argument('-o', '--output', type=str, required=True, 585 | help='Output image name') 586 | parser.add_argument('-t', '--time-limit', type=float, default=None, 587 | help='Maximum time to allow the solver to work, in ' 588 | 'seconds.') 589 | parser.add_argument('-O', '--optimize', nargs='?', type=str, const='max', 590 | help='Try and find the "best" solution, rather than ' 591 | 'just a feasible solution. Pass "min" to find ' 592 | 'the least best solution.') 593 | parser.add_argument('-C', '--check', action='store_true', 594 | help='Check the result against the (forward) cascade.') 595 | parser.add_argument('-l', '--lp-path',type=str, default=None, 596 | help='File to write LP constraints to.') 597 | args = parser.parse_args() 598 | 599 | print "Loading cascade..." 600 | cascade = Cascade.load(args.cascade) 601 | 602 | docloud_context = DOcloudContext.make_default_context(DOCLOUD_URL) 603 | docloud_context.print_information() 604 | env = Environment() 605 | env.print_information() 606 | 607 | print "Solving..." 608 | im = inverse_haar(cascade, 609 | min_optimize=(args.optimize == "min"), 610 | max_optimize=(args.optimize == "max"), 611 | time_limit=args.time_limit, 612 | docloud_context=docloud_context, 613 | lp_path=args.lp_path) 614 | 615 | cv2.imwrite(args.output, im) 616 | print "Wrote {}".format(args.output) 617 | 618 | if args.check: 619 | print "Checking..." 620 | ret = cascade.detect(im) 621 | if ret != 1: 622 | print "Image failed the forward cascade at stage {}".format(-ret) 623 | 624 | --------------------------------------------------------------------------------