2 * Nude.js - Nudity detection with Javascript and HTMLCanvas
4 * Author: Patrick Wied ( http://www.patrick-wied.at )
5 * Version: 0.1 (2010-11-21)
9 Array.prototype.remove = function(index) {
10 var rest = this.slice(index + 1);
12 return this.push.apply(this, rest);
15 var nude = (function(){
16 // private var definition
23 initCanvas = function(){
24 canvas = document.createElement("canvas");
25 // the canvas should not be visible
26 canvas.style.display = "none";
27 var b = document.getElementsByTagName("body")[0];
28 b.appendChild(canvas);
29 ctx = canvas.getContext("2d");
31 loadImageById = function(id){
33 var img = document.getElementById(id);
34 // apply the width and height to the canvas element
35 canvas.width = img.width;
36 canvas.height = img.height;
37 // reset the result function
39 // draw the image into the canvas element
40 ctx.drawImage(img, 0, 0);
43 loadImageByElement = function(element){
44 // apply width and height to the canvas element
45 // make sure you set width and height at the element
46 canvas.width = element.width;
47 canvas.height = element.height;
48 // reset result function
50 // draw the image/video element into the canvas
51 ctx.drawImage(element, 0, 0);
53 scanImage = function(){
55 var image = ctx.getImageData(0, 0, canvas.width, canvas.height),
56 imageData = image.data,
65 var addMerge = function(from, to){
68 var len = mergeRegions.length,
75 var region = mergeRegions[len],
80 if(region[rlen] == from){
84 if(region[rlen] == to){
92 if(fromIndex != -1 && toIndex != -1 && fromIndex == toIndex){
96 if(fromIndex == -1 && toIndex == -1){
98 mergeRegions.push([from, to]);
102 if(fromIndex != -1 && toIndex == -1){
104 mergeRegions[fromIndex].push(to);
107 if(fromIndex == -1 && toIndex != -1){
108 mergeRegions[toIndex].push(from);
112 if(fromIndex != -1 && toIndex != -1 && fromIndex != toIndex){
113 mergeRegions[fromIndex] = mergeRegions[fromIndex].concat(mergeRegions[toIndex]);
114 mergeRegions.remove(toIndex);
120 // iterate the image from the top left to the bottom right
121 var length = imageData.length,
122 width = canvas.width;
124 for(var i = 0, u = 1; i < length; i+=4, u++){
126 var r = imageData[i],
129 x = (u>width)?((u%width)-1):u,
130 y = (u>width)?(Math.ceil(u/width)-1):1;
132 if(classifySkin(r, g, b)){ //
133 skinMap.push({"id": u, "skin": true, "region": 0, "x": x, "y": y, "checked": false});
136 checkIndexes = [u-2, (u-width)-2, u-width-1, (u-width)],
139 for(var o = 0; o < 4; o++){
140 var index = checkIndexes[o];
141 if(skinMap[index] && skinMap[index].skin){
142 if(skinMap[index].region!=region && region!=-1 && lastFrom!=region && lastTo!=skinMap[index].region){
143 addMerge(region, skinMap[index].region);
145 region = skinMap[index].region;
151 skinMap[u-1].region = detectedRegions.length;
152 detectedRegions.push([skinMap[u-1]]);
158 if(!detectedRegions[region]){
159 detectedRegions[region] = [];
162 skinMap[u-1].region = region;
163 detectedRegions[region].push(skinMap[u-1]);
169 skinMap.push({"id": u, "skin": false, "region": 0, "x": x, "y": y, "checked": false});
174 merge(detectedRegions, mergeRegions);
177 // function for merging detected regions
178 merge = function(detectedRegions, mergeRegions){
180 var length = mergeRegions.length,
184 // merging detected regions
187 var region = mergeRegions[length],
188 rlen = region.length;
190 if(!detRegions[length])
191 detRegions[length] = [];
194 var index = region[rlen];
195 detRegions[length] = detRegions[length].concat(detectedRegions[index]);
196 detectedRegions[index] = [];
201 // push the rest of the regions to the detRegions array
202 // (regions without merging)
203 var l = detectedRegions.length;
205 if(detectedRegions[l].length > 0){
206 detRegions.push(detectedRegions[l]);
211 clearRegions(detRegions);
215 // only pushes regions which are bigger than a specific amount to the final result
216 clearRegions = function(detectedRegions){
218 var length = detectedRegions.length;
220 for(var i=0; i < length; i++){
221 if(detectedRegions[i].length > 30){
222 skinRegions.push(detectedRegions[i]);
227 analyseRegions = function(){
229 // sort the detected regions by size
230 var length = skinRegions.length,
231 totalPixels = canvas.width * canvas.height,
234 // if there are less than 3 regions
236 resultHandler(false);
240 // sort the skinRegions with bubble sort algorithm
245 for(var i = 0; i < length-1; i++){
246 if(skinRegions[i].length < skinRegions[i+1].length){
248 var temp = skinRegions[i];
249 skinRegions[i] = skinRegions[i+1];
250 skinRegions[i+1] = temp;
256 // count total skin pixels
258 totalSkin += skinRegions[length].length;
261 // check if there are more than 15% skin pixel in the image
262 if((totalSkin/totalPixels)*100 < 15){
263 // if the percentage lower than 15, it's not nude!
264 //console.log("it's not nude :) - total skin percent is "+((totalSkin/totalPixels)*100)+"% ");
265 resultHandler(false);
270 // check if the largest skin region is less than 35% of the total skin count
271 // AND if the second largest region is less than 30% of the total skin count
272 // AND if the third largest region is less than 30% of the total skin count
273 if((skinRegions[0].length/totalSkin)*100 < 35
274 && (skinRegions[1].length/totalSkin)*100 < 30
275 && (skinRegions[2].length/totalSkin)*100 < 30){
276 // the image is not nude.
277 //console.log("it's not nude :) - less than 35%,30%,30% skin in the biggest areas :" + ((skinRegions[0].length/totalSkin)*100) + "%, " + ((skinRegions[1].length/totalSkin)*100)+"%, "+((skinRegions[2].length/totalSkin)*100)+"%");
278 resultHandler(false);
283 // check if the number of skin pixels in the largest region is less than 45% of the total skin count
284 if((skinRegions[0].length/totalSkin)*100 < 45){
286 //console.log("it's not nude :) - the biggest region contains less than 45%: "+((skinRegions[0].length/totalSkin)*100)+"%");
287 resultHandler(false);
292 // build the bounding polygon by the regions edge values:
293 // Identify the leftmost, the uppermost, the rightmost, and the lowermost skin pixels of the three largest skin regions.
294 // Use these points as the corner points of a bounding polygon.
297 // check if the total skin count is less than 30% of the total number of pixels
298 // AND the number of skin pixels within the bounding polygon is less than 55% of the size of the polygon
299 // if this condition is true, it's not nude.
301 // TODO: include bounding polygon functionality
302 // if there are more than 60 skin regions and the average intensity within the polygon is less than 0.25
303 // the image is not nude
304 if(skinRegions.length > 60){
305 //console.log("it's not nude :) - more than 60 skin regions");
306 resultHandler(false);
311 // otherwise it is nude
315 // the result handler will be executed when the analysing process is done
316 // the result contains true (it is nude) or false (it is not nude)
317 // if the user passed an result function to the scan function, the result function will be executed
318 // otherwise the default resulthandling executes
319 resultHandler = function(result){
325 console.log("the picture contains nudity");
329 // colorizeRegions function is for testdevelopment only
330 // the detected skinRegions will be painted in random colors (one color per region)
331 colorizeRegions = function(){
333 var length = skinRegions.length;
334 for(var i = 0; i < length; i++){
336 var region = skinRegions[i],
337 regionLength = region.length,
338 randR = Math.ceil(Math.random()*255),
339 randG = Math.ceil(Math.random()*255),
340 rangB = Math.ceil(Math.random()*255);
342 for(var o = 0; o < regionLength; o++){
344 var pixel = ctx.getImageData(region[o].x, region[o].y, 1,1),
353 ctx.putImageData(pixel, region[o].x, region[o].y);
360 classifySkin = function(r, g, b){
361 // A Survey on Pixel-Based Skin Color Detection Techniques
362 var rgbClassifier = ((r>95) && (g>40 && g <100) && (b>20) && ((Math.max(r,g,b) - Math.min(r,g,b)) > 15) && (Math.abs(r-g)>15) && (r > g) && (r > b)),
363 nurgb = toNormalizedRgb(r, g, b),
367 normRgbClassifier = (((nr/ng)>1.185) && (((r*b)/(Math.pow(r+g+b,2))) > 0.107) && (((r*g)/(Math.pow(r+g+b,2))) > 0.112)),
368 //hsv = toHsv(r, g, b),
371 //hsvClassifier = (h < 50 && h > 0 && s > 0.23 && s < 0.68);
372 hsv = toHsvTest(r, g, b),
375 hsvClassifier = (h > 0 && h < 35 && s > 0.23 && s < 0.68);
379 ycc = toYcc(r, g, b),
383 yccClassifier = ((y > 80) && (cb > 77 && cb < 127) && (cr > 133 && cr < 173));
386 return (rgbClassifier || normRgbClassifier || hsvClassifier); //
388 toYcc = function(r, g, b){
389 r/=255,g/=255,b/=255;
390 var y = 0.299*r + 0.587*g + 0.114*b,
396 toHsv = function(r, g, b){
399 Math.acos((0.5*((r-g)+(r-b)))/(Math.sqrt((Math.pow((r-g),2)+((r-b)*(g-b)))))),
401 1-(3*((Math.min(r,g,b))/(r+g+b))),
406 toHsvTest = function(r, g, b){
408 mx = Math.max(r, g, b),
409 mn = Math.min(r, g, b),
424 return [h, 1-(3*((Math.min(r,g,b))/(r+g+b))),(1/3)*(r+g+b)] ;
427 toNormalizedRgb = function(r, g, b){
429 return [(r/sum), (g/sum), (b/sum)];
437 load: function(param){
438 if(typeof(param) == "string"){
439 loadImageById(param);
441 loadImageByElement(param);
445 if(arguments.length>0 && typeof(arguments[0]) == "function"){
452 // register nude at window object