Skip to content

Instantly share code, notes, and snippets.

@koyachi
Created March 1, 2011 12:54
Show Gist options
  • Save koyachi/849079 to your computer and use it in GitHub Desktop.
Save koyachi/849079 to your computer and use it in GitHub Desktop.
node-nude + request
var nude = require('../lib/nude').nude
, request = require('request')
, fs = require('fs');
var imageUrls = [
// 'http://s3.amazonaws.com/data.tumblr.com/tumblr_lhc3h7IAJZ1qz4c38o1_1280.jpg?AWSAccessKeyId=0RYTHV9YYQ4W5Q3HQMG2&Expires=1299059361&Signature=9cc/hHZrXZRN0i56mq2aqPjg3BE%3D',
// 'http://s3.amazonaws.com/data.tumblr.com/tumblr_lhc3g6JLhv1qz4c38o1_1280.jpg?AWSAccessKeyId=0RYTHV9YYQ4W5Q3HQMG2&Expires=1299059429&Signature=ExZMEh7c2Vdz6izJkBjl%2Bi%2BHKFk%3D',
// 'http://s3.amazonaws.com/data.tumblr.com/tumblr_lhc3fdpHoJ1qz4c38o1_1280.jpg?AWSAccessKeyId=0RYTHV9YYQ4W5Q3HQMG2&Expires=1299059438&Signature=NGr7tNzEdLpisj19eVCvr0b9SfQ%3D',
'http://www.learningtoloveyoumore.com/images/30/green_jonathan2/1.jpg',
'http://img3.wiredvision.jp/news/201102/2011022819-1.jpg',
// 'http://mrmt.tumblr.com/photo/1280/3580891217/1/tumblr_lhc3ufzOKw1qz4c38',
];
imageUrls.forEach(function(url){
var path = __dirname + '/tmp2/' + url.split('/').pop();
var wStream = fs.createWriteStream(path);
wStream.on('close', function(){
nude.load(path);
nude.scan(function(isNude){
console.log(path + ' is :' + (isNude ? 'NUDE' : 'not NUDE') + '.');
});
});
var r = request({uri: url});
r.pipe(wStream);
});
// [memo]
// - https://github.com/goddamnbugs/node-nude との違い
// - jsdom使わない
// - node-canvas使う
// - 最初と最後のnode.jsっぽいとこい以外はnude.jsほとんどそのまま
// - 元々のnude.jsのanalyseRegions内にコメント書いてあるけどバウンディングポリゴンを使った処理は省かれている
// - node-nude流行らなさそう && これ使いまわしそう or いい名前思いついたら別の名前でnpmからインストールできるようにするかも
var Canvas = require('canvas')
, Image = Canvas.Image
, fs = require('fs');
Array.prototype.remove = function(index) {
var rest = this.slice(index + 1);
this.length = index;
return this.push.apply(this, rest);
};
var canvas = null
, ctx = null
, skinRegions = []
, resultFn = null
, img = null;
function initCanvas() {
// canvas = new Canvas();
// ctx = canvas.getContext('2d');
}
//function loadImageById() {
//}
//
//function loadImageByElement() {
//}
function loadImageByUrl() {
// download image,
// create Image
// draw image to ctx
}
function loadImageByLocal(filePath) {
// load local image file,
// create Image
// draw image to ctx
var img = new Image;
img.onerror = function(err){
throw err;
};
img.onload = function(){
canvas = new Canvas(img.width, img.height);
ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
// console.log('w = ' + img.width);
// console.log('h = ' + img.height);
};
img.src = filePath;
}
function scanImage() {
// get the image data
var image = ctx.getImageData(0, 0, canvas.width, canvas.height)
, imageData = image.data
, skinMap = []
, detectedRegions = []
, mergeRegions = []
, width = canvas.width
, lastFrom = -1
, lastTo = -1;
var addMerge = function(from, to) {
lastFrom = from;
lastTo = to;
var len = mergeRegions.length
, fromIndex = -1
, toIndex = -1;
while (len--) {
var region = mergeRegions[len]
, rlen = region.length;
while (rlen--) {
if (region[rlen] == from) {
fromIndex = len;
}
if (region[rlen] == to) {
toIndex = len;
}
}
}
if (fromIndex != -1 && toIndex != -1 && fromIndex == toIndex) {
return;
}
if (fromIndex == -1 && toIndex == -1) {
mergeRegions.push([from, to]);
return;
}
if (fromIndex != -1 && toIndex == -1) {
mergeRegions[fromIndex].push(to);
return;
}
if (fromIndex == -1 && toIndex != -1) {
mergeRegions[toIndex].push(from);
return;
}
if (fromIndex != -1 && toIndex != -1 && fromIndex != toIndex) {
mergeRegions[fromIndex] = mergeRegions[fromIndex].concat(mergeRegions[toIndex]);
mergeRegions.remove(toIndex);
return;
}
};
// iterate the image from the top left to the botom right
var length = imageData.length;
for (var i = 0, u = 1; i < length; i+=4, u++) {
var r = imageData[i]
, g = imageData[i+1]
, b = imageData[i+2]
, x = (u > width) ? (u % width) - 1 : u
, y = (u > width) ? Math.ceil(u / width) - 1 : 1;
if (classifySkin(r,g,b)) {
skinMap.push({
id: u,
skin: true,
region: 0,
x: x,
y: y,
checked: false
});
var region = -1
, checkIndexes = [u -2, (u - width) - 2, u - width - 1, (u - width)]
, checker = false;
for (var o = 0; o < 4; o++) {
var index = checkIndexes[o];
if (skinMap[index] && skinMap[index].skin) {
if (skinMap[index].region != region &&
region != -1 &&
lastFrom != region &&
lastTo != skinMap[index].region)
{
addMerge(region, skinMap[index].region);
}
region = skinMap[index].region;
checker = true;
}
}
if (!checker) {
skinMap[u-1].region = detectedRegions.length;
detectedRegions.push([skinMap[u-1]]);
continue;
} else {
if (region > -1) {
if (!detectedRegions[region]){
detectedRegions[region] = [];
}
skinMap[u-1].region = region;
detectedRegions[region].push(skinMap[u-1]);
}
}
} else {
skinMap.push({
id: u,
skin: false,
region: 0,
x: x,
y: y,
checked: false
});
}
}
merge(detectedRegions, mergeRegions);
analyseRegions();
}
// function for merging detected regions
function merge(detectedRegions, mergeRegions) {
var length = mergeRegions.length
, detRegions = [];
while (length--) {
var region = mergeRegions[length]
, rlen = region.length;
if (!detRegions[length]) detRegions[length] = [];
while (rlen--) {
var index = region[rlen];
detRegions[length] = detRegions[length].concat(detectedRegions[index]);
detectedRegions[index ] = [];
}
}
// push the rest of the regions to the detRegions array
// (regions without merging)
var l = detectedRegions.length;
while (l--) {
if (detectedRegions[l].length > 0) {
detRegions.push(detectedRegions[l]);
}
}
// clean up
cleanRegions(detRegions);
}
// clean up function
// only pushes regions which are bigger than a specific amount to the final result
function cleanRegions(detectedRegions) {
var length = detectedRegions.length;
for (var i = 0; i < length; i++) {
if (detectedRegions[i].length > 30) {
skinRegions.push(detectedRegions[i]);
}
}
}
function analyseRegions() {
// sort the detected regions by size
var length = skinRegions.length
, totalPixels = canvas.width * canvas.height
, totalSkin = 0;
// if there are less than 3 regions
if (length < 3) {
resultHandler(false);
return;
}
// sort the skinRegions with bubble sort algorithm
(function(){
var sorted = false;
while (!sorted) {
sorted = true;
for (var i = 0; i < length - 1; i++) {
if (skinRegions[i].length < skinRegions[i+1].length) {
sorted = false;
var temp = skinRegions[i];
skinRegions[i] = skinRegions[i+1];
skinRegions[i+1] = temp;
}
}
}
})();
// count total skin pixels
while (length--) {
totalSkin += skinRegions[length].length;
}
// check if there are more than 15% skin pixel in the image
if ((totalSkin / totalPixels) * 100 < 15) {
// if the percentage lower than 15, it's not nude!
//console.log("it's not nude :) - total skin percent is "+((totalSkin/totalPixels)*100)+"% ");
resultHandler(false);
return;
}
// check if the largest skin region is less than 35% of the total skin count
// AND if the second largest skin region is less than 30% of the total skin count
// AND if the third largest skin region is less than 30% of the total skin count
if ((skinRegions[0].length / totalSkin) * 100 < 35 &&
(skinRegions[1].length / totalSkin) * 100 < 30 &&
(skinRegions[2].length / totalSkin) * 100 < 30)
{
// the image is not nude.
//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)+"%");
resultHandler(false);
return;
}
// check if the number of skin pixels in the largest region is less than 45% of the total skin count
if ((skinRegions[0].length / totalSkin) * 100 < 45) {
// it's not nude
//console.log("it's not nude :) - the biggest region contains less than 45%: "+((skinRegions[0].length/totalSkin)*100)+"%");
resultHandler(false);
return;
}
// TODO:
// build the bounding polygon by the regions edge values:
// Identify the leftmost, the uppermost, the rightmost, and the lowermost skin pixels of the three largest skin regions.
// Use these points as the corner points of a bouding polygn.
// TODO:
// check if the total skin count is less than 40% of the total number of pixels
// AND the number of skin pixels within the bounding polygon is less than 55% of the size of the polygon
// if this condition is true, it's not nude.
// TODO: include bounding polygon functionality
// if there are more than 60 skin regions and the average intensity within the polygon is less than 0.25
// the image is not nude
if (skinRegions.length > 60) {
//console.log("it's not nude :) - more than 60 skin regions");
resultHandler(false);
return;
}
// otherwise it is nude
resultHandler(true);
}
// the result handler will be executed when the analysing process is done
// the result contains true (it is nude) or false (it is not nude)
// if the user passed an result function to the scan function, the result function will be executed
// otherwise the default resulthandling executes
function resultHandler(result) {
if (resultFn) {
resultFn(result);
} else {
if (result) console.log("the picture contains nudity");
}
}
// colorizeRegions function is for testdevelopment only
// the detected skinRegions will be painted in random colors (one color per region)
function colorizeRegions () {
var length = skinRegions.length;
for (var i = 0; i < length; i++) {
var region = skinRegions[i]
, regionLength = region.length
, randR = Math.ceil(Math.random() * 255)
, randG = Math.ceil(Math.random() * 255)
, randB = Math.ceil(Math.random() * 255);
for (var o = 0; o < regionLength; o++) {
var pixel = ctx.getImageData(region[o].x, region[o].y, 1,1)
, pdata = pixel.data;
pdata[0] = randR;
pdata[1] = randG;
pdata[2] = randB;
pixel.data = pdata;
ctx.putImageData(pixel, region[0].x, region[o].y);
}
}
}
function classifySkin(r, g, b) {
// A Survey on Pixel-Based Skin Color Detection Techniques
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))
, nurgb = toNormalizedRgb(r,g,b)
, nr = nurgb[0]
, ng = nurgb[1]
, nb = nurgb[2]
, powRgb2 = Math.pow(r+g+b,2)
, normRgbClassifier = (
(nr/ng > 1.185) &&
((r*b)/powRgb2 > 0.107) &&
((r*g)/powRgb2 > 0.112))
, hsv = toHsvTest(r,g,b)
, h = hsv[0]
, s = hsv[1]
, hsvClassifier = (h > 0 && h < 35 && s > 0.23 && s < 0.68);
/*
* ycc doesnt work
ycc = toYcc(r, g, b),
y = ycc[0],
cb = ycc[1],
cr = ycc[2],
yccClassifier = ((y > 80) && (cb > 77 && cb < 127) && (cr > 133 && cr < 173));
*/
return (rgbClassifier || normRgbClassifier || hsvClassifier);
}
function toYcc(r ,g, b) {
r /= 255, g /= 255, b /= 255;
var y = 0.299 * r + 0.587 * g + 0.114 * b
, cr = r - y
, cb = b - y;
return [y, cr, cb];
}
function toHsv(r, g, b) {
var sumRgb = r + g + b
, r_g = r - g
, r_b = r - b
, g_b = g - b;
return [
// hue
Math.acos( (0.5 * (r_g+r_b)) / Math.sqrt(Math.pow(r_g,2) + r_b*g_b) ),
// saturation
1 - 3 * (Math.min(r,g,b) / sumRgb),
// value
(1/3) * sumRgb
];
}
function toHsvTest(r, g, b) {
var h = 0
, mx = Math.max(r, g, b)
, mn = Math.min(r, g, b)
, dif = mx - mn
, sumRgb = r + g + b;
if (mx == r) {
h = (g - b) / dif;
} else if (mx == g) {
h = 2 + (g - r) / dif;
} else {
h = 4 + (r - g) / dif;
}
h = h * 60;
if (h < 0) h = h + 360;
return [h, 1 - 3 * mn / sumRgb, (1/3) * sumRgb];
}
function toNormalizedRgb(r, g, b) {
var sum = r + g + b;
return [r/sum, g/sum, b/sum];
}
exports.nude = {
// init: function(){
// initCanvas();
// },
load: function(url){
loadImageByLocal(url);
},
scan: function(fn){
if (arguments.length > 0 && typeof(arguments[0]) == 'function') {
resultFn = fn;
}
scanImage();
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment