PageRenderTime 247ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/noworker.nude.js

http://github.com/pa7/nude.js
JavaScript | 456 lines | 296 code | 80 blank | 80 comment | 81 complexity | 21434d1111e4df06e061c4959821a250 MD5 | raw file
  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. var nude = (function(){
  15. // private var definition
  16. var canvas = null,
  17. ctx = null,
  18. skinRegions = [],
  19. resultFn = null,
  20. img = null,
  21. // private functions
  22. initCanvas = function(){
  23. canvas = document.createElement("canvas");
  24. // the canvas should not be visible
  25. canvas.style.display = "none";
  26. var b = document.getElementsByTagName("body")[0];
  27. b.appendChild(canvas);
  28. ctx = canvas.getContext("2d");
  29. },
  30. loadImageById = function(id){
  31. // get the image
  32. var img = document.getElementById(id);
  33. // apply the width and height to the canvas element
  34. canvas.width = img.width;
  35. canvas.height = img.height;
  36. // reset the result function
  37. resultFn = null;
  38. // draw the image into the canvas element
  39. ctx.drawImage(img, 0, 0);
  40. },
  41. loadImageByElement = function(element){
  42. // apply width and height to the canvas element
  43. // make sure you set width and height at the element
  44. canvas.width = element.width;
  45. canvas.height = element.height;
  46. // reset result function
  47. resultFn = null;
  48. // draw the image/video element into the canvas
  49. ctx.drawImage(element, 0, 0);
  50. },
  51. scanImage = function(){
  52. // get the image data
  53. var image = ctx.getImageData(0, 0, canvas.width, canvas.height),
  54. imageData = image.data,
  55. skinMap = [],
  56. detectedRegions = [],
  57. mergeRegions = [],
  58. width = canvas.width,
  59. lastFrom = -1,
  60. lastTo = -1;
  61. var addMerge = function(from, to){
  62. lastFrom = from;
  63. lastTo = to;
  64. var len = mergeRegions.length,
  65. fromIndex = -1,
  66. toIndex = -1;
  67. while(len--){
  68. var region = mergeRegions[len],
  69. rlen = region.length;
  70. while(rlen--){
  71. if(region[rlen] == from){
  72. fromIndex = len;
  73. }
  74. if(region[rlen] == to){
  75. toIndex = len;
  76. }
  77. }
  78. }
  79. if(fromIndex != -1 && toIndex != -1 && fromIndex == toIndex){
  80. return;
  81. }
  82. if(fromIndex == -1 && toIndex == -1){
  83. mergeRegions.push([from, to]);
  84. return;
  85. }
  86. if(fromIndex != -1 && toIndex == -1){
  87. mergeRegions[fromIndex].push(to);
  88. return;
  89. }
  90. if(fromIndex == -1 && toIndex != -1){
  91. mergeRegions[toIndex].push(from);
  92. return;
  93. }
  94. if(fromIndex != -1 && toIndex != -1 && fromIndex != toIndex){
  95. mergeRegions[fromIndex] = mergeRegions[fromIndex].concat(mergeRegions[toIndex]);
  96. mergeRegions.remove(toIndex);
  97. return;
  98. }
  99. };
  100. // iterate the image from the top left to the bottom right
  101. var length = imageData.length,
  102. width = canvas.width;
  103. for(var i = 0, u = 1; i < length; i+=4, u++){
  104. var r = imageData[i],
  105. g = imageData[i+1],
  106. b = imageData[i+2],
  107. x = (u>width)?((u%width)-1):u,
  108. y = (u>width)?(Math.ceil(u/width)-1):1;
  109. if(classifySkin(r, g, b)){ //
  110. skinMap.push({"id": u, "skin": true, "region": 0, "x": x, "y": y, "checked": false});
  111. var region = -1,
  112. checkIndexes = [u-2, (u-width)-2, u-width-1, (u-width)],
  113. checker = false;
  114. for(var o = 0; o < 4; o++){
  115. var index = checkIndexes[o];
  116. if(skinMap[index] && skinMap[index].skin){
  117. if(skinMap[index].region!=region && region!=-1 && lastFrom!=region && lastTo!=skinMap[index].region){
  118. addMerge(region, skinMap[index].region);
  119. }
  120. region = skinMap[index].region;
  121. checker = true;
  122. }
  123. }
  124. if(!checker){
  125. skinMap[u-1].region = detectedRegions.length;
  126. detectedRegions.push([skinMap[u-1]]);
  127. continue;
  128. }else{
  129. if(region > -1){
  130. if(!detectedRegions[region]){
  131. detectedRegions[region] = [];
  132. }
  133. skinMap[u-1].region = region;
  134. detectedRegions[region].push(skinMap[u-1]);
  135. }
  136. }
  137. }else{
  138. skinMap.push({"id": u, "skin": false, "region": 0, "x": x, "y": y, "checked": false});
  139. }
  140. }
  141. merge(detectedRegions, mergeRegions);
  142. analyseRegions();
  143. },
  144. // function for merging detected regions
  145. merge = function(detectedRegions, mergeRegions){
  146. var length = mergeRegions.length,
  147. detRegions = [];
  148. // merging detected regions
  149. while(length--){
  150. var region = mergeRegions[length],
  151. rlen = region.length;
  152. if(!detRegions[length])
  153. detRegions[length] = [];
  154. while(rlen--){
  155. var index = region[rlen];
  156. detRegions[length] = detRegions[length].concat(detectedRegions[index]);
  157. detectedRegions[index] = [];
  158. }
  159. }
  160. // push the rest of the regions to the detRegions array
  161. // (regions without merging)
  162. var l = detectedRegions.length;
  163. while(l--){
  164. if(detectedRegions[l].length > 0){
  165. detRegions.push(detectedRegions[l]);
  166. }
  167. }
  168. // clean up
  169. clearRegions(detRegions);
  170. },
  171. // clean up function
  172. // only pushes regions which are bigger than a specific amount to the final result
  173. clearRegions = function(detectedRegions){
  174. var length = detectedRegions.length;
  175. for(var i=0; i < length; i++){
  176. if(detectedRegions[i].length > 30){
  177. skinRegions.push(detectedRegions[i]);
  178. }
  179. }
  180. },
  181. analyseRegions = function(){
  182. // sort the detected regions by size
  183. var length = skinRegions.length,
  184. totalPixels = canvas.width * canvas.height,
  185. totalSkin = 0;
  186. // if there are less than 3 regions
  187. if(length < 3){
  188. resultHandler(false);
  189. return;
  190. }
  191. // sort the skinRegions with bubble sort algorithm
  192. (function(){
  193. var sorted = false;
  194. while(!sorted){
  195. sorted = true;
  196. for(var i = 0; i < length-1; i++){
  197. if(skinRegions[i].length < skinRegions[i+1].length){
  198. sorted = false;
  199. var temp = skinRegions[i];
  200. skinRegions[i] = skinRegions[i+1];
  201. skinRegions[i+1] = temp;
  202. }
  203. }
  204. }
  205. })();
  206. // count total skin pixels
  207. while(length--){
  208. totalSkin += skinRegions[length].length;
  209. }
  210. // check if there are more than 15% skin pixel in the image
  211. if((totalSkin/totalPixels)*100 < 15){
  212. // if the percentage lower than 15, it's not nude!
  213. //console.log("it's not nude :) - total skin percent is "+((totalSkin/totalPixels)*100)+"% ");
  214. resultHandler(false);
  215. return;
  216. }
  217. // check if the largest skin region is less than 35% of the total skin count
  218. // AND if the second largest region is less than 30% of the total skin count
  219. // AND if the third largest region is less than 30% of the total skin count
  220. if((skinRegions[0].length/totalSkin)*100 < 35
  221. && (skinRegions[1].length/totalSkin)*100 < 30
  222. && (skinRegions[2].length/totalSkin)*100 < 30){
  223. // the image is not nude.
  224. //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)+"%");
  225. resultHandler(false);
  226. return;
  227. }
  228. // check if the number of skin pixels in the largest region is less than 45% of the total skin count
  229. if((skinRegions[0].length/totalSkin)*100 < 45){
  230. // it's not nude
  231. //console.log("it's not nude :) - the biggest region contains less than 45%: "+((skinRegions[0].length/totalSkin)*100)+"%");
  232. resultHandler(false);
  233. return;
  234. }
  235. // TODO:
  236. // build the bounding polygon by the regions edge values:
  237. // Identify the leftmost, the uppermost, the rightmost, and the lowermost skin pixels of the three largest skin regions.
  238. // Use these points as the corner points of a bounding polygon.
  239. // TODO:
  240. // check if the total skin count is less than 30% of the total number of pixels
  241. // AND the number of skin pixels within the bounding polygon is less than 55% of the size of the polygon
  242. // if this condition is true, it's not nude.
  243. // TODO: include bounding polygon functionality
  244. // if there are more than 60 skin regions and the average intensity within the polygon is less than 0.25
  245. // the image is not nude
  246. if(skinRegions.length > 60){
  247. //console.log("it's not nude :) - more than 60 skin regions");
  248. resultHandler(false);
  249. return;
  250. }
  251. // otherwise it is nude
  252. resultHandler(true);
  253. },
  254. // the result handler will be executed when the analysing process is done
  255. // the result contains true (it is nude) or false (it is not nude)
  256. // if the user passed an result function to the scan function, the result function will be executed
  257. // otherwise the default resulthandling executes
  258. resultHandler = function(result){
  259. if(resultFn){
  260. resultFn(result);
  261. }else{
  262. if(result)
  263. console.log("the picture contains nudity");
  264. }
  265. },
  266. // colorizeRegions function is for testdevelopment only
  267. // the detected skinRegions will be painted in random colors (one color per region)
  268. colorizeRegions = function(){
  269. var length = skinRegions.length;
  270. for(var i = 0; i < length; i++){
  271. var region = skinRegions[i],
  272. regionLength = region.length,
  273. randR = Math.ceil(Math.random()*255),
  274. randG = Math.ceil(Math.random()*255),
  275. rangB = Math.ceil(Math.random()*255);
  276. for(var o = 0; o < regionLength; o++){
  277. var pixel = ctx.getImageData(region[o].x, region[o].y, 1,1),
  278. pdata = pixel.data;
  279. pdata[0] = randR;
  280. pdata[1] = randG;
  281. pdata[2] = rangB;
  282. pixel.data = pdata;
  283. ctx.putImageData(pixel, region[o].x, region[o].y);
  284. }
  285. }
  286. },
  287. classifySkin = function(r, g, b){
  288. // A Survey on Pixel-Based Skin Color Detection Techniques
  289. 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)),
  290. nurgb = toNormalizedRgb(r, g, b),
  291. nr = nurgb[0],
  292. ng = nurgb[1],
  293. nb = nurgb[2],
  294. 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)),
  295. //hsv = toHsv(r, g, b),
  296. //h = hsv[0]*100,
  297. //s = hsv[1],
  298. //hsvClassifier = (h < 50 && h > 0 && s > 0.23 && s < 0.68);
  299. hsv = toHsvTest(r, g, b),
  300. h = hsv[0],
  301. s = hsv[1],
  302. hsvClassifier = (h > 0 && h < 35 && s > 0.23 && s < 0.68);
  303. /*
  304. * ycc doesnt work
  305. ycc = toYcc(r, g, b),
  306. y = ycc[0],
  307. cb = ycc[1],
  308. cr = ycc[2],
  309. yccClassifier = ((y > 80) && (cb > 77 && cb < 127) && (cr > 133 && cr < 173));
  310. */
  311. return (rgbClassifier || normRgbClassifier || hsvClassifier); //
  312. },
  313. toYcc = function(r, g, b){
  314. r/=255,g/=255,b/=255;
  315. var y = 0.299*r + 0.587*g + 0.114*b,
  316. cr = r - y,
  317. cb = b - y;
  318. return [y, cr, cb];
  319. },
  320. toHsv = function(r, g, b){
  321. return [
  322. // hue
  323. Math.acos((0.5*((r-g)+(r-b)))/(Math.sqrt((Math.pow((r-g),2)+((r-b)*(g-b)))))),
  324. // saturation
  325. 1-(3*((Math.min(r,g,b))/(r+g+b))),
  326. // value
  327. (1/3)*(r+g+b)
  328. ];
  329. },
  330. toHsvTest = function(r, g, b){
  331. var h = 0,
  332. mx = Math.max(r, g, b),
  333. mn = Math.min(r, g, b),
  334. dif = mx - mn;
  335. if(mx == r){
  336. h = (g - b)/dif;
  337. }else if(mx == g){
  338. h = 2+((g - r)/dif)
  339. }else{
  340. h = 4+((r - g)/dif);
  341. }
  342. h = h*60;
  343. if(h < 0){
  344. h = h+360;
  345. }
  346. return [h, 1-(3*((Math.min(r,g,b))/(r+g+b))),(1/3)*(r+g+b)] ;
  347. },
  348. toNormalizedRgb = function(r, g, b){
  349. var sum = r+g+b;
  350. return [(r/sum), (g/sum), (b/sum)];
  351. };
  352. // public interface
  353. return {
  354. init: function(){
  355. initCanvas();
  356. },
  357. load: function(param){
  358. if(typeof(param) == "string"){
  359. loadImageById(param);
  360. }else{
  361. loadImageByElement(param);
  362. }
  363. },
  364. scan: function(fn){
  365. if(arguments.length>0 && typeof(arguments[0]) == "function"){
  366. resultFn = fn;
  367. }
  368. scanImage();
  369. }
  370. };
  371. })();
  372. // register nude at window object
  373. window.nude = nude;
  374. nude.init();
  375. })();