Project 2: New Faces – Mauricio Giraldo
Background
New Faces is a project that began in 2009 after I got fed up with Colombia’s online newspapers’ (one site, another site) high emphasis in sports and celebtrity-related news[1]. I decided to take a screenshot of the offending website and remove this information (along with advertising) in order to see what was left:
Clearly sports, advertising and celebrity news have a relatively high importance for this colombian newspaper (at least in its online incarnation). This led me to wonder if this was the same across different countries. I decided to create a Python script that makes use of webkit2png-0.5 to take a screenshot of eleven news websites every night at 8pm. This script has been ran almost every night since November 8, 2009.
New faces
After seeing Rutherford Chang’s work with the New York Times where he blacks-out every element in the page except faces (no link available, sorry) I thought it would be an interesting idea to execute this same process in my dataset of more than 3,000 files and 10 GB.
I tested various technical solution for this process and decided upon using the OpenCV library for Processing. I could not find a library that could do non-rectagular detection (this one seems to work only in Windows and I have a Mac).

OpenCV for Processing: Hello World!
After that it was a matter of applying it to my dataset and see if it worked:
I tried several parameters for the detect() method since there were many false positives (text selected as face) and false negatives (face undetected when there should be) but could not get 100% accuracy. These errors provide an interesting outcome anyway.
The faces are extracted from the screenshots and saved as separate files along with their original location in x,y and number of total faces in the screenshot. There were 28,000+ faces deteced (less than 10 faces per screenshot):
With this new subset I then created a Flash interface to view the faces in their original x,y coordinates with buttons to toggle the visibility of individual news sites. Next to the buttons a blue line indicates average face area, a green line indicates how many screenshots (individual days captured) and a red line indicates total amount of faces in that site.
View the Flash interface online in your browser. F for fullscreen (click the interface to give it focus). CAUTION: ~30,000 images files loaded might clog up your browser (although they’re about 2kb each only).
Some screenshots below:
Future work
From this initial work some superficial conclusions could be made (see photo captions). Further analysis could provide some interesting information such as:
- dominant race or skin color
- gender distribution
- dominant face expression (sad, happy, angry, etc.)
- average face area
- preferred location in the page
- how the above change over time
The interface makes use of Bit-101′s Minimal Comps.
For those so inclined, this is the code for detecting faces in Processing (assumes a folder with images and a text file listing the images to use):
import hypermedia.video.*; import java.awt.Rectangle; OpenCV opencv; // contrast/brightness values int contrast_value = 0; int brightness_value = 0; PImage img; PImage maskimg; boolean is_masking = false; boolean has_started = false; int current = 0; String files[]; String current_file = ""; String current_file_no_suffix = ""; String path = ""; PFont font; // ================================================== // setup() // ================================================== void setup() { opencv = new OpenCV( this ); size(300, 100); fill(255,255,255); font = createFont("Inconsolata", 12); textFont(font); path = selectFolder(); files = loadStrings(selectInput()); } // ================================================== // draw() // ================================================== void draw() { if (!is_masking && files != null && files.length>0 && current<files.length) { is_masking = true; current_file = path + "/" + files[current]; current_file_no_suffix = current_file.substring(0,current_file.indexOf(".")); current_file_no_suffix = current_file_no_suffix.substring(33); processFile(); current++; } } void clearScreen() { background(0); } void processFile() { img = loadImage(current_file); opencv.allocate(img.width, img.height); opencv.copy(img); opencv.cascade( OpenCV.CASCADE_FRONTALFACE_ALT ); // load detection description, here-> front face detection : "haarcascade_frontalface_alt.xml" // Size of applet saveFaces(); } void saveFaces() { Rectangle[] faces = opencv.detect(1.3,3,opencv.HAAR_DO_CANNY_PRUNING,10,10); /**/ color white = color(255, 255, 255); if (faces.length>0) { for( int i=0; i<faces.length; i++ ) { /**/ maskimg = createImage(faces[i].width, faces[i].height, RGB); for (int j = 0; j < faces[i].width; j++) { for (int k = 0; k < faces[i].height; k++) { maskimg.set(j,k,img.get(faces[i].x+j,faces[i].y+k)); } } maskimg.save("output/" + current_file_no_suffix + "_" + i + "-" + faces.length + "_" + faces[i].x + "-" + faces[i].y + "_" + faces[i].width + "-" + faces[i].height + ".jpg"); /**/ clearScreen(); text("Done!\nProcessing file " + current + " of " + files.length + " with " + faces.length + " faces: " + current_file,10,10,280,80); is_masking = false; } } else { is_masking = false; } } public void stop() { opencv.stop(); super.stop(); }
The code for the Flash interface:
package com.mga { import com.bit101.components.Label; import flash.events.KeyboardEvent; import com.bit101.components.HSlider; import flash.events.IOErrorEvent; import com.bit101.components.PushButton; import com.bit101.components.Style; import com.bit101.components.VSlider; import flash.display.LoaderInfo; import flash.events.ProgressEvent; import flash.display.Loader; import flash.net.URLLoader; import flash.net.URLRequest; import flash.display.StageDisplayState; import flash.display.StageScaleMode; import flash.display.StageAlign; import flash.events.Event; import flash.display.Sprite; /** * @author mga */ public class FaceViewer extends Sprite { private var _filenames : Array; private var _imagesVector : Vector.<Loader>; private var _sites : Object; private var _baseFolder : String = "file:////Users/mga/Documents/Processing/face_opencv_03/output/";// private var _textLoader : URLLoader; private var _currentFile : int; private var _isLoading : Boolean; private var _loaderClip : LoaderClip; //private var _database : Vector.<Object>; public var _imageContainer : Sprite; private var _hScrollBar : HSlider; private var _vScrollBar : VSlider; private var _controlsClip : Sprite; private var _baseWidth : Number = 800; private var _scrollBarWidth : Number = 10; private var _buttonWidth : Number = 65; private var _buttonHeight : Number = 20; private var _maxWidth : Number = 1200; private var _labelWidth : Number = 65; private var _graphWidth : Number = 60; private var _graphHeight : Number = 4; public function FaceViewer() { init(); } private function init() : void { initProperties(); initContainers(); initControls(); addListeners(); processList(); } private function initContainers() : void { // image container clip _imageContainer = new Sprite(); _imageContainer.name = "_imageContainer"; addChild(_imageContainer); var tmp:Sprite; var i:Number = 0; for (var site:String in _sites) { tmp = new Sprite(); tmp.name = site; _imageContainer.addChild(tmp); i++; } } private function initControls() : void { // interface Style.setStyle(Style.DARK); _hScrollBar = new HSlider(this,_buttonWidth,0,onHScroll); _hScrollBar.setSize(_baseWidth - (_buttonWidth*2), _scrollBarWidth); _hScrollBar.setSliderParams(-(_baseWidth-_maxWidth)/2, (_baseWidth-_maxWidth)/2, 0); _hScrollBar.alpha = 0.5; addChild(_hScrollBar); _vScrollBar = new VSlider(this,_baseWidth - _scrollBarWidth,_scrollBarWidth,onVScroll); _vScrollBar.setSize(_scrollBarWidth, _baseWidth/ 2); _vScrollBar.alpha = 0.5; addChild(_vScrollBar); // base loading clip _loaderClip = new LoaderClip(); addChild(_loaderClip); _loaderClip.visible = false; _loaderClip.y = stage.stageHeight - _loaderClip.height; _controlsClip = new Sprite(); var site:String; var btn:PushButton; var lbl:Label; var i:Number = 0; var fSquare:Sprite; var aSquare:Sprite; var dSquare:Sprite; for (site in _sites) { btn = new PushButton(_controlsClip, 0, i * _buttonHeight, site, onSiteButtonPressed); btn.toggle = true; btn.name = site; btn.label = _sites[site].name; btn.alpha = 0.5; btn.setSize(_buttonWidth, _buttonHeight); _controlsClip.addChild(btn); /** lbl = new Label(_controlsClip, _buttonWidth, i * _buttonHeight, "F:0 A:0"); lbl.name = site + "_lbl"; _controlsClip.addChild(lbl); /**/ fSquare = new Sprite(); fSquare.x = _buttonWidth + 2; fSquare.y = _buttonHeight * i; fSquare.name = site + "f"; fSquare.graphics.beginFill(0xFF0000); fSquare.graphics.moveTo(0, 0); fSquare.graphics.lineTo(5, 0); fSquare.graphics.lineTo(5, _graphHeight); fSquare.graphics.lineTo(0, _graphHeight); fSquare.graphics.lineTo(0, 0); fSquare.graphics.endFill(); _controlsClip.addChild(fSquare); dSquare = new Sprite(); dSquare.x = _buttonWidth + 2; dSquare.y = (_buttonHeight * i) + (_graphHeight)+2; dSquare.name = site + "d"; dSquare.graphics.beginFill(0x00FF00); dSquare.graphics.moveTo(0, 0); dSquare.graphics.lineTo(5, 0); dSquare.graphics.lineTo(5, _graphHeight); dSquare.graphics.lineTo(0, _graphHeight); dSquare.graphics.lineTo(0, 0); dSquare.graphics.endFill(); _controlsClip.addChild(dSquare); aSquare = new Sprite(); aSquare.x = _buttonWidth + 2; aSquare.y = (_buttonHeight * i) + ((_graphHeight * 2))+4; aSquare.name = site + "a"; aSquare.graphics.beginFill(0x0000FF); aSquare.graphics.moveTo(0, 0); aSquare.graphics.lineTo(5, 0); aSquare.graphics.lineTo(5, _graphHeight); aSquare.graphics.lineTo(0, _graphHeight); aSquare.graphics.lineTo(0, 0); aSquare.graphics.endFill(); _controlsClip.addChild(aSquare); i++; } addChild(_controlsClip); } private function onSiteButtonPressed(event : Event) : void { _sites[event.target.name].show = !_sites[event.target.name].show; trace(_sites[event.target.name].show); } private function processList() : void { // loads the file var file:String = _baseFolder + "lista.txt"; var request:URLRequest = new URLRequest(file); _textLoader.load(request); } private function loadImages(e:Event) : void { trace("loaded!"); _filenames = String(_textLoader.data).split("\n"); trace("files:",_filenames.length); _currentFile = 0; _isLoading = true; loadNextFile(); } private function loadAllFiles() : void { var i:Number; for (i=0;i < _filenames.length;++i) { _imagesVector[i] = new Loader(); //trace("loading:",_filenames[_currentFile]); _imagesVector[i].load(new URLRequest(_baseFolder + _filenames[i])); var o:Object = objectify(i); _imagesVector[i].x = (stage.stageWidth/2) + o.x - (_sites[o.site][1]/2); _imagesVector[i].y = o.y; Sprite(_imageContainer.getChildByName(o.site)).addChild(_imagesVector[i]); } } private function loadNextFile() : void { if (_isLoading && _currentFile < _filenames.length && _filenames[_currentFile]!="") { updateLoader(_loaderClip, (_currentFile + 1) / _filenames.length, (_currentFile+1) + "/" + _filenames.length); _imagesVector[_currentFile] = new Loader(); //trace("loading:",_baseFolder + _filenames[_currentFile]); _imagesVector[_currentFile].load(new URLRequest(_baseFolder + _filenames[_currentFile])); _imagesVector[_currentFile].contentLoaderInfo.addEventListener(Event.COMPLETE, imageLoaded); _imagesVector[_currentFile].contentLoaderInfo.addEventListener(IOErrorEvent.IO_ERROR, imageLoadError); } else { trace("done!"); _isLoading = false; hideLoaders(); } } private function imageLoadError(event : IOErrorEvent) : void { trace("error loading: ", _baseFolder + _filenames[_currentFile]); } private function imageLoaded(event : Event) : void { LoaderInfo(event.target).removeEventListener(Event.COMPLETE, imageLoaded); LoaderInfo(event.target).removeEventListener(IOErrorEvent.IO_ERROR, imageLoadError); placeImage(_currentFile); _currentFile++; loadNextFile(); } private function placeImage(num : int) : void { var o:Object = objectify(num); _sites[o.site].area = normalize(_sites[o.site].faces,_sites[o.site].area, o.width * o.height); if (_sites[o.site].lastday!=o.timestamp) { _sites[o.site].lastday = o.timestamp; _sites[o.site].days++; } _sites[o.site].faces++; updateGraphs(); _imagesVector[num].x = (stage.stageWidth/2) + o.x - (_sites[o.site].width/2); _imagesVector[num].y = o.y; Sprite(_imageContainer.getChildByName(o.site)).addChild(_imagesVector[num]); //Label(_controlsClip.getChildByName(o.site+"_lbl")).text = "F:" + _sites[o.site].faces + " A:" + _sites[o.site].area; } private function updateGraphs() : void { var site:String; var gF:Sprite; var gA:Sprite; var gD:Sprite; var maxArea:Number = 0; var maxFaces:Number = 0; var maxDays:Number = 0; for (site in _sites) { if (_sites[site].area > maxArea) maxArea = _sites[site].area; if (_sites[site].faces > maxFaces) maxFaces = _sites[site].faces; if (_sites[site].days > maxDays) maxDays = _sites[site].days; } for (site in _sites) { gF = Sprite(_controlsClip.getChildByName(site+"f")); gF.width = _sites[site].faces / maxFaces * _graphWidth; gA = Sprite(_controlsClip.getChildByName(site+"a")); gA.width = _sites[site].area / maxArea * _graphWidth; gD = Sprite(_controlsClip.getChildByName(site+"d")); gD.width = _sites[site].days / maxDays * _graphWidth; } } private function normalize(faces : Number, oldarea : Number, newarea : Number) : Number { var a:Number = 0; if (!isNaN(faces) && !isNaN(oldarea) && !isNaN(newarea)) { a = ((faces * oldarea) + newarea) / (faces+1); return int(a); } return a; } private function objectify(num : int) : Object { // break up the name into its components var name:String = _filenames[num]; //20091108190345clarin-full_1-17_715-3292_24-24.jpg //YYYYMMDDHHIISSsite-full_num-total_x-y_w-h.jpg var tmp:Object = {}; tmp.timestamp = Number(name.substr(0,8)); tmp.year = Number(name.substr(0,4)); tmp.month = Number(name.substr(4,2)); tmp.day = Number(name.substr(6,2)); tmp.site = name.substr(14).substring(0,name.substr(14).indexOf("-")); var arr:Array = name.split("_"); tmp.num = Number(arr[1].split("-")[0]); tmp.total = Number(arr[1].split("-")[1]); tmp.x = Number(arr[2].split("-")[0]); tmp.y = Number(arr[2].split("-")[1]); tmp.width = Number(arr[3].split("-")[0]); tmp.height = Number(arr[3].split("-")[1].split(".")[0]); //_database[num] = tmp; return tmp;//_database[num]; } private function hideLoaders() : void { _loaderClip.visible = false; } private function draw(event : Event) : void { updateControls(); } private function updateControls() : void { _hScrollBar.x = stage.stageWidth - _hScrollBar.width; _vScrollBar.x = stage.stageWidth-_scrollBarWidth; _vScrollBar.minimum = -_imageContainer.height; _vScrollBar.maximum = 0; var i:Number = 0; for (var site:String in _sites) { _imageContainer.getChildByName(site).x = (stage.stageWidth / 2) - (_sites[site].width / 2); _imageContainer.getChildByName(site).visible = _sites[site].show; i++; } } private function updateLoader(clip:LoaderClip, pct:Number, txt:String = "") : void { clip.visible = true; clip.bar_mc.width = clip.track_mc.width * pct; clip.status_txt.text = txt; } private function addListeners() : void { stage.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler); _textLoader.addEventListener(Event.COMPLETE, loadImages); //Wait to be added to stage addEventListener(Event.ADDED_TO_STAGE, onAddedToStage); } private function onVScroll(event : Event) : void { _imageContainer.y = _vScrollBar.value; } private function onHScroll(event : Event) : void { _imageContainer.x = _hScrollBar.value; } private function imageLoadProgress(event : ProgressEvent) : void { var ldr:LoaderInfo = LoaderInfo(event.target); var pct:Number = ldr.bytesLoaded / ldr.bytesTotal; if (pct>=1) { ldr.removeEventListener(ProgressEvent.PROGRESS, imageLoadProgress); } } private function initProperties() : void { // interface elements // variables _textLoader = new URLLoader(); // arrays //_database = new Vector.<Object>(); _imagesVector = new Vector.<Loader>(); _filenames = []; _sites = {}; _sites["clarin"] = {}; _sites["clarin"].name = "el clarín"; _sites["clarin"].width = 1000; _sites["clarin"].faces = 0; _sites["clarin"].area = 0; _sites["clarin"].shots = 0; _sites["clarin"].days = 0; _sites["clarin"].lastday = 0; _sites["clarin"].show = true; _sites["elpais"] = {}; _sites["elpais"].name = "el país"; _sites["elpais"].width = 996; _sites["elpais"].faces = 0; _sites["elpais"].area = 0; _sites["elpais"].shots = 0; _sites["elpais"].days = 0; _sites["clarin"].lastday = 0; _sites["elpais"].show = true; _sites["elespectador"] = {}; _sites["elespectador"].name = "el espectador"; _sites["elespectador"].width = 1000; _sites["elespectador"].faces = 0; _sites["elespectador"].area = 0; _sites["elespectador"].shots = 0; _sites["elespectador"].days = 0; _sites["elespectador"].lastday = 0; _sites["elespectador"].show = true; _sites["eltiempo"] = {}; _sites["eltiempo"].name = "el tiempo"; _sites["eltiempo"].width = 970; _sites["eltiempo"].faces = 0; _sites["eltiempo"].area = 0; _sites["eltiempo"].shots = 0; _sites["eltiempo"].days = 0; _sites["eltiempo"].lastday = 0; _sites["eltiempo"].show = true; _sites["guardian"] = {}; _sites["guardian"].name = "the guardian"; _sites["guardian"].width = 950; _sites["guardian"].faces = 0; _sites["guardian"].area = 0; _sites["guardian"].shots = 0; _sites["guardian"].days = 0; _sites["guardian"].lastday = 0; _sites["guardian"].show = true; _sites["huffingtonpost"] = {}; _sites["huffingtonpost"].name = "the huffington post"; _sites["huffingtonpost"].width = 1016; _sites["huffingtonpost"].faces = 0; _sites["huffingtonpost"].area = 0; _sites["huffingtonpost"].shots = 0; _sites["huffingtonpost"].days = 0; _sites["huffingtonpost"].lastday = 0; _sites["huffingtonpost"].show = true; _sites["lemonde"] = {}; _sites["lemonde"].name = "le monde"; _sites["lemonde"].width = 1026; _sites["lemonde"].faces = 0; _sites["lemonde"].area = 0; _sites["lemonde"].shots = 0; _sites["lemonde"].days = 0; _sites["lemonde"].lastday = 0; _sites["lemonde"].show = true; _sites["nytimes"] = {}; _sites["nytimes"].name = "new york times"; _sites["nytimes"].width = 972; _sites["nytimes"].faces = 0; _sites["nytimes"].area = 0; _sites["nytimes"].shots = 0; _sites["nytimes"].days = 0; _sites["nytimes"].lastday = 0; _sites["nytimes"].show = true; _sites["smh"] = {}; _sites["smh"].name = "sydney morning herald"; _sites["smh"].width = 990; _sites["smh"].faces = 0; _sites["smh"].area = 0; _sites["smh"].shots = 0; _sites["smh"].days = 0; _sites["smh"].lastday = 0; _sites["smh"].show = true; _sites["theage"] = {}; _sites["theage"].name = "the age"; _sites["theage"].width = 990; _sites["theage"].faces = 0; _sites["theage"].area = 0; _sites["theage"].shots = 0; _sites["theage"].days = 0; _sites["theage"].lastday = 0; _sites["theage"].show = true; _sites["wsj"] = {}; _sites["wsj"].name = "wall street journal"; _sites["wsj"].width = 992; _sites["wsj"].faces = 0; _sites["wsj"].area = 0; _sites["wsj"].shots = 0; _sites["wsj"].days = 0; _sites["wsj"].lastday = 0; _sites["wsj"].show = true; _sites["thesun"] = {}; _sites["thesun"].name = "the sun"; _sites["thesun"].width = 996; _sites["thesun"].faces = 0; _sites["thesun"].area = 0; _sites["thesun"].shots = 0; _sites["thesun"].days = 0; _sites["thesun"].lastday = 0; _sites["thesun"].show = true; } private function onAddedToStage(event : Event) : void { //If added to stage, set stage properties and start listening to the global enterframe. stage.frameRate = 120; stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; stage.addEventListener(Event.ENTER_FRAME, draw); stage.addEventListener(Event.RESIZE, resizeHandler); } private function resizeHandler(event : Event) : void { updateControls(); } private function keyDownHandler(e:KeyboardEvent):void { // 32 = SPACE var code:uint = e.keyCode; if (code == 70) { // F if (stage.displayState == "normal") { stage.displayState = "fullScreen"; } } } } }
–
1 While this might have changed since then (at least ine site suffered an important redesign, not for the better, in my opinion)














Comments from PiratePad A:
Stripping the news down to the news. Nice concept, too bad the actual companies don’t adopt this policy. This is like what would happen if you took all the flashy visuals and particle effects away from Fox. You’d come up with a very uninformative show without enough of the bells and whistles to keep the unintelligent interested in the programming.
DOM replacement as a sketch is nice
jesus christ 10 gb? of screenshots? compressed png? ffffuuuuuuuuu 10 gig
link to artist who blacks out the news with face?
Nice narrative presentation style. Presentation time is lost in a lot of places, including explaining the filenaming structure. However, the narrative skipped from taking out the “sports” to capturing faces. I got lost for a second. I liked how you showed your early prototypes and why you discarded them. Buttons for website on/off don’t have state feedback.
Great to see all of those faces! It would be nice if there was some way to see images below when the overlap. Maybe stacks could explode when you mouse over.
It wasn’t immediately obvious that this is mapping to positions on the original page. If the software lived within a web browser window it would instantly be easier to understand.
Smart way of mapping the images with their positions and layering with the other pages.
Totally agree – News would be so much more appealing if the amount of sport + celebrity coverage was toned down. >> But then again, everyone would rather see pictures of Ke$ha all day than pictures of Nancy Pelosi. >> This is true.
The sheer amount of data makes this really compelling. Now curious to know what else you’ve been collecting in 10gb bundles.
I like the face recognition idea ;) One thing you could about the duplicating portraits is just discard faces if they have a certain threshold of pixel overlap with another face. The flash visualization looks somewhat chaotic. Is there a way to organize the content? But it is interesting, just seeing the position ;) zooming would be nice.
How long will this take to load?? I love the comparisons you can make between countries.
Great idea, but I wonder if you could maybe present the data in a format that’s easier to view.
Very cool that you have been collecting this data since 2009. Nice concept to look at faces. Visually the collection is very rich. Wow, the end result is very impressive! Nicely done! I like that all of your visual variables carry some significance, like the position that they originally appeared on the site. The ability to sort by site is a nice interactive feature to filter through the information and make some interesting comparisons. Impressive technical implementation.
Very cool! Awesome it turned out well.
Really interesting, really cool, massive data set. Maybe the ability to load/play over time. So you can hit play and see the faces move and jump as time passes. Really cool, nice presentation.
It would be really interesting to try to use some identity detection to try to guess who all these people are. Also have you considered changing your cron so that it saves the html so you can better search it? (Or maybe a pdf or something) I wuold also like to see this plotted across time
This is awesome! This simple plotting reveals so much – from grid structures, to kinds of faces the newspapers show. Navigation, like you said, it’s hard to tell what newspapers are on or off. Since the images are overlapping, it might be interesting to see a heat map of how many images are in one location.
So many faces! I am also interested in the gender/age thing.. I think having this numbers could reveal a lot about the papers. I like the loading phase of the application because it shows how time passes and how news change.. what becomes important in a particular moment might be forgotten a few months later. Nice work!
Comments from PiratePad B:
Nice to have mentioned your inspiration from Ruthorford Chang’s NYT face project.
Nice mentions of the technologies you mentioned. Truly freestyle computing.
I was very interested by the finding that the Australian newspaper have more faces. I’m wondering if there are regional differences in facial expressions. Your ideas about time-based display are good.
I like how you explain the multiple iterations you went through for this project in your presentation
great data set…great interface, could be a little cleaner but this is a great project. Could be interesting to also have a toggle to spread them out on a grid according to size or something
Interesting to see when clusters of photos appear – kinda gives insight into news page layout patterns.
I liked the false positives of text that appear to be abstract faces.
Those toggle buttons need indicators – as you showed us. The zoom-out that you mentioned would be nice – I know that can be done in Flash with a vCam as3 (Though I don’t know exactly how you implemented this project).
Great idea, Agree w/the green above me; indicate those toggles!!! There’s too much going on not to have some kind of indicator of on or off, otherwise it’s very confusing as to where the faces are coming from.
Side note: I find it really funny that The Huffington Post example that you showed us is just a false positive and a few pictures of Sarah Palin. Also interesting to see that the wall street journal’s faces are like 50% hand-drawn (again, from the small snippet of the bajillions of pixels you have available for viewing).
I love this! I like how you can start to understand the grid structure and stategie of the image placement. The design is a bit cluncky but the outcome is still really interesting.