001/** 002 * Copyright (c) 2011, The University of Southampton and the individual contributors. 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without modification, 006 * are permitted provided that the following conditions are met: 007 * 008 * * Redistributions of source code must retain the above copyright notice, 009 * this list of conditions and the following disclaimer. 010 * 011 * * Redistributions in binary form must reproduce the above copyright notice, 012 * this list of conditions and the following disclaimer in the documentation 013 * and/or other materials provided with the distribution. 014 * 015 * * Neither the name of the University of Southampton nor the names of its 016 * contributors may be used to endorse or promote products derived from this 017 * software without specific prior written permission. 018 * 019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 029 */ 030/** 031 * 032 */ 033package org.openimaj.demos.sandbox.image; 034 035import java.io.File; 036import java.io.FilenameFilter; 037import java.io.IOException; 038import java.lang.reflect.Field; 039import java.net.MalformedURLException; 040import java.net.URL; 041import java.util.Collections; 042import java.util.Comparator; 043import java.util.HashSet; 044import java.util.Iterator; 045import java.util.List; 046import java.util.UUID; 047 048import org.kohsuke.args4j.Option; 049import org.openimaj.image.DisplayUtilities; 050import org.openimaj.image.FImage; 051import org.openimaj.image.ImageUtilities; 052import org.openimaj.image.MBFImage; 053import org.openimaj.image.colour.RGBColour; 054import org.openimaj.image.dataset.BingImageDataset; 055import org.openimaj.image.feature.global.SharpPixelProportion; 056import org.openimaj.image.processing.face.detection.DetectedFace; 057import org.openimaj.image.processing.face.detection.FaceDetector; 058import org.openimaj.image.processing.face.recognition.FaceRecognitionEngine; 059import org.openimaj.image.typography.hershey.HersheyFont; 060import org.openimaj.io.IOUtils; 061import org.openimaj.math.geometry.shape.Rectangle; 062import org.openimaj.ml.annotation.ScoredAnnotation; 063import org.openimaj.tools.faces.recognition.options.RecognitionEngineProvider; 064import org.openimaj.tools.faces.recognition.options.RecognitionStrategy; 065import org.openimaj.util.api.auth.DefaultTokenFactory; 066import org.openimaj.util.api.auth.common.BingAPIToken; 067import org.openimaj.util.pair.IndependentPair; 068 069/** 070 * Class for providing verification of unseen people in images. It does this by 071 * limiting the search space to be a verification problem rather than a 072 * recognition problem - that is, the possible number of people that could 073 * possibly be in an image is limited. The tool does a web search (currently 074 * using Bing) to retrieve images of the people in question and trains a face 075 * recognition engine using these. It then looks in the query image for faces 076 * and attempts to classify the found faces. It will classify them in increasing 077 * size order; so if more than one face is classified as a particular person, 078 * then only the largest instance will remain (if the options to allow only one 079 * instance is true). Annotations which are removed due to this constraint will 080 * be relabelled, if possible. There is also an option to ignore faces which are 081 * significantly blurred ("significantly" can be defined). 082 * <p> 083 * Note that, when run for the first time, the system will ask you to go and get 084 * an APPID for the Bing Search, and it will give you the URL to go get it. 085 * <p> 086 * The main method is just a test method - it will delete the recogniser after 087 * it's done. 088 * 089 * @author David Dupplaw (dpd@ecs.soton.ac.uk) 090 * @created 5 Feb 2013 091 * @version $Author$, $Revision$, $Date$ 092 */ 093public class PersonMatcher 094{ 095 /** The file the recogniser will be saved into */ 096 private static final String RECOGNISER_FILE = "recogniser.rec"; 097 098 /** Where to cache images */ 099 private static final String CACHE_DIR = "cache"; 100 101 /** The face detector to use to detect faces in all images */ 102 private final FaceDetector<?, FImage> faceDetector; 103 104 /** The face recognition engine we'll use */ 105 private final FaceRecognitionEngine<? extends DetectedFace, String> faceRecogniser; 106 107 /** Whether to cache search results */ 108 private final boolean cacheImages = true; 109 110 /** Whether to save the recogniser or not */ 111 private boolean saveRecogniser = false; 112 113 /** 114 * The threshold to apply to the annotator above which no match will be 115 * considered 116 */ 117 @Option(name = "--threshold", aliases = "-t", 118 usage = "The matching threshold (default: 8)") 119 private float matchingThreshold = 8f; 120 121 /** The recognition strategy to use */ 122 @Option(name = "--strategy", aliases = "-s", 123 usage = "The recognition strategy to use (default: CLMFeature_KNN)") 124 private final RecognitionStrategy strategy = RecognitionStrategy.CLMFeature_KNN; 125 126 @Option(name = "--onlyOne", aliases = "-o", 127 usage = "Allow only one instance of each person (default: true)") 128 /** If true, only one instance of each person will be allowed in a photo */ 129 private final boolean allowOnlyOneInstance = true; 130 131 @Option(name = "--ignoreBlurred", aliases = "-b", 132 usage = "Ignore faces which are considerably blurred (default: true)") 133 /** If true, will ignore faces which are blurred */ 134 private final boolean ignoreBlurredFaces = true; 135 136 @Option(name = "--blurThreshold", aliases = "-bt", 137 usage = "The threshold to use for blur detection (default: 0.2)") 138 /** Only used if ignoreBlurredFaces is true */ 139 private final float blurThreshold = 0.2f; 140 141 /** 142 * Create a person matcher 143 * 144 * @throws Exception 145 */ 146 public PersonMatcher() throws Exception 147 { 148 this(null); 149 } 150 151 /** 152 * Create a person matcher with the given file 153 * 154 * @param recogniserFile 155 * The recogniser file to load 156 * @throws Exception 157 */ 158 public PersonMatcher(final File recogniserFile) throws Exception 159 { 160 // Setup a new face recognition engine 161 this.faceRecogniser = this.getFaceRecogniserEngine(recogniserFile); 162 163 if (this.faceRecogniser == null) 164 throw new Exception("Face recogniser not initialised"); 165 166 // Get the face detector for this strategy 167 this.faceDetector = this.faceRecogniser.getDetector(); 168 } 169 170 /** 171 * Create a recogniser for the given person into the given file. 172 * 173 * @param person 174 * The person to create a recogniser for 175 * @param recogniserFile 176 * The recogniser file to save 177 * @throws Exception 178 * If the face recognition engine could not be initialised 179 */ 180 public PersonMatcher(final String person, final File recogniserFile) throws Exception 181 { 182 this(new String[] { person }, recogniserFile, true); 183 } 184 185 /** 186 * Constructor that takes a query string. 187 * 188 * @param queries 189 * The query strings to use 190 * @param recogniserFile 191 * The file to save the recogniser into 192 * @param addCounterExamples 193 * Whether to add counter examples 194 * @throws Exception 195 * If the face recognition engine could not be initialised 196 */ 197 public PersonMatcher(final List<String> queries, final File recogniserFile, 198 final boolean addCounterExamples) 199 throws Exception 200 { 201 this(queries.toArray(new String[0]), recogniserFile, addCounterExamples); 202 } 203 204 /** 205 * Constructor that takes a set of queries to search for 206 * 207 * @param queries 208 * The query strings to use 209 * @param recogniserFile 210 * The file to save the recogniser into (NULL for no saving) 211 * @param addCounterExamples 212 * Whether to add counter examples 213 * @throws Exception 214 * If the face recognition engine could not be initialised 215 */ 216 public PersonMatcher(final String[] queries, final File recogniserFile, 217 final boolean addCounterExamples) 218 throws Exception 219 { 220 this(recogniserFile); 221 222 if (recogniserFile != null) 223 this.saveRecogniser = true; 224 225 // Train using the given queries 226 this.train(queries); 227 228 // Add a set of images that are not of the query person. 229 if (addCounterExamples) 230 this.addCounterExamples(); 231 232 // Save the recogniser for later 233 if (this.saveRecogniser) 234 this.saveRecogniser(recogniserFile); 235 } 236 237 /** 238 * After training, you might want to save the recogniser 239 * 240 * @param recogniserFile 241 * The recogniser file to save to 242 * @throws IOException 243 */ 244 public void saveRecogniser(final File recogniserFile) throws IOException 245 { 246 System.out.println("Saving recogniser to " + recogniserFile); 247 248 // Save the recogniser 249 this.faceRecogniser.save(recogniserFile); 250 } 251 252 /** 253 * Train the recogniser with examples retrieved from searching with the 254 * given queries. 255 * 256 * @param queries 257 * The query strings 258 */ 259 public void train(final String[] queries) 260 { 261 // Now go and retrieve the images for the query 262 for (final String query : queries) 263 this.searchForExamples(query, query, false); 264 } 265 266 /** 267 * @param fi 268 * The image to find the query person within 269 * @return The matching results 270 */ 271 public List<? extends IndependentPair<? extends DetectedFace, ScoredAnnotation<String>>> 272 query(final FImage fi) 273 { 274 System.out.println("Querying with image"); 275 276 // Recognise the unknown faces in the image. 277 final List<? extends IndependentPair<? extends DetectedFace, ScoredAnnotation<String>>> recognisedFaces = this.faceRecogniser 278 .recogniseBest(fi); 279 280 for (final IndependentPair<? extends DetectedFace, ScoredAnnotation<String>> p : recognisedFaces) { 281 if (p.secondObject() == null) 282 p.setSecondObject(new ScoredAnnotation<String>("Unknown", 1.0f)); 283 } 284 285 // If we are to ignore blurred faces, we'll remove them here 286 // by using the SharpPixelProportion analyser to detect whether 287 // the image within the face region is blurred or not 288 if (this.ignoreBlurredFaces) 289 { 290 // We'll use the SharpPixelProportion analyser to work out how much 291 // is blurred 292 final SharpPixelProportion spp = new SharpPixelProportion(); 293 294 // Iterate over the detected faces 295 final Iterator<? extends IndependentPair<? extends DetectedFace, ScoredAnnotation<String>>> it = recognisedFaces 296 .iterator(); 297 while (it.hasNext()) 298 { 299 final IndependentPair<? extends DetectedFace, ScoredAnnotation<String>> facePair = it.next(); 300 301 // Analyse the face patch... 302 facePair.firstObject().getFacePatch().analyseWith(spp); 303 304 // If the pixels are mostly blurred, remove the face from the 305 // list. 306 final double pp = spp.getBlurredPixelProportion(); 307 if (pp < this.blurThreshold) 308 it.remove(); 309 } 310 } 311 312 // Sort on the size of the face 313 Collections.sort(recognisedFaces, 314 new Comparator<IndependentPair<? extends DetectedFace, ScoredAnnotation<String>>>() 315 { 316 @Override 317 public int compare( 318 final IndependentPair<? extends DetectedFace, ScoredAnnotation<String>> o1, 319 final IndependentPair<? extends DetectedFace, ScoredAnnotation<String>> o2) 320 { 321 return (int) (o2.firstObject().getShape().calculateArea() 322 - o1.firstObject().getShape().calculateArea()); 323 } 324 }); 325 326 System.out.println("Recognised " + recognisedFaces.size() + " faces."); 327 System.out.println(recognisedFaces); 328 329 // If we're only allowing a single instance of a face within an image, 330 // we need 331 // to check whether the recognised faces have been assigned to the same 332 // person 333 // more than once. If so, we'll check the faces in size order and remove 334 // any 335 // existing names. 336 if (this.allowOnlyOneInstance) 337 { 338 final HashSet<String> seenPeople = new HashSet<String>(); 339 final Iterator<? extends IndependentPair<? extends DetectedFace, ScoredAnnotation<String>>> it = recognisedFaces 340 .iterator(); 341 while (it.hasNext()) 342 { 343 final IndependentPair<? extends DetectedFace, ScoredAnnotation<String>> facePair = it.next(); 344 345 // If we've already seen the person that this face might be, we 346 // remove it from the list. We'll try again at recognising them. 347 if (seenPeople.contains(facePair.secondObject().annotation)) 348 { 349 // it.remove(); 350 facePair.secondObject().annotation = "Removed " + facePair.secondObject().annotation; 351 352 // Try to annotate this face again but within some 353 // constraints. 354 // The constraints will be all the possible annotations 355 // minus those 356 // that we've already seen. 357 final HashSet<String> constraints = new HashSet<String>(); 358 constraints.addAll(this.faceRecogniser.getRecogniser().getAnnotations()); 359 constraints.removeAll(seenPeople); 360 361 // Recognise the best from the people we've not already seen 362 final List<? extends IndependentPair<? extends DetectedFace, ScoredAnnotation<String>>> r = this.faceRecogniser 363 .recogniseBest( 364 facePair.firstObject().getFacePatch(), constraints); 365 366 // If we have a new person, then update the pair and add it 367 // to 368 // the seen people 369 if (r != null && r.size() > 0 && r.get(0).getSecondObject() != null) 370 { 371 // Update the annotation for this face. 372 facePair.getSecondObject().annotation = r.get(0).getSecondObject().annotation; 373 374 // Remember that this person has been seen 375 seenPeople.add(facePair.getSecondObject().annotation); 376 } 377 } 378 379 // Note that we've seen this person 380 seenPeople.add(facePair.secondObject().annotation); 381 } 382 } 383 384 return recognisedFaces; 385 } 386 387 /** 388 * Returns a face recogniser by using the FaceRecogniserTools. 389 * 390 * @param recogniserFile 391 * @return The face recogniser engine 392 * @throws IOException 393 */ 394 private FaceRecognitionEngine<? extends DetectedFace, String> getFaceRecogniserEngine( 395 final File recogniserFile) throws IOException 396 { 397 // If we have a pre-trained file to load, load it in. 398 if (recogniserFile != null && recogniserFile.exists()) 399 { 400 System.out.println("Loading existing recogniser from " + recogniserFile + " to update..."); 401 402 final FaceRecognitionEngine<DetectedFace, String> fre = FaceRecognitionEngine 403 .load(recogniserFile); 404 return fre; 405 } 406 407 // No pre-trained file? Then just create a new, clean, fresh and sparkly 408 // new engine. 409 try 410 { 411 // We look for a field called "threshold" in the strategy and set 412 // the threshold 413 // to the value in the options. If the field doesn't exist, we'll 414 // ignore it. 415 final Field f = this.strategy.getClass().getDeclaredField("threshold"); 416 f.setAccessible(true); 417 f.setFloat(this.strategy, this.matchingThreshold); 418 System.out.println("Field: " + f); 419 } catch (final NoSuchFieldException e) 420 { 421 System.out.println("WARNING: No threshold field to set in " + this.strategy + "."); 422 } catch (final SecurityException e) 423 { 424 System.out.println("WARNING: No threshold field to set in " + this.strategy + "."); 425 } catch (final IllegalArgumentException e) 426 { 427 e.printStackTrace(); 428 } catch (final IllegalAccessException e) 429 { 430 e.printStackTrace(); 431 } 432 final RecognitionEngineProvider<?> o = this.strategy.getOptions(); 433 return o.createRecognitionEngine(); 434 } 435 436 /** 437 * Adds a set of counter examples to the recogniser by searching the web for 438 * the generic string "face" and adding them as an unknown person. 439 */ 440 public void addCounterExamples() 441 { 442 this.searchForExamples("face", "unknown", true); 443 } 444 445 /** 446 * Retrieves a set of images from Bing that match the query then for each 447 * one calls the face recogniser to train it. 448 */ 449 private void searchForExamples(final String query, final String label, final boolean facesOnly) 450 { 451 System.out.println("Searching for '" + query + "'"); 452 453 File f = null; 454 if (this.cacheImages && (f = new File(PersonMatcher.CACHE_DIR + "/" + label + "/")).exists()) 455 { 456 System.out.println("Using cached images: "); 457 for (final File cachedImage : f.listFiles(new FilenameFilter() 458 { 459 @Override 460 public boolean accept(final File file, final String filename) 461 { 462 return filename.endsWith(".png"); 463 } 464 })) 465 { 466 try 467 { 468 this.processImageURL(ImageUtilities.readMBF(cachedImage), label); 469 } catch (final MalformedURLException m) 470 { 471 m.printStackTrace(); 472 } catch (final IOException e) 473 { 474 e.printStackTrace(); 475 } 476 } 477 return; 478 } 479 480 final BingAPIToken apiToken = DefaultTokenFactory.get(BingAPIToken.class); 481 final BingImageDataset<MBFImage> results = BingImageDataset.create( 482 ImageUtilities.MBFIMAGE_READER, apiToken, query, 10); 483 484 System.out.println(" - Got " + results.getImages().size() + " results"); 485 486 // Loop over all the results and process each one 487 for (final MBFImage result : results) 488 this.processImageURL(result, label); 489 } 490 491 /** 492 * For each URL (that is an image representation of the query), load it in 493 * and train the face recogniser. 494 * 495 * @param result 496 * The URL of an image that is a representation of the query 497 * @param label 498 * The classification of the URL 499 */ 500 private void processImageURL(final MBFImage result, final String label) 501 { 502 final UUID uuid = UUID.nameUUIDFromBytes(result.toByteImage()); 503 final String cacheFilename = PersonMatcher.CACHE_DIR + "/" + label + "/" + uuid + ".png"; 504 if (this.cacheImages) 505 { 506 try 507 { 508 final File f = new File(cacheFilename); 509 f.getParentFile().mkdirs(); 510 if (!f.exists()) 511 ImageUtilities.write(result, f); 512 } catch (final IOException e) 513 { 514 e.printStackTrace(); 515 } 516 } 517 518 System.out.println("Reading " + result); 519 520 // Read in the result image 521 final FImage img = result.flatten(); 522 523 // Get the detected faces from the given image 524 List<? extends DetectedFace> detectedFaces = null; 525 if (this.cacheImages && new File(cacheFilename + ".detectedFaces").exists()) 526 { 527 System.out.println(" - Reading from file " + cacheFilename + ".detectedFaces..."); 528 try { 529 detectedFaces = IOUtils.readFromFile(new File(cacheFilename + ".detectedFaces")); 530 } catch (final IOException e1) { 531 e1.printStackTrace(); 532 } 533 } 534 // No cache? Let's run the face detector. 535 else 536 { 537 detectedFaces = this.faceDetector.detectFaces(img); 538 539 // If we're caching, let's also cache the face detection results 540 if (this.cacheImages) 541 { 542 try 543 { 544 final File f = new File(cacheFilename + ".detectedFaces"); 545 IOUtils.writeToFile(detectedFaces, f); 546 } catch (final IOException e) 547 { 548 e.printStackTrace(); 549 } 550 } 551 } 552 553 System.out.println(" - Found " + detectedFaces.size() + " faces "); 554 555 // If there is more than one person in the image (or none), 556 // then we can't sensibly say which one was the query.. so 557 // we must ignore. If there is only one detected face, we 558 // assume that it's the face of the person in the query 559 if (detectedFaces.size() == 1) 560 this.faceRecogniser.train(label, img); 561 else 562 System.out.println(" - Ignoring this image."); 563 } 564 565 /** 566 * Get the current matching threhsold of this person matcher. 567 * 568 * @return The matching threshold 569 */ 570 public float getMatchingThreshold() 571 { 572 return this.matchingThreshold; 573 } 574 575 /** 576 * Set the matching threshold of this person matcher. Note that this must be 577 * called prior to processing a frame; calling afterwards will have no 578 * effect. 579 * 580 * @param matchingThreshold 581 * The matching threshold to set 582 */ 583 public void setMatchingThreshold(final float matchingThreshold) 584 { 585 this.matchingThreshold = matchingThreshold; 586 } 587 588 /** 589 * 590 * @param resource 591 * @return The displayed image 592 * @throws Exception 593 */ 594 public static MBFImage displayQueryResults(final URL resource) throws Exception 595 { 596 System.out.println("----------- QUERYING ----------- "); 597 final FImage fi = ImageUtilities.readF(resource); 598 final PersonMatcher pm = new PersonMatcher(new File(PersonMatcher.RECOGNISER_FILE)); 599 final List<? extends IndependentPair<? extends DetectedFace, ScoredAnnotation<String>>> l = pm.query(fi); 600 601 final MBFImage m = new MBFImage(fi.getWidth(), fi.getHeight(), 3); 602 m.addInplace(fi); 603 int count = 1; 604 for (final IndependentPair<? extends DetectedFace, ScoredAnnotation<String>> i : l) 605 { 606 final Rectangle b = i.firstObject().getBounds(); 607 m.drawShape(b, RGBColour.RED); 608 final String name = count + " : " + 609 (i.secondObject() == null ? "Unknown" : i.secondObject().annotation); 610 m.drawText(name, (int) b.x, (int) b.y, 611 HersheyFont.TIMES_MEDIUM, 12, RGBColour.GREEN); 612 count++; 613 } 614 DisplayUtilities.display(m); 615 return m; 616 } 617 618 /** 619 * @param args 620 * @throws IOException 621 */ 622 public static void main(final String[] args) throws IOException 623 { 624 // Use the constructor that takes the queries and automatically creates 625 // a recogniser for the given person and then saves it 626 try 627 { 628 System.out.println("----------- TRAINING ---------- "); 629 new PersonMatcher( 630 new String[] { "Barack Obama", "Arnold Schwarzenegger" }, 631 new File(PersonMatcher.RECOGNISER_FILE), 632 true); 633 } catch (final Exception e) 634 { 635 e.printStackTrace(); 636 } 637 638 // Load back in the recogniser and try querying using the given image 639 try 640 { 641 // PersonMatcher.displayQueryResults( 642 // PersonMatcher.class.getResource( 643 // "/org/openimaj/demos/sandbox/BarackObama1.jpg")); 644 // 645 // PersonMatcher.displayQueryResults( 646 // PersonMatcher.class.getResource( 647 // "/org/openimaj/demos/sandbox/BarackObama2.jpg")); 648 // 649 // PersonMatcher.displayQueryResults( 650 // PersonMatcher.class.getResource( 651 // "/org/openimaj/demos/sandbox/BarackObama5.jpg")); 652 // 653 // PersonMatcher.displayQueryResults( 654 // PersonMatcher.class.getResource( 655 // "/org/openimaj/demos/sandbox/ArnoldSchwarzenegger1.jpg")); 656 657 PersonMatcher 658 .displayQueryResults(new URL( 659 "http://www2.pictures.gi.zimbio.com/Barack%2BObama%2BArnold%2BSchwarzenegger%2BBloomberg%2BO6kM6r0LSK-l.jpg")); 660 661 PersonMatcher.displayQueryResults(new URL( 662 "http://static.guim.co.uk/sys-images/Guardian/Pix/pictures/2008/08/02/Arnie-460x276.jpg")); 663 664 PersonMatcher.displayQueryResults(new URL( 665 "http://images.politico.com/global/2012/09/120930_arnold_maria_reu.jpg")); 666 667 PersonMatcher 668 .displayQueryResults(new URL( 669 "http://assets-s3.usmagazine.com/uploads/assets/articles/56812-what-do-you-want-to-ask-president-barack-obama/1350336415_barack-obama-467.jpg")); 670 671 // Remove the recogniser (for testing) 672 new File(PersonMatcher.RECOGNISER_FILE).delete(); 673 } catch (final Exception e) 674 { 675 e.printStackTrace(); 676 } 677 } 678}