//
you're reading...
Deep Packet Analysis, Lawful Intercept, Social Graphing, Source Code

Detect nude images and pornography without using OpenCV

OpenCV (Open Source Computer Vision) is a library of programming functions for processing video and images. The library has more than 2500 optimized algorithms. Usage ranges from interactive art, to mines inspection, stitching maps on the web or through advanced robotics.

Below is some code copied from that OpenCV group that gives a mask for the ‘flesh color’, as you can see this solutions is quite cumbersome and requires the OpenCV library

src_RGB = RGB-Image (IPL_DEPTH_8U , 3); // Source
mask_BW = GRAY-Image (IPL_DEPTH_8U , 3);// Resulting Mask same size as source !!
//after GetSkinMask you can use cvAnd between src_RGB and mask_BW.


void GetSkinMask(IplImage * src_RGB, IplImage * mask_BW, int
erosions=1, int dilations=7)
{   

CvSize size;

CvSize sz = cvSize( src_RGB->width & -2, src_RGB->height & -2);
//get the size of input_image (src_RGB)

IplImage* pyr = cvCreateImage( cvSize(sz.width/2, sz.height/2), 8,   3 ); //create 2 temp-images

IplImage* src = cvCreateImage(cvGetSize(src_RGB), IPL_DEPTH_8U ,3);
cvCopyImage(src_RGB, src);

IplImage* tmpYCR = cvCreateImage(cvGetSize(src), IPL_DEPTH_8U , 3);
cvPyrDown( src, pyr, 7 );
//remove noise from input
cvPyrUp( pyr, src, 7 );

cvCvtColor(src ,tmpYCR , CV_RGB2YCrCb);

uchar Y;
uchar Cr;
uchar Cb;

CvPixelPosition8u pos_src;
CvPixelPosition8u pos_dst;

int x =0;
int y =0;

CV_INIT_PIXEL_POS(pos_src,(unsigned char *) tmpYCR->imageData,
tmpYCR->widthStep,    cvGetSize(tmpYCR), x,y, tmpYCR->origin);

CV_INIT_PIXEL_POS(pos_dst,    (unsigned char *) mask_BW->imageData,
mask_BW->widthStep,    cvGetSize(mask_BW),   x,y, mask_BW->origin);

uchar * ptr_src;
uchar * ptr_dst;

for( y=0;y<src-> height; y++)
{
    for ( x=0; x<src->width; x++)
    {
        ptr_src = CV_MOVE_TO(pos_src,x,y,3);
        ptr_dst = CV_MOVE_TO(pos_dst,x,y,3);

        Y = ptr_src[0];
        Cb= ptr_src[1];
        Cr= ptr_src[2];

        if( Cr > 138 && Cr < 178 &&
        Cb + 0.6 * Cr >200 && Cb + 0.6 * Cr <215)
        {
            ptr_dst[0] = 255;
            ptr_dst[1] = 255;
            ptr_dst[2] = 255;
        }
        else
        {
            ptr_dst[0] = 0;
            ptr_dst[1] = 0;
            ptr_dst[2] = 0;
        }
    }
}

if(erosions>0) cvErode(mask_BW,mask_BW,0,erosions);
if (dilations>0) cvDilate(mask_BW,mask_BW,0,dilations);

cvReleaseImage(&pyr);
cvReleaseImage(&tmpYCR);
cvReleaseImage(&src);
}

Such pornographic imagery recognition begins by extracting features that discriminate adult video and non-adult ones. Since uncovered skin distribution is of importance for the detection of such videos, we need to detect maximum skin pixels of the abdomen area. Pornographic images will have more congruent pixels of skin pigment than non-pornographic due to showing the area of a stomach, chest, and hips.

We can see a great demonstration of this and some additional factors in noworker.nude from the nude.js project by Patrick Wied @patrickwied

