/nude.js

https://gitlab.com/qb1t/node-nude · JavaScript · 226 lines · 221 code · 2 blank · 3 comment · 86 complexity · a6cdfb3f9438360d6e58fa822e288475 MD5 · raw file

  1. var Canvas = require('canvas'),
  2. fs = require('fs'),
  3. http = require('http'),
  4. https = require('https'),
  5. url = require('url'),
  6. Image = Canvas.Image;
  7. var classifySkin = function(r, g, b) {
  8. 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,
  9. nurgb = toNormalizedRgb(r, g, b),
  10. nr = nurgb[0],
  11. ng = nurgb[1],
  12. nb = nurgb[2],
  13. 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,
  14. hsv = toHsvTest(r, g, b),
  15. h = hsv[0],
  16. s = hsv[1],
  17. hsvClassifier = h > 0 && h < 35 && s > 0.23 && s < 0.68;
  18. return rgbClassifier || normRgbClassifier || hsvClassifier;
  19. },
  20. toHsvTest = function(r, g, b) {
  21. var h = 0,
  22. mx = Math.max(r, g, b),
  23. mn = Math.min(r, g, b),
  24. dif = mx - mn;
  25. if(mx == r)
  26. h = (g - b) / dif;
  27. else if(mx == g)
  28. h = 2 + (g - r) / dif;
  29. else
  30. h = 4 + (r - g) / dif;
  31. h *= 60;
  32. if(h < 0)
  33. h += 360;
  34. return [h, 1 - 3 * (Math.min(r, g, b) / (r + g + b)), 1 / 3 * (r + g + b)];
  35. },
  36. toNormalizedRgb = function(r, g, b) {
  37. var sum = r + g + b;
  38. return [r / sum, g / sum, b / sum];
  39. };
  40. var scan = function(data, callback) {
  41. var canvas = new Canvas(1, 1),
  42. ctx = canvas.getContext('2d'),
  43. img = new Image(),
  44. skinRegions = [],
  45. skinMap = [],
  46. detectedRegions = [],
  47. mergeRegions = [],
  48. detRegions = [],
  49. lastFrom = -1,
  50. lastTo = -1,
  51. totalSkin = 0,
  52. addMerge = function(from, to) {
  53. lastFrom = from;
  54. lastTo = to;
  55. var len = mergeRegions.length,
  56. fromIndex = -1,
  57. toIndex = -1,
  58. region,
  59. rlen;
  60. while(len--) {
  61. region = mergeRegions[len];
  62. rlen = region.length;
  63. while(rlen--) {
  64. if(region[rlen] == from)
  65. fromIndex = len;
  66. if(region[rlen] == to)
  67. toIndex = len;
  68. }
  69. }
  70. if(fromIndex != -1 && toIndex != -1 && fromIndex == toIndex)
  71. return;
  72. if(fromIndex == -1 && toIndex == -1)
  73. return mergeRegions.push([from, to]);
  74. if(fromIndex != -1 && toIndex == -1)
  75. return mergeRegions[fromIndex].push(to);
  76. if(fromIndex == -1 && toIndex != -1)
  77. return mergeRegions[toIndex].push(from);
  78. if(fromIndex != -1 && toIndex != -1 && fromIndex != toIndex) {
  79. mergeRegions[fromIndex] = mergeRegions[fromIndex].concat(mergeRegions[toIndex]);
  80. mergeRegions = [].concat(mergeRegions.slice(0, toIndex), mergeRegions.slice(toIndex + 1));
  81. }
  82. },
  83. totalPixels,
  84. imageData,
  85. length;
  86. img.src = data;
  87. canvas.width = img.width;
  88. canvas.height = img.height;
  89. totalPixels = canvas.width * canvas.height;
  90. try {
  91. ctx.drawImage(img, 0, 0);
  92. } catch (e) {
  93. console.log('unsupported image format');
  94. }
  95. imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
  96. length = imageData.length;
  97. for(var i = 0, u = 1; i < length; i += 4, u++) {
  98. var r = imageData[i],
  99. g = imageData[i + 1],
  100. b = imageData[i + 2],
  101. x = u > canvas.width ? u % canvas.width - 1 : u,
  102. y = u > canvas.width ? Math.ceil(u / canvas.width) - 1 : 1;
  103. if(classifySkin(r, g, b)) {
  104. skinMap.push({id: u, skin: true, region: 0, x: x, y: y, checked: false});
  105. var region = -1,
  106. checkIndexes = [u - 2, u - canvas.width - 2, u - canvas.width - 1, u - canvas.width],
  107. checker = false;
  108. for(var o = 0, index; o < 4; o++) {
  109. index = checkIndexes[o];
  110. if(skinMap[index] && skinMap[index].skin) {
  111. if(skinMap[index].region != region && region != -1 && lastFrom != region && lastTo != skinMap[index].region)
  112. addMerge(region, skinMap[index].region);
  113. region = skinMap[index].region;
  114. checker = true;
  115. }
  116. }
  117. if(!checker) {
  118. skinMap[u - 1].region = detectedRegions.length;
  119. detectedRegions.push([skinMap[u - 1]]);
  120. continue;
  121. }
  122. else
  123. if(region > -1) {
  124. if(!detectedRegions[region])
  125. detectedRegions[region] = [];
  126. skinMap[u - 1].region = region;
  127. detectedRegions[region].push(skinMap[u - 1]);
  128. }
  129. }
  130. else
  131. skinMap.push({ id: u, skin: false, region: 0, x: x, y: y, checked: false });
  132. }
  133. length = mergeRegions.length;
  134. while(length--) {
  135. region = mergeRegions[length];
  136. var rlen = region.length;
  137. if(!detRegions[length])
  138. detRegions[length] = [];
  139. while(rlen--) {
  140. index = region[rlen];
  141. detRegions[length] = detRegions[length].concat(detectedRegions[index]);
  142. detectedRegions[index] = [];
  143. }
  144. }
  145. length = detectedRegions.length;
  146. while(length--)
  147. if(detectedRegions[length].length > 0)
  148. detRegions.push(detectedRegions[length]);
  149. length = detRegions.length;
  150. for(var i = 0; i < length; i++)
  151. if(detRegions[i].length > 30)
  152. skinRegions.push(detRegions[i]);
  153. length = skinRegions.length;
  154. if(length < 3)
  155. return callback && callback(false);
  156. (function() {
  157. var sorted = false, temp;
  158. while(!sorted) {
  159. sorted = true;
  160. for(var i = 0; i < length-1; i++)
  161. if(skinRegions[i].length < skinRegions[i + 1].length) {
  162. sorted = false;
  163. temp = skinRegions[i];
  164. skinRegions[i] = skinRegions[i + 1];
  165. skinRegions[i + 1] = temp;
  166. }
  167. }
  168. })();
  169. while(length--)
  170. totalSkin += skinRegions[length].length;
  171. if((totalSkin / totalPixels) * 100 < 15)
  172. return callback && callback(false);
  173. if((skinRegions[0].length / totalSkin) * 100 < 35 && (skinRegions[1].length / totalSkin) * 100 < 30 && (skinRegions[2].length / totalSkin) * 100 < 30)
  174. return callback && callback(false);
  175. if((skinRegions[0].length / totalSkin) * 100 < 45)
  176. return callback && callback(false);
  177. if(skinRegions.length > 60)
  178. return callback && callback(false);
  179. return callback && callback(true);
  180. }
  181. module.exports = {
  182. scanFile: function(src, callback){
  183. fs.readFile(src, function(err, data) {
  184. if (err) throw err;
  185. scan(data, callback);
  186. });
  187. },
  188. scanURL: function(src, callback){
  189. var data = [];
  190. var opts = url.parse(src);
  191. // opts.headers = {
  192. // 'Content-Type': 'image/jpeg'
  193. // };
  194. if ( src.match( 'https' ) ) {
  195. https.request(opts, function(res) {
  196. res.setEncoding('binary');
  197. res.on('data', function(chunk) {
  198. data.push(chunk.toString('binary'));
  199. });
  200. res.on('error', function(err) {
  201. throw err;
  202. });
  203. res.on('end', function() {
  204. data = new Buffer(data.join(''), 'binary');
  205. scan(data, callback);
  206. });
  207. }).end();
  208. } else {
  209. http.request(opts, function(res) {
  210. res.setEncoding('binary');
  211. res.on('data', function(chunk) {
  212. data.push(chunk.toString('binary'));
  213. });
  214. res.on('error', function(err) {
  215. throw err;
  216. });
  217. res.on('end', function() {
  218. data = new Buffer(data.join(''), 'binary');
  219. scan(data, callback);
  220. });
  221. }).end();
  222. }
  223. },
  224. };