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