source: internals/2016/aptoideimagesdetector/trunk/Source Code/nude.py-master/nude.py-master/nude.py @ 16289

Last change on this file since 16289 was 16289, checked in by dferreira, 3 years ago

Initial content. All the tests done to three open-source platrofms.

  • Property svn:executable set to *
File size: 16.1 KB
Line 
1#!/usr/bin/env python
2# encoding: utf-8
3
4from __future__ import (absolute_import, division,
5                        print_function, unicode_literals)
6
7import copy
8import math
9import sys
10import time
11from collections import namedtuple
12from PIL import Image
13
14
15def is_nude(path_or_io):
16    nude = Nude(path_or_io)
17    return nude.parse().result
18
19
20class Nude(object):
21
22    Skin = namedtuple("Skin", "id skin region x y checked")
23
24    def __init__(self, path_or_io):
25        if isinstance(path_or_io, Image.Image):
26            self.image = path_or_io
27        elif isinstance(path_or_io, (str, file)):
28            self.image = Image.open(path_or_io)
29        else:
30            self.image = path_or_io
31        bands = self.image.getbands()
32        # convert greyscale to rgb
33        if len(bands) == 1:
34            new_img = Image.new("RGB", self.image.size)
35            new_img.paste(self.image)
36            f = self.image.filename
37            self.image = new_img
38            self.image.filename = f
39        self.skin_map = []
40        self.skin_regions = []
41        self.detected_regions = []
42        self.merge_regions = []
43        self.last_from, self.last_to = -1, -1
44        self.result = None
45        self.message = None
46        self.width, self.height = self.image.size
47        self.total_pixels = self.width * self.height
48
49    def resize(self, maxwidth=1000, maxheight=1000):
50        """
51        Will resize the image proportionately based on maxwidth and maxheight.
52        NOTE: This may effect the result of the detection algorithm.
53
54        Return value is 0 if no change was made, 1 if the image was changed
55        based on width, 2 if the image was changed based on height, 3 if it
56        was changed on both
57
58        maxwidth - The max size for the width of the picture
59        maxheight - The max size for the height of the picture
60        Both can be set to False to ignore
61        """
62        ret = 0
63        if maxwidth:
64            if self.width > maxwidth:
65                wpercent = (maxwidth / float(self.width))
66                hsize = int((float(self.height) * float(wpercent)))
67                fname = self.image.filename
68                self.image = self.image.resize((maxwidth, hsize), Image.ANTIALIAS)
69                self.image.filename = fname
70                self.width, self.height = self.image.size
71                self.total_pixels = self.width * self.height
72                ret += 1
73        if maxheight:
74            if self.height > maxheight:
75                hpercent = (maxheight / float(self.height))
76                wsize = int((float(self.width) * float(hpercent)))
77                fname = self.image.filename
78                self.image = self.image.resize((wsize, maxheight), Image.ANTIALIAS)
79                self.image.filename = fname
80                self.width, self.height = self.image.size
81                self.total_pixels = self.width * self.height
82                ret += 2
83        return ret
84
85    def parse(self):
86        if self.result:
87            return self
88
89        pixels = self.image.load()
90        for y in range(self.height):
91            for x in range(self.width):
92                r = pixels[x, y][0]   # red
93                g = pixels[x, y][1]   # green
94                b = pixels[x, y][2]   # blue
95                _id = x + y * self.width + 1
96
97                if not self._classify_skin(r, g, b):
98                    self.skin_map.append(self.Skin(_id, False, 0, x, y, False))
99                else:
100                    self.skin_map.append(self.Skin(_id, True, 0, x, y, False))
101
102                    region = -1
103                    check_indexes = [_id - 2,
104                                     _id - self.width - 2,
105                                     _id - self.width - 1,
106                                     _id - self.width]
107                    checker = False
108
109                    for index in check_indexes:
110                        try:
111                            self.skin_map[index]
112                        except IndexError:
113                            break
114                        if self.skin_map[index].skin:
115                            if (self.skin_map[index].region != region and
116                                    region != -1 and
117                                    self.last_from != region and
118                                    self.last_to != self.skin_map[index].region):
119                                self._add_merge(region, self.skin_map[index].region)
120                            region = self.skin_map[index].region
121                            checker = True
122
123                    if not checker:
124                        _skin = self.skin_map[_id - 1]._replace(region=len(self.detected_regions))
125                        self.skin_map[_id - 1] = _skin
126                        self.detected_regions.append([self.skin_map[_id - 1]])
127                        continue
128                    else:
129                        if region > -1:
130                            try:
131                                self.detected_regions[region]
132                            except IndexError:
133                                self.detected_regions.append([])
134                            _skin = self.skin_map[_id - 1]._replace(region=region)
135                            self.skin_map[_id - 1] = _skin
136                            self.detected_regions[region].append(self.skin_map[_id - 1])
137
138        self._merge(self.detected_regions, self.merge_regions)
139        self._analyse_regions()
140        return self
141
142    def inspect(self):
143        _nude_class = "{_module}.{_class}:{_addr}".format(_module=self.__class__.__module__,
144                                                          _class=self.__class__.__name__,
145                                                          _addr=hex(id(self)))
146        _image = "'%s' '%s' '%dx%d'" % (
147            self.image.filename, self.image.format, self.width, self.height)
148        return "#<{_nude_class}({_image}): result={_result} message='{_message}'>".format(
149            _nude_class=_nude_class, _image=_image, _result=self.result, _message=self.message)
150
151    def _add_merge(self, _from, _to):
152        self.last_from = _from
153        self.last_to = _to
154        from_index = -1
155        to_index = -1
156
157        for index, region in enumerate(self.merge_regions):
158            for r_index in region:
159                if r_index == _from:
160                    from_index = index
161                if r_index == _to:
162                    to_index = index
163
164        if from_index != -1 and to_index != -1:
165            if from_index != to_index:
166                _tmp = copy.copy(self.merge_regions[from_index])
167                _tmp.extend(self.merge_regions[to_index])
168                self.merge_regions[from_index] = _tmp
169                del(self.merge_regions[to_index])
170            return
171
172        if from_index == -1 and to_index == -1:
173            self.merge_regions.append([_from, _to])
174            return
175
176        if from_index != -1 and to_index == -1:
177            self.merge_regions[from_index].append(_to)
178            return
179
180        if from_index == -1 and to_index != -1:
181            self.merge_regions[to_index].append(_from)
182            return
183
184    # function for merging detected regions
185    def _merge(self, detected_regions, merge_regions):
186        new_detected_regions = []
187
188        # merging detected regions
189        for index, region in enumerate(merge_regions):
190            try:
191                new_detected_regions[index]
192            except IndexError:
193                new_detected_regions.append([])
194            for r_index in region:
195                _tmp = copy.copy(new_detected_regions[index])
196                _tmp.extend(detected_regions[r_index])
197                new_detected_regions[index] = _tmp
198                detected_regions[r_index] = []
199
200        # push the rest of the regions to the detRegions array
201        # (regions without merging)
202        for region in detected_regions:
203            if len(region) > 0:
204                new_detected_regions.append(region)
205
206        # clean up
207        self._clear_regions(new_detected_regions)
208
209    # clean up function
210    # only pushes regions which are bigger than a specific amount to the final result
211    def _clear_regions(self, detected_regions):
212        for region in detected_regions:
213            if len(region) > 30:
214                self.skin_regions.append(region)
215
216    def _analyse_regions(self):
217        # if there are less than 3 regions
218        if len(self.skin_regions) < 3:
219            self.message = "Less than 3 skin regions ({_skin_regions_size})".format(
220                _skin_regions_size=len(self.skin_regions))
221            self.result = False
222            return self.result
223
224        # sort the skin regions
225        self.skin_regions = sorted(self.skin_regions, key=lambda s: len(s),
226                                   reverse=True)
227
228        # count total skin pixels
229        total_skin = float(sum([len(skin_region) for skin_region in self.skin_regions]))
230
231        # check if there are more than 15% skin pixel in the image
232        if total_skin / self.total_pixels * 100 < 15:
233            # if the percentage lower than 15, it's not nude!
234            self.message = "Total skin percentage lower than 15 (%.3f%%)" % (
235                total_skin / self.total_pixels * 100)
236            self.result = False
237            return self.result
238
239        # check if the largest skin region is less than 35% of the total skin count
240        # AND if the second largest region is less than 30% of the total skin count
241        # AND if the third largest region is less than 30% of the total skin count
242        if len(self.skin_regions[0]) / total_skin * 100 < 35 and \
243           len(self.skin_regions[1]) / total_skin * 100 < 30 and \
244           len(self.skin_regions[2]) / total_skin * 100 < 30:
245            self.message = 'Less than 35%, 30%, 30% skin in the biggest regions'
246            self.result = False
247            return self.result
248
249        # check if the number of skin pixels in the largest region is
250        # less than 45% of the total skin count
251        if len(self.skin_regions[0]) / total_skin * 100 < 45:
252            self.message = "The biggest region contains less than 45 (%.3f%%)" % (
253                len(self.skin_regions[0]) / total_skin * 100)
254            self.result = False
255            return self.result
256
257        # TODO:
258        # build the bounding polygon by the regions edge values:
259        # Identify the leftmost, the uppermost, the rightmost, and the lowermost
260        # skin pixels of the three largest skin regions.
261        # Use these points as the corner points of a bounding polygon.
262
263        # TODO:
264        # check if the total skin count is less than 30% of the total number of pixels
265        # AND the number of skin pixels within the bounding polygon is
266        # less than 55% of the size of the polygon if this condition is True, it's not nude.
267
268        # TODO: include bounding polygon functionality
269        # if there are more than 60 skin regions and the average intensity
270        # within the polygon is less than 0.25 the image is not nude
271        if len(self.skin_regions) > 60:
272            self.message = "More than 60 skin regions ({_skin_regions_size})".format(
273                _skin_regions_size=len(self.skin_regions))
274            self.result = False
275            return self.result
276
277        # otherwise it is nude
278        self.message = "Nude!!"
279        self.result = True
280        return self.result
281
282    # A Survey on Pixel-Based Skin Color Detection Techniques
283    def _classify_skin(self, r, g, b):
284        rgb_classifier = r > 95 and \
285            g > 40 and g < 100 and \
286            b > 20 and \
287            max([r, g, b]) - min([r, g, b]) > 15 and \
288            abs(r - g) > 15 and \
289            r > g and \
290            r > b
291
292        nr, ng, nb = self._to_normalized(r, g, b)
293        norm_rgb_classifier = nr / ng > 1.185 and \
294            float(r * b) / ((r + g + b) ** 2) > 0.107 and \
295            float(r * g) / ((r + g + b) ** 2) > 0.112
296
297        # TODO: Add normalized HSI, HSV, and a few non-parametric skin models too
298
299        h, s, v = self._to_hsv(r, g, b)
300        hsv_classifier = h > 0 and \
301            h < 35 and \
302            s > 0.23 and \
303            s < 0.68
304
305        y, cb, cr = self._to_ycbcr(r, g,  b)
306        # Based on this paper http://research.ijcaonline.org/volume94/number6/pxc3895695.pdf
307        ycbcr_classifier = 97.5 <= cb <= 142.5 and 134 <= cr <= 176
308
309        nh, ns, nv = self._to_normalized(h, s, v)
310        # norm_hsv_classifier =
311        # ycc doesn't work
312        return rgb_classifier or norm_rgb_classifier or hsv_classifier or ycbcr_classifier
313
314    def _to_normalized_hsv(self, h, s, v):
315        if h == 0:
316            h = 0.0001
317        if s == 0:
318            s = 0.0001
319        if v == 0:
320            v = 0.0001
321        _sum = float(h + s + v)
322        return [h / 360.0, s / 100.0, v / 100.0]
323
324    def _to_normalized(self, r, g, b):
325        if r == 0:
326            r = 0.0001
327        if g == 0:
328            g = 0.0001
329        if b == 0:
330            b = 0.0001
331        _sum = float(r + g + b)
332        return [r / _sum, g / _sum, b / _sum]
333
334    def _to_ycbcr(self, r, g, b):
335        # Copied from here.
336        # http://stackoverflow.com/questions/19459831/rgb-to-ycbcr-conversion-problems
337        y = .299*r + .587*g + .114*b
338        cb = 128 - 0.168736*r - 0.331364*g + 0.5*b
339        cr = 128 + 0.5*r - 0.418688*g - 0.081312*b
340        return y, cb, cr
341
342    def _to_hsv(self, r, g, b):
343        h = 0
344        _sum = float(r + g + b)
345        _max = float(max([r, g, b]))
346        _min = float(min([r, g, b]))
347        diff = float(_max - _min)
348        if _sum == 0:
349            _sum = 0.0001
350
351        if _max == r:
352            if diff == 0:
353                h = sys.maxsize
354            else:
355                h = (g - b) / diff
356        elif _max == g:
357            h = 2 + ((g - r) / diff)
358        else:
359            h = 4 + ((r - g) / diff)
360
361        h *= 60
362        if h < 0:
363            h += 360
364
365        return [h, 1.0 - (3.0 * (_min / _sum)), (1.0 / 3.0) * _max]
366
367
368def _testfile(fname, resize=False):
369    start = time.time()
370    n = Nude(fname)
371    if resize:
372        n.resize(maxheight=800, maxwidth=600)
373    n.parse()
374    totaltime = int(math.ceil(time.time() - start))
375    size = str(n.height) + 'x' + str(n.width)
376    return (fname, n.result, totaltime, size, n.message)
377
378
379def _poolcallback(results):
380    fname, result, totaltime, size, message = results
381    print(fname, result, sep="\t")
382
383
384def _poolcallbackverbose(results):
385    fname, result, totaltime, size, message = results
386    print(fname, result, totaltime, size, message, sep=', ')
387
388
389def main():
390    """
391    Command line interface
392    """
393    import argparse
394    import os
395    import multiprocessing
396
397    parser = argparse.ArgumentParser(description='Detect nudity in images.')
398    parser.add_argument('files', metavar='image', nargs='+',
399                        help='Images you wish to test')
400    parser.add_argument('-r', '--resize', action='store_true',
401                        help='Reduce image size to increase speed of scanning')
402    parser.add_argument('-t', '--threads', metavar='int', type=int, required=False, default=0,
403                        help='The number of threads to start.')
404    parser.add_argument('-v', '--verbose', action='store_true')
405    args = parser.parse_args()
406
407    if args.threads <= 1:
408        args.threads = 0
409    if len(args.files) < args.threads:
410        args.threads = len(args.files)
411
412    callback = _poolcallback
413    if args.verbose:
414        print("#File Name, Result, Scan Time(sec), Image size, Message")
415        callback = _poolcallbackverbose
416
417    # If the user tuned on multi processing
418    if(args.threads):
419        threadlist = []
420        pool = multiprocessing.Pool(args.threads)
421        for fname in args.files:
422            if os.path.isfile(fname):
423                threadlist.append(pool.apply_async(_testfile, (fname, ),
424                                  {'resize': args.resize}, callback))
425            else:
426                print(fname, "is not a file")
427        pool.close()
428        try:
429            for t in threadlist:
430                t.wait()
431        except KeyboardInterrupt:
432            pool.terminate()
433            pool.join()
434    # Run without multiprocessing
435    else:
436        for fname in args.files:
437            if os.path.isfile(fname):
438                callback(_testfile(fname, resize=args.resize))
439            else:
440                print(fname, "is not a file")
441
442if __name__ == "__main__":
443    main()
Note: See TracBrowser for help on using the repository browser.