thuja

social media without the bullshit
git clone git://kqueue.dev/thuja.git
Log | Files | Refs | README | LICENSE

nude.go (9204B)


      1 package nude
      2 
      3 import (
      4 	"fmt"
      5 	"image"
      6 	"math"
      7 	"path/filepath"
      8 	"sort"
      9 )
     10 
     11 func IsNude(img image.Image) (bool, error) {
     12 	return IsImageNude(img)
     13 }
     14 
     15 func IsFileNude(imageFilePath string) (bool, error) {
     16 	path, err := filepath.Abs(imageFilePath)
     17 	if err != nil {
     18 		return false, err
     19 	}
     20 
     21 	img, err := decodeImage(path)
     22 	if err != nil {
     23 		return false, err
     24 	}
     25 
     26 	return IsImageNude(img)
     27 }
     28 
     29 func IsImageNude(img image.Image) (bool, error) {
     30 	d:= NewDetector(img)
     31 	return d.Parse()
     32 }
     33 
     34 type Detector struct {
     35 	image           image.Image
     36 	width           int
     37 	height          int
     38 	totalPixels     int
     39 	pixels          Region
     40 	SkinRegions     Regions
     41 	detectedRegions Regions
     42 	mergeRegions    [][]int
     43 	lastFrom        int
     44 	lastTo          int
     45 	message         string
     46 	result          bool
     47 }
     48 
     49 func NewDetector(img image.Image) *Detector {
     50 	d := &Detector{image: img }
     51 	return d
     52 }
     53 
     54 func (d *Detector) Parse() (result bool, err error) {
     55 	img := d.image
     56 	bounds := img.Bounds()
     57 	d.image = img
     58 	d.width = bounds.Size().X
     59 	d.height = bounds.Size().Y
     60 	d.lastFrom = -1
     61 	d.lastTo = -1
     62 	d.totalPixels = d.width * d.height
     63 
     64 	for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
     65 		width := bounds.Size().X
     66 		for x := bounds.Min.X; x < bounds.Max.X; x++ {
     67 			r, g, b, _ := d.image.At(x, y).RGBA()
     68 			normR := r / 256
     69 			normG := g / 256
     70 			normB := b / 256
     71 			currentIndex := x + y*width
     72 			nextIndex := currentIndex + 1
     73 
     74 			isSkin, v := classifySkin(normR, normG, normB)
     75 			if !isSkin {
     76 				d.pixels = append(d.pixels, &Pixel{currentIndex, false, 0, x, y, false, v})
     77 			} else {
     78 				d.pixels = append(d.pixels, &Pixel{currentIndex, true, 0, x, y, false, v})
     79 
     80 				region := -1
     81 				checkIndexes := []int{
     82 					nextIndex - 2,
     83 					nextIndex - width - 2,
     84 					nextIndex - width - 1,
     85 					nextIndex - width,
     86 				}
     87 				checker := false
     88 
     89 				for _, checkIndex := range checkIndexes {
     90 					if checkIndex < 0 {
     91 						continue
     92 					}
     93 					skin := d.pixels[checkIndex]
     94 					if skin != nil && skin.isSkin {
     95 						if skin.region != region &&
     96 							region != -1 &&
     97 							d.lastFrom != region &&
     98 							d.lastTo != skin.region {
     99 							d.addMerge(region, skin.region)
    100 						}
    101 						region = d.pixels[checkIndex].region
    102 						checker = true
    103 					}
    104 				}
    105 
    106 				if !checker {
    107 					d.pixels[currentIndex].region = len(d.detectedRegions)
    108 					d.detectedRegions = append(d.detectedRegions, Region{d.pixels[currentIndex]})
    109 					continue
    110 				} else {
    111 					if region > -1 {
    112 						if len(d.detectedRegions) >= region {
    113 							d.detectedRegions = append(d.detectedRegions, Region{})
    114 						}
    115 						d.pixels[currentIndex].region = region
    116 						d.detectedRegions[region] = append(d.detectedRegions[region], d.pixels[currentIndex])
    117 					}
    118 				}
    119 			}
    120 		}
    121 	}
    122 
    123 	d.merge(d.detectedRegions, d.mergeRegions)
    124 	d.analyzeRegions()
    125 
    126 	return d.result, err
    127 }
    128 
    129 func (d *Detector) addMerge(from, to int) {
    130 	d.lastFrom = from
    131 	d.lastTo = to
    132 
    133 	fromIndex := -1
    134 	toIndex := -1
    135 
    136 	for index, region := range d.mergeRegions {
    137 		for _, regionIndex := range region {
    138 			if regionIndex == from {
    139 				fromIndex = index
    140 			}
    141 			if regionIndex == to {
    142 				toIndex = index
    143 			}
    144 		}
    145 	}
    146 
    147 	if fromIndex != -1 && toIndex != -1 {
    148 		if fromIndex != toIndex {
    149 			fromRegion := d.mergeRegions[fromIndex]
    150 			toRegion := d.mergeRegions[toIndex]
    151 			region := append(fromRegion, toRegion...)
    152 			d.mergeRegions[fromIndex] = region
    153 			d.mergeRegions = append(d.mergeRegions[:toIndex], d.mergeRegions[toIndex+1:]...)
    154 		}
    155 		return
    156 	}
    157 
    158 	if fromIndex == -1 && toIndex == -1 {
    159 		d.mergeRegions = append(d.mergeRegions, []int{from, to})
    160 		return
    161 	}
    162 
    163 	if fromIndex != -1 && toIndex == -1 {
    164 		d.mergeRegions[fromIndex] = append(d.mergeRegions[fromIndex], to)
    165 		return
    166 	}
    167 
    168 	if fromIndex == -1 && toIndex != -1 {
    169 		d.mergeRegions[toIndex] = append(d.mergeRegions[toIndex], from)
    170 		return
    171 	}
    172 
    173 }
    174 
    175 // function for merging detected regions
    176 func (d *Detector) merge(detectedRegions Regions, mergeRegions [][]int) {
    177 	var newDetectedRegions Regions
    178 
    179 	// merging detected regions
    180 	for index, region := range mergeRegions {
    181 		if len(newDetectedRegions) >= index {
    182 			newDetectedRegions = append(newDetectedRegions, Region{})
    183 		}
    184 		for _, r := range region {
    185 			newDetectedRegions[index] = append(newDetectedRegions[index], detectedRegions[r]...)
    186 			detectedRegions[r] = Region{}
    187 		}
    188 	}
    189 
    190 	// push the rest of the regions to the newDetectedRegions array
    191 	// (regions without merging)
    192 	for _, region := range detectedRegions {
    193 		if len(region) > 0 {
    194 			newDetectedRegions = append(newDetectedRegions, region)
    195 		}
    196 	}
    197 
    198 	// clean up
    199 	d.clearRegions(newDetectedRegions)
    200 }
    201 
    202 // clean up function
    203 // only push regions which are bigger than a specific amount to the final resul
    204 func (d *Detector) clearRegions(detectedRegions Regions) {
    205 	for _, region := range detectedRegions {
    206 		if len(region) > 30 {
    207 			d.SkinRegions = append(d.SkinRegions, region)
    208 		}
    209 	}
    210 }
    211 
    212 func (d *Detector) analyzeRegions() bool {
    213 	skinRegionLength := len(d.SkinRegions)
    214 
    215 	// if there are less than 3 regions
    216 	if skinRegionLength < 3 {
    217 		d.message = fmt.Sprintf("Less than 3 skin regions (%v)", skinRegionLength)
    218 		d.result = false
    219 		return d.result
    220 	}
    221 
    222 	// sort the skinRegions
    223 	sort.Sort(sort.Reverse(d.SkinRegions))
    224 
    225 	// count total skin pixels
    226 	totalSkinPixels := float64(d.SkinRegions.totalPixels())
    227 
    228 	// check if there are more than 15% skin pixel in the image
    229 	totalSkinParcentage := totalSkinPixels / float64(d.totalPixels) * 100
    230 	if totalSkinParcentage < 15 {
    231 		// if the parcentage lower than 15, it's not nude!
    232 		d.message = fmt.Sprintf("Total skin parcentage lower than 15 (%v%%)", totalSkinParcentage)
    233 		d.result = false
    234 		return d.result
    235 	}
    236 
    237 	// check if the largest skin region is less than 35% of the total skin count
    238 	// AND if the second largest region is less than 30% of the total skin count
    239 	// AND if the third largest region is less than 30% of the total skin count
    240 	biggestRegionParcentage := float64(len(d.SkinRegions[0])) / totalSkinPixels * 100
    241 	secondLargeRegionParcentage := float64(len(d.SkinRegions[1])) / totalSkinPixels * 100
    242 	thirdLargesRegionParcentage := float64(len(d.SkinRegions[2])) / totalSkinPixels * 100
    243 	if biggestRegionParcentage < 35 &&
    244 		secondLargeRegionParcentage < 30 &&
    245 		thirdLargesRegionParcentage < 30 {
    246 		d.message = "Less than 35%, 30%, 30% skin in the biggest regions"
    247 		d.result = false
    248 		return d.result
    249 	}
    250 
    251 	// check if the number of skin pixels in the largest region is less than 45% of the total skin count
    252 	if biggestRegionParcentage < 45 {
    253 		d.message = fmt.Sprintf("The biggest region contains less than 45%% (%v)", biggestRegionParcentage)
    254 		d.result = false
    255 		return d.result
    256 	}
    257 
    258 	// check if the total skin count is less than 30% of the total number of pixels
    259 	// AND the number of skin pixels within the bounding polygon is less than 55% of the size of the polygon
    260 	// if this condition is true, it's not nude.
    261 	if totalSkinParcentage < 30 {
    262 		for i, region := range d.SkinRegions {
    263 			skinRate := region.skinRateInBoundingPolygon()
    264 			//fmt.Printf("skinRate[%v] = %v\n", i, skinRate)
    265 			if skinRate < 0.55 {
    266 				d.message = fmt.Sprintf("region[%d].skinRate(%v) < 0.55", i, skinRate)
    267 				d.result = false
    268 				return d.result
    269 			}
    270 		}
    271 	}
    272 
    273 	// if there are more than 60 skin regions and the average intensity within the polygon is less than 0.25
    274 	// the image is not nude
    275 	averageIntensity := d.SkinRegions.averageIntensity()
    276 	if skinRegionLength > 60 && averageIntensity < 0.25 {
    277 		d.message = fmt.Sprintf("More than 60 skin regions(%v) and averageIntensity(%v) < 0.25", skinRegionLength, averageIntensity)
    278 		d.result = false
    279 		return d.result
    280 	}
    281 
    282 	// otherwise it is nude
    283 	d.result = true
    284 	return d.result
    285 }
    286 
    287 func (d *Detector) String() string {
    288 	return fmt.Sprintf("#<nude.Detector result=%t, message=%s>", d.result, d.message)
    289 }
    290 
    291 // A Survey on Pixel-Based Skin Color Detection Techniques
    292 func classifySkin(r, g, b uint32) (bool, float64) {
    293 	rgbClassifier := r > 95 &&
    294 		g > 40 && g < 100 &&
    295 		b > 20 &&
    296 		maxRgb(r, g, b)-minRgb(r, g, b) > 15 &&
    297 		math.Abs(float64(r-g)) > 15 &&
    298 		r > g &&
    299 		r > b
    300 
    301 	nr, ng, _ := toNormalizedRgb(r, g, b)
    302 	normalizedRgbClassifier := nr/ng > 1.185 &&
    303 		(float64(r*b))/math.Pow(float64(r+g+b), 2) > 0.107 &&
    304 		(float64(r*g))/math.Pow(float64(r+g+b), 2) > 0.112
    305 
    306 	h, s, v := toHsv(r, g, b)
    307 	hsvClassifier := h > 0 &&
    308 		h < 35 &&
    309 		s > 0.23 &&
    310 		s < 0.68
    311 
    312 	// ycc doesnt work
    313 
    314 	result := rgbClassifier || normalizedRgbClassifier || hsvClassifier
    315 	return result, v
    316 }
    317 
    318 func maxRgb(r, g, b uint32) float64 {
    319 	return math.Max(math.Max(float64(r), float64(g)), float64(b))
    320 }
    321 
    322 func minRgb(r, g, b uint32) float64 {
    323 	return math.Min(math.Min(float64(r), float64(g)), float64(b))
    324 }
    325 
    326 func toNormalizedRgb(r, g, b uint32) (nr, ng, nb float64) {
    327 	sum := float64(r + g + b)
    328 	nr = float64(r) / sum
    329 	ng = float64(g) / sum
    330 	nb = float64(b) / sum
    331 
    332 	return nr, ng, nb
    333 }
    334 
    335 func toHsv(r, g, b uint32) (h, s, v float64) {
    336 	h = 0.0
    337 	sum := float64(r + g + b)
    338 	max := maxRgb(r, g, b)
    339 	min := minRgb(r, g, b)
    340 	diff := max - min
    341 
    342 	if max == float64(r) {
    343 		h = float64(g-b) / diff
    344 	} else if max == float64(g) {
    345 		h = 2 + float64(g-r)/diff
    346 	} else {
    347 		h = 4 + float64(r-g)/diff
    348 	}
    349 
    350 	h *= 60
    351 	if h < 0 {
    352 		h += 360
    353 	}
    354 
    355 	s = 1.0 - 3.0*(min/sum)
    356 	v = (1.0 / 3.0) * max
    357 
    358 	return h, s, v
    359 }