First commit; calling this version 0.1
[imgurfusker.git] / noworker.nude.js
blobaa64cea5c3006fccecc9073868f49888b8a28210
1 /*
2  * Nude.js - Nudity detection with Javascript and HTMLCanvas
3  * 
4  * Author: Patrick Wied ( http://www.patrick-wied.at )
5  * Version: 0.1  (2010-11-21)
6  * License: MIT License
7  */
8 (function(){
9         Array.prototype.remove = function(index) {
10                   var rest = this.slice(index + 1);
11                   this.length = index;
12                   return this.push.apply(this, rest);
13         };
14         
15         var nude = (function(){
16                 // private var definition
17                 var canvas = null,
18                 ctx = null,
19                 skinRegions = [],
20                 resultFn = null,
21                 img = null,
22                 // private functions
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");
30                 },
31                 loadImageById = function(id){
32                         // get the image
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
38                         resultFn = null;
39                         // draw the image into the canvas element
40                         ctx.drawImage(img, 0, 0);
42                 },
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
49                         resultFn = null;
50                         // draw the image/video element into the canvas
51                         ctx.drawImage(element, 0, 0);
52                 },
53                 scanImage = function(){
54                         // get the image data
55                         var image = ctx.getImageData(0, 0, canvas.width, canvas.height),
56                         imageData = image.data,
57                         skinMap = [],
58                         detectedRegions = [],
59                         mergeRegions = [],
60                         width = canvas.width,
61                         lastFrom = -1,
62                         lastTo = -1;
63         
64                                 
65                         var addMerge = function(from, to){
66                                 lastFrom = from;
67                                 lastTo = to;
68                                 var len = mergeRegions.length,
69                                 fromIndex = -1,
70                                 toIndex = -1;
71                                 
72                                 
73                                 while(len--){
74                                 
75                                         var region = mergeRegions[len],
76                                         rlen = region.length;
77                                         
78                                         while(rlen--){
79                                         
80                                                 if(region[rlen] == from){
81                                                         fromIndex = len;
82                                                 }
83                                                 
84                                                 if(region[rlen] == to){
85                                                         toIndex = len;
86                                                 }
87                                                                                                 
88                                         }
89                                         
90                                 }
91                         
92                                 if(fromIndex != -1 && toIndex != -1 && fromIndex == toIndex){
93                                         return;
94                                 }
95         
96                                 if(fromIndex == -1 && toIndex == -1){
98                                         mergeRegions.push([from, to]);
99                 
100                                         return;
101                                 }
102                                 if(fromIndex != -1 && toIndex == -1){
104                                         mergeRegions[fromIndex].push(to);
105                                         return;
106                                 }
107                                 if(fromIndex == -1 && toIndex != -1){
108                                         mergeRegions[toIndex].push(from);
109                                         return;
110                                 }
111                                 
112                                 if(fromIndex != -1 && toIndex != -1 && fromIndex != toIndex){
113                                         mergeRegions[fromIndex] = mergeRegions[fromIndex].concat(mergeRegions[toIndex]);
114                                         mergeRegions.remove(toIndex);
115                                         return;
116                                 }
118                         };
119                         
120                         // iterate the image from the top left to the bottom right
121                         var length = imageData.length,
122                         width = canvas.width;
123                         
124                         for(var i = 0, u = 1; i < length; i+=4, u++){
125                                 
126                                 var r = imageData[i],
127                                 g = imageData[i+1],
128                                 b = imageData[i+2],
129                                 x = (u>width)?((u%width)-1):u,
130                                 y = (u>width)?(Math.ceil(u/width)-1):1;
131                                 
132                                 if(classifySkin(r, g, b)){ // 
133                                         skinMap.push({"id": u, "skin": true, "region": 0, "x": x, "y": y, "checked": false});
134                                         
135                                         var region = -1,
136                                         checkIndexes = [u-2, (u-width)-2, u-width-1, (u-width)],
137                                         checker = false;
138                                         
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);
144                                                         }
145                                                         region = skinMap[index].region;
146                                                         checker = true;
147                                                 }
148                                         }
150                                         if(!checker){
151                                                 skinMap[u-1].region = detectedRegions.length;
152                                                 detectedRegions.push([skinMap[u-1]]);
153                                                 continue;
154                                         }else{
155                                                 
156                                                 if(region > -1){
157                                                         
158                                                         if(!detectedRegions[region]){
159                                                                 detectedRegions[region] = [];
160                                                         }
161                 
162                                                         skinMap[u-1].region = region;                                   
163                                                         detectedRegions[region].push(skinMap[u-1]);
165                                                 }
166                                         }
167                                         
168                                 }else{
169                                         skinMap.push({"id": u, "skin": false, "region": 0, "x": x, "y": y, "checked": false});
170                                 }
171                         
172                         }
174                         merge(detectedRegions, mergeRegions);
175                         analyseRegions();
176                 },
177                 // function for merging detected regions
178                 merge = function(detectedRegions, mergeRegions){
179                         
180                         var length = mergeRegions.length,
181                         detRegions = [];
182                         
183                         
184                         // merging detected regions 
185                         while(length--){
186                                 
187                                 var region = mergeRegions[length],
188                                 rlen = region.length;
190                                 if(!detRegions[length])
191                                         detRegions[length] = [];
193                                 while(rlen--){
194                                         var index = region[rlen];
195                                         detRegions[length] = detRegions[length].concat(detectedRegions[index]);
196                                         detectedRegions[index] = [];
197                                 }
199                         }
200                         
201                         // push the rest of the regions to the detRegions array
202                         // (regions without merging)
203                         var l = detectedRegions.length;
204                         while(l--){
205                                 if(detectedRegions[l].length > 0){
206                                         detRegions.push(detectedRegions[l]);
207                                 }
208                         }
209                         
210                         // clean up
211                         clearRegions(detRegions);
212                         
213                 },
214                 // clean up function
215                 // only pushes regions which are bigger than a specific amount to the final result
216                 clearRegions = function(detectedRegions){
217                         
218                         var length = detectedRegions.length;
219                         
220                         for(var i=0; i < length; i++){
221                                 if(detectedRegions[i].length > 30){
222                                         skinRegions.push(detectedRegions[i]);
223                                 }
224                         }
225                         
226                 },
227                 analyseRegions = function(){
228                         
229                         // sort the detected regions by size
230                         var length = skinRegions.length,
231                         totalPixels = canvas.width * canvas.height,
232                         totalSkin = 0;
233                         
234                         // if there are less than 3 regions
235                         if(length < 3){
236                                 resultHandler(false);
237                                 return;
238                         }
239                         
240                         // sort the skinRegions with bubble sort algorithm
241                         (function(){ 
242                                 var sorted = false;
243                                 while(!sorted){
244                                         sorted = true;
245                                         for(var i = 0; i < length-1; i++){
246                                                 if(skinRegions[i].length < skinRegions[i+1].length){
247                                                         sorted = false;
248                                                         var temp = skinRegions[i];
249                                                         skinRegions[i] = skinRegions[i+1];
250                                                         skinRegions[i+1] = temp;
251                                                 }
252                                         }
253                                 }
254                         })();
255                         
256                         // count total skin pixels
257                         while(length--){
258                                 totalSkin += skinRegions[length].length;
259                         }
260                         
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);
266                                 return;                         
267                         }
268                         
269                         
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);
279                                 return;
280                                 
281                         }
282                         
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){
285                                 // it's not nude
286                                 //console.log("it's not nude :) - the biggest region contains less than 45%: "+((skinRegions[0].length/totalSkin)*100)+"%");
287                                 resultHandler(false);
288                                 return;
289                         }
290                         
291                         // TODO:
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.
295                         
296                         // TODO:
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.
300                         
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);
307                                 return;
308                         }
309                         
310                         
311                         // otherwise it is nude
312                         resultHandler(true);
313                                                 
314                 },
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){
320                         
321                         if(resultFn){
322                                 resultFn(result);
323                         }else{
324                                 if(result)
325                                         console.log("the picture contains nudity");
326                         }
328                 },
329                 // colorizeRegions function is for testdevelopment only
330                 // the detected skinRegions will be painted in random colors (one color per region)
331                 colorizeRegions = function(){
332                         
333                         var length = skinRegions.length;
334                         for(var i = 0; i < length; i++){
335                                 
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);
341                                 
342                                 for(var o = 0; o < regionLength; o++){
343                                         
344                                         var pixel = ctx.getImageData(region[o].x, region[o].y, 1,1),
345                                         pdata = pixel.data;
346                                         
347                                         pdata[0] = randR;
348                                         pdata[1] = randG;
349                                         pdata[2] = rangB;
350                                         
351                                         pixel.data = pdata;
352                                         
353                                         ctx.putImageData(pixel, region[o].x, region[o].y);
354                                         
355                                 }
356                                 
357                         }
358                         
359                 }, 
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),
364                         nr = nurgb[0],
365                         ng = nurgb[1],
366                         nb = nurgb[2],
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),
369                         //h = hsv[0]*100,
370                         //s = hsv[1],
371                         //hsvClassifier = (h < 50 && h > 0 && s > 0.23 && s < 0.68);
372                         hsv = toHsvTest(r, g, b),
373                         h = hsv[0],
374                         s = hsv[1],
375                         hsvClassifier = (h > 0 && h < 35 && s > 0.23 && s < 0.68);
376                         /*
377                          * ycc doesnt work
378                          
379                         ycc = toYcc(r, g, b),
380                         y = ycc[0],
381                         cb = ycc[1],
382                         cr = ycc[2],
383                         yccClassifier = ((y > 80) && (cb > 77 && cb < 127) && (cr > 133 && cr < 173));
384                         */
385                         
386                         return (rgbClassifier || normRgbClassifier || hsvClassifier); // 
387                 },
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,
391                         cr = r - y,
392                         cb = b - y;
393                         
394                         return [y, cr, cb];
395                 },
396                 toHsv = function(r, g, b){
397                         return [
398                                 // hue
399                                 Math.acos((0.5*((r-g)+(r-b)))/(Math.sqrt((Math.pow((r-g),2)+((r-b)*(g-b)))))),
400                                 // saturation
401                                 1-(3*((Math.min(r,g,b))/(r+g+b))),
402                                 // value
403                                 (1/3)*(r+g+b)
404                                 ];
405                 },
406                 toHsvTest = function(r, g, b){
407                         var h = 0,
408                         mx = Math.max(r, g, b),
409                         mn = Math.min(r, g, b),
410                         dif = mx - mn;
411                         
412                         if(mx == r){
413                                 h = (g - b)/dif;
414                         }else if(mx == g){
415                                 h = 2+((g - r)/dif)
416                         }else{
417                                 h = 4+((r - g)/dif);
418                         }
419                         h = h*60;
420                         if(h < 0){
421                                 h = h+360;
422                         }
423                         
424                         return [h, 1-(3*((Math.min(r,g,b))/(r+g+b))),(1/3)*(r+g+b)] ;   
425                         
426                 },
427                 toNormalizedRgb = function(r, g, b){
428                         var sum = r+g+b;
429                         return [(r/sum), (g/sum), (b/sum)];
430                 };
431                 
432                 // public interface
433                 return {
434                         init: function(){
435                                 initCanvas();
436                         },
437                         load: function(param){
438                                 if(typeof(param) == "string"){
439                                         loadImageById(param);
440                                 }else{
441                                         loadImageByElement(param);
442                                 }
443                         },
444                         scan: function(fn){
445                                 if(arguments.length>0 && typeof(arguments[0]) == "function"){
446                                         resultFn = fn;
447                                 }
448                                 scanImage();
449                         }
450                 };
451         })();
452         // register nude at window object
453         window.nude = nude;
454         nude.init();
455 })();