/*
 * Nude.js - Nudity detection with Javascript and HTMLCanvas
 * 
 * Author: Patrick Wied ( http://www.patrick-wied.at )
 * Version: 0.1  (2010-11-21)
 * License: MIT License
 */
(function(){
	Array.prototype.remove = function(index) {
		  var rest = this.slice(index + 1);
		  this.length = index;
		  return this.push.apply(this, rest);
	};
	
	var nude = (function(){
		// private var definition
		var canvas = null,
		ctx = null,
		skinRegions = [],
		resultFn = null,
		img = null,
		// private functions
		initCanvas = function(){
			canvas = document.createElement("canvas");
			// the canvas should not be visible
			canvas.style.display = "none";
			var b = document.getElementsByTagName("body")[0];
			b.appendChild(canvas);
			ctx = canvas.getContext("2d");
		},
		loadImage = function(id){
			
			img = document.getElementById(id);
			canvas.width = img.width;
			canvas.height = img.height;
			// reset the arrays
			skinRegions = [],
			resultFn = null;
			// draw the image into the canvas element
			ctx.drawImage(img, 0, 0);
			
		
		},
		scanImage = function(){
			// 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 bottom right
			var length = imageData.length,
			width = canvas.width;
			
			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
		merge = function(detectedRegions, mergeRegions){
			
			var length = mergeRegions.length,
			detRegions = [];
			
			
			// merging detected regions 
			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
			clearRegions(detRegions);
			
		},
		// clean up function
		// only pushes regions which are bigger than a specific amount to the final result
		clearRegions = function(detectedRegions){
			
			var length = detectedRegions.length;
			
			for(var i=0; i < length; i++){
				if(detectedRegions[i].length > 30){
					skinRegions.push(detectedRegions[i]);
				}
			}
			
		},
		analyseRegions = function(){
			
			// 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 region is less than 30% of the total skin count
			// AND if the third largest 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 bounding polygon.
			
			// TODO:
			// check if the total skin count is less than 30% 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
		resultHandler = function(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)
		colorizeRegions = function(){
			
			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),
				rangB = 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] = rangB;
					
					pixel.data = pdata;
					
					ctx.putImageData(pixel, region[o].x, region[o].y);
					
				}
				
			}
			
		}, 
		classifySkin = function(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],
			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)),
			//hsv = toHsv(r, g, b),
			//h = hsv[0]*100,
			//s = hsv[1],
			//hsvClassifier = (h < 50 && h > 0 && s > 0.23 && s < 0.68);
			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); // 
		},
		toYcc = function(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];
		},
		toHsv = function(r, 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))/(r+g+b))),
			        // value
			        (1/3)*(r+g+b)
			        ];
		},
		toHsvTest = function(r, g, b){
			var h = 0,
			mx = Math.max(r, g, b),
			mn = Math.min(r, g, b),
			dif = mx - mn;
			
			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*((Math.min(r,g,b))/(r+g+b))),(1/3)*(r+g+b)] ;	
			
		},
		toNormalizedRgb = function(r, g, b){
			var sum = r+g+b;
			return [(r/sum), (g/sum), (b/sum)];
		};
		
		// public interface
		return {
			init: function(){
				initCanvas();
			},
			load: function(id){
				loadImage(id);
			},
			scan: function(fn){
				if(arguments.length>0 && typeof(arguments[0]) == "function"){
					resultFn = fn;
				}
				scanImage();
			}
		};
	})();
	// register nude at window object
	window.nude = nude;
	nude.init();
})();

The nude.js project is a brilliant idea as online marketers and the internet in general becomes more careless about nudity. With this library browser extensions could easily be implimented to help cut down on minor exposure without forcing content regulations on everyone. I might even use it to back a browser extension once the performance issues are cleared up a bit.

Back to the task at hand. It is time to write our own c# class to do this on our own. A followup post to come shortly,

About Pythorian

Exploration and Production oriented security consultant for securing IT infrastructures relating to natural resources.

Discussion

No comments yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: