File Coverage

blib/lib/Imager/SkinDetector.pm
Criterion Covered Total %
statement 12 152 7.8
branch 0 52 0.0
condition 0 26 0.0
subroutine 4 14 28.5
pod 6 9 66.6
total 22 253 8.7


line stmt bran cond sub pod time code
1             # vim: ts=4 sw=4 tw=0 et
2             # $Id: SkinDetector.pm 136 2008-10-18 21:11:51Z Cosimo $
3              
4             package Imager::SkinDetector;
5              
6 1     1   35886 use strict;
  1         3  
  1         45  
7 1     1   6 use Carp q(croak);
  1         2  
  1         68  
8 1     1   1328 use File::Temp ();
  1         34110  
  1         34  
9 1     1   11 use base q(Imager);
  1         2  
  1         1575  
10              
11             our $VERSION = '0.05';
12              
13             sub new {
14 0     0 0   my ($class, %opt) = @_;
15              
16 0 0         return unless %opt;
17              
18 0           my $file = $opt{file};
19 0           my $url = $opt{url};
20              
21 0 0 0       if (! defined $file && ! defined $url) {
22 0           return;
23             }
24              
25 0   0       $class = ref($class) || $class;
26 0           my $self = Imager->new();
27              
28             # If url, download it and open a temp file
29 0 0 0       if (defined $url && $url =~ m{^https?://}) {
30 0           $file = _download_temp_file($url);
31             }
32              
33 0 0         if (! $self->open(file=>$file)) {
34 0           croak $self->errstr();
35             }
36              
37 0           bless $self, $class;
38             }
39              
40             # A rough estimate, based on all other meaningful factors
41             sub contains_nudity {
42 0     0 1   my ($img) = @_;
43              
44             # All factors should have range (0..1)
45 0           my $skinniness = $img->skinniness();
46 0   0       my $coloriness = $img->has_different_colors() || 0.0001;
47              
48             # Apply gaussian function to $coloriness.
49              
50             # We assume that 0.2 (7 distinct color hues, 0.2*36)
51             # is the center and maximum of our gaussian curve,
52             # rapidly decreasing to zero for values lower and higher
53             # than 7.
54              
55             # If the image has a color histogram with many different
56             # hue intervals (max=36) then it's unlikely it contains nudity.
57             # Same goes for images with 1 or 2 distinct color hues.
58              
59             # See: http://en.wikipedia.org/wiki/Gaussian_function
60              
61 0           $coloriness = exp(-(($coloriness - 0.2)**2 / 20));
62              
63 0           my $nudity_factor = $skinniness * $coloriness;
64              
65 0           return $nudity_factor;
66             }
67              
68             # Make a HTTP request and save it as temporary file
69             sub _download_temp_file {
70 0     0     my ($url) = @_;
71              
72 0           eval { require LWP::Simple };
  0            
73 0 0         if ($@) {
74 0           croak "Can't download URL $url without LWP";
75             }
76              
77 0           my $pic = LWP::Simple::get($url);
78 0 0         if (! $pic) {
79 0           croak "Failed to load image at URL $url";
80             }
81              
82 0           my $tmpf = File::Temp->new( UNLINK => 0 );
83 0           my $file_name = $tmpf->filename();
84 0           binmode $tmpf;
85 0           print $tmpf $pic;
86 0           close $tmpf;
87              
88 0           return $file_name;
89             }
90              
91             sub hue_frequencies {
92 0     0 1   my ($img) = @_;
93              
94 0 0         return unless $img;
95              
96 0           my $width = $img->getwidth() - 1;
97 0           my $height = $img->getheight() - 1;
98 0           my @frequency = (0) x 36;
99 0           my ($r, $g, $b, $h, $s, $v);
100 0           my $color;
101 0           my $color_interval;
102 0           my $total = 0;
103              
104             # Sample the image and check pixel colors
105 0           for (my $x = 0; $x < $width; $x += 5) {
106              
107 0           for (my $y = 0; $y < $height; $y += 5) {
108              
109 0 0         next unless $color = $img->getpixel(x => $x, y => $y);
110              
111 0           ($r, $g, $b) = $color->rgba();
112 0           ($h, $s, $v) = rgb2hsv($r, $g, $b);
113              
114 0           $color_interval = int ($h / 10);
115 0           $frequency[$color_interval]++;
116 0           $total++;
117             }
118             }
119              
120             # Normalize frequencies, removing spurious results
121 0 0         if (! $total) {
122 0           return;
123             }
124              
125 0           for my $value (@frequency) {
126 0           $value /= $total;
127             }
128              
129 0           return @frequency;
130             }
131              
132             sub minmax {
133 0     0 0   local $_;
134 0           my $max = my $min = $_[0];
135              
136 0           for(@_) {
137 0 0         $max = $_ if $_ > $max;
138 0 0         $min = $_ if $_ < $min;
139             }
140 0           return($min, $max);
141             }
142              
143             # Naive rgb-to-hsv conversion. Slooow...
144             sub rgb2hsv {
145 0     0 1   my($r, $g, $b) = @_;
146              
147             #$r = 255 if $r > 255;
148             #$g = 255 if $g > 255;
149             #$b = 255 if $b > 255;
150              
151 0           my($h1, $s1, $v1);
152 0           my($max, $min, $diff);
153              
154 0           ($min, $max) = minmax($r, $g, $b);
155 0           $diff = $max - $min;
156              
157 0 0         if($max == 0) {
158 0           $h1 = $s1 = $v1 = 0;
159             }
160             else {
161 0           $v1 = $max;
162 0           $s1 = $diff / $max;
163 0 0         if($s1 == 0) {
164 0           $h1 = 0;
165             }
166             else {
167             # Foley & VanDam HSV space
168 0 0         if ($r == $max) {
169 0           $h1 = ($g - $b) / $diff;
170             }
171 0 0         if ($g == $max) {
172 0           $h1 = 2 + ($b - $r) / $diff;
173             }
174 0 0         if ($b == $max) {
175 0           $h1 = 4 + ($r - $g) / $diff;
176             }
177              
178             # Convert to range [0, 360] degrees
179 0           $h1 *= 60;
180 0 0         $h1 += 360 if $h1 < 0;
181             }
182             }
183              
184 0           return($h1, $s1, $v1);
185             }
186              
187             sub is_skin {
188 0     0 1   my ($color) = $_[0];
189 0           my ($r, $g, $b, $a) = $color->rgba;
190 0           my ($h, $s, $v) = rgb2hsv($r, $g, $b);
191              
192             #print "RGBA(", join(', ', $r, $g, $b, $a), ")\n";
193             #print "HSV (", join(', ', $h, $s, $v), "\n";
194             #;
195              
196             # Hue 5..40 could be a good approximation of "white" skin
197 0 0 0       if ($h >= 5 && $h <= 40 && $v > 60) {
      0        
198 0           return 1;
199             }
200              
201             # TODO Detect also black skin
202 0           return 0;
203             }
204              
205             sub is_fuzzy {
206 0     0 1   my ($img) = @_;
207              
208 0 0         return unless $img;
209              
210 0           my $width = $img->getwidth() - 1;
211 0           my $height = $img->getheight() - 1;
212              
213 0           my $differences = 0;
214 0           my $total_samples = 0;
215 0           my ($r, $g, $b);
216              
217             # Sample first pixel
218 0           my $color = $img->getpixel(x=>0, y=>0);
219 0           my($r2, $g2, $b2, undef) = $color->rgba();
220 0           my $pixel_diff = 0;
221 0           my $pixel_diff_threshold = 40;
222              
223             # Sample the image and check how much "variance"
224             # there is between pixels
225 0           for (my $x = 5; $x < $width - 5; $x += 1) {
226              
227 0           for (my $y = 5; $y < $height - 5; $y += 1) {
228              
229 0           $color = $img->getpixel(x => $x, y => $y);
230 0           ($r, $g, $b, undef) = $color->rgba();
231              
232 0 0 0       if ($r < 5 && $g < 5 && $b < 5) {
      0        
233             next
234 0           }
235              
236 0           $pixel_diff = abs($r2 - $r);
237 0           $pixel_diff += abs($g2 - $g);
238 0           $pixel_diff += abs($b2 - $b);
239              
240 0 0         if ($pixel_diff > $pixel_diff_threshold) {
241 0           $differences += $pixel_diff;
242             }
243              
244 0           $r2 = $r;
245 0           $g2 = $g;
246 0           $b2 = $b;
247              
248 0           $total_samples++;
249             }
250              
251             }
252              
253 0 0         if ($total_samples == 0) {
254             return
255 0           }
256              
257 0           $differences = $differences / $total_samples;
258              
259 0           return $differences;
260             }
261              
262             sub has_different_colors {
263 0     0 0   my ($img) = @_;
264              
265             # Filter out colors with <= 3%
266 0           my $value_threshold = 0.04;
267              
268             # Extract hue histogram
269 0           my @freq = $img->hue_frequencies();
270              
271 0           my $distinct = 0;
272 0           for (@freq) {
273 0 0         ++$distinct if $_ > $value_threshold;
274             }
275              
276             # 36 is total possible different hue intervals
277 0           return $distinct / 36;
278             }
279              
280             sub skinniness {
281 0     0 1   my ($img) = @_;
282              
283 0 0         return unless $img;
284              
285 0           my $width = $img->getwidth() - 1;
286 0           my $height = $img->getheight() - 1;
287              
288 0           my $skin_colors = 0;
289 0           my $total_samples = 0;
290 0           my $color;
291              
292             # Sample the image and check pixel colors
293 0           for (my $x = 0; $x < $width; $x += 10) {
294 0           for (my $y = 0; $y < $height; $y += 10) {
295              
296 0           $color = $img->getpixel(x => $x, y => $y);
297              
298 0 0 0       if ($color && is_skin($color)) {
299 0           $skin_colors++;
300             }
301              
302 0           $total_samples++;
303             }
304             }
305              
306 0 0         if ($total_samples == 0) {
307             return
308 0           }
309              
310 0           return $skin_colors / $total_samples;
311             }
312              
313             1;
314              
315             __END__