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 }