class Nude

Constants

Skin
VERSION

Attributes

image[R]

@return [Magick::Image]

message[R]

@return [String]

nude?[R]

@return [Boolean]

result[R]

@return [Boolean]

Public Class Methods

new(path_or_io) click to toggle source

@private

# File lib/nude.rb, line 42
def initialize(path_or_io)
  # get the image data
  @image = (
    case path_or_io
    when Magick::Image
      path_or_io
    when IO
      Magick::Image.from_blob(path_or_io.read).first
    else
      Magick::Image.read(path_or_io).first
    end
  )
  unless @image
    raise NudeError, 'Could not read image data'
  end
  @skin_map = []
  @skin_regions = []
  @detected_regions = []
  @merge_regions = []
  @width = @image.columns
  @height = @image.rows
  @total_pixels = @width * @height
  @last_from = -1
  @last_to = -1
  @result = nil
  @message = nil
end
nude?(path_or_io) click to toggle source

@param path_or_io [String, IO, Magick::Image] @return [Boolean] @example

Nude.nude?('/path/to/image.jpg')
# File lib/nude.rb, line 16
def nude?(path_or_io)
  parse(path_or_io).nude?
end
parse(path_or_io) click to toggle source

@param path_or_io [String, IO, Magick::Image] @return [Nude] @example

Nude.parse?('/path/to/image.jpg')
# File lib/nude.rb, line 24
def parse(path_or_io)
  new(path_or_io).parse
end

Public Instance Methods

inspect() click to toggle source

@private

# File lib/nude.rb, line 129
def inspect
  variables = [:@result, :@message, :@image].map { |key|
    key.to_s + "=" + instance_variable_get(key).inspect
  }.join(", ")
  "#<#{self.class}:0x#{object_id} #{variables}>"
end
parse() click to toggle source

@return [Nude]

# File lib/nude.rb, line 71
def parse
  return self unless @result.nil?

  # iterate the image from the top left to the bottom right
  @image.each_pixel do |pixel, x, y|
    r  = pixel.red   / 256
    g  = pixel.green / 256
    b  = pixel.blue  / 256
    id = x + y * @width + 1

    if not classify_skin(r, g, b)
      @skin_map << Skin.new(id, false, 0, x, y, false)
    else
      @skin_map << Skin.new(id, true, 0, x, y, false)

      region = -1
      check_indexes = [
        id - 2,
        id - @width - 2,
        id - @width - 1,
        id - @width
      ]
      checker = false

      check_indexes.each do |index|
        if @skin_map[index] && @skin_map[index].skin
          if @skin_map[index].region != region &&
              region != -1 &&
              @last_from != region &&
              @last_to != @skin_map[index].region
            add_merge(region, @skin_map[index].region)
          end
          region = @skin_map[index].region
          checker = true
        end
      end

      if not checker
        @skin_map[id - 1].region = @detected_regions.size
        @detected_regions << [@skin_map[id - 1]]
        next
      else
        if region > -1
          @detected_regions[region] ||= []
          @skin_map[id - 1].region = region
          @detected_regions[region] << @skin_map[id - 1]
        end
      end
    end
  end

  merge(@detected_regions, @merge_regions)
  analyse_regions

  self
end

Private Instance Methods

add_merge(from, to) click to toggle source
# File lib/nude.rb, line 138
def add_merge(from, to)
  @last_from = from
  @last_to = to

  from_index = -1
  to_index = -1

  @merge_regions.each.with_index do |region, index|
    region.each do |r_index|
      from_index = index if r_index == from
      to_index   = index if r_index == to
    end
  end

  if from_index != -1 && to_index != -1
    if from_index != to_index
      region = @merge_regions[from_index].concat(@merge_regions.delete_at(to_index))
      @merge_regions[from_index] = region
    end
    return
  end

  if from_index == -1 && to_index == -1
    @merge_regions << [from, to]
    return
  end

  if from_index != -1 && to_index == -1
    @merge_regions[from_index] << to
    return
  end

  if from_index == -1 && to_index != -1
    @merge_regions[to_index] << from
    return
  end
end
analyse_regions() click to toggle source
# File lib/nude.rb, line 208
def analyse_regions
  # if there are less than 3 regions
  if @skin_regions.size < 3
    @message = "Less than 3 skin regions (#{@skin_regions.size})"
    return @result = false
  end

  # sort the skin regions
  @skin_regions = @skin_regions.sort_by { |region| - region.size }

  # count total skin pixels
  total_skin = @skin_regions.map(&:size).reduce(:+).to_f

  # check if there are more than 15% skin pixel in the image
  if total_skin / @total_pixels * 100 < 15
    # if the percentage lower than 15, it's not nude!
    @message = "Total skin parcentage lower than 15 (#{total_skin / @total_pixels * 100}%)"
    return @result = false
  end

  # 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 @skin_regions[0].size / total_skin * 100 < 35 &&
      @skin_regions[1].size / total_skin * 100 < 30 &&
      @skin_regions[2].size / total_skin * 100 < 30
    @message = 'Less than 35%, 30%, 30% skin in the biggest regions'
    return @result = false
  end

  # check if the number of skin pixels in the largest region is less than 45% of the total skin count
  if @skin_regions.first.size / total_skin * 100 < 45
    @message = "The biggest region contains less than 45% (#{@skin_regions.first.size / total_skin * 100}%)"
    return @result = false
  end

  # 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 @skin_regions.size > 60
    @message = "More than 60 skin regions (#{@skin_regions.size})"
    return @result = false
  end

  # otherwise it is nude
  @result = true
end
classify_skin(r, g, b) click to toggle source

A Survey on Pixel-Based Skin Color Detection Techniques

# File lib/nude.rb, line 267
def classify_skin(r, g, b)
  rgb_classifier =
    r > 95 &&
    g > 40 && g < 100 &&
    b > 20 &&
    [r, g, b].max - [r, g, b].min > 15 &&
    (r - g).abs > 15 &&
    r > g &&
    r > b

  nr, ng, nb = *to_normalized_rgb(r, g, b)
  norm_rgb_classifier =
    nr / ng > 1.185 &&
    (r * b).to_f / ((r + g + b) ** 2) > 0.107 &&
    (r * g).to_f / ((r + g + b) ** 2) > 0.112

  h, s, v = *to_hsv(r, g, b)
  hsv_classifier =
    h > 0 &&
    h < 35 &&
    s > 0.23 &&
    s < 0.68

  # ycc doesnt work

  rgb_classifier || norm_rgb_classifier || hsv_classifier
end
clear_regions(detected_regions) click to toggle source

clean up function only pushes regions which are bigger than a specific amount to the final result

# File lib/nude.rb, line 202
def clear_regions(detected_regions)
  detected_regions.each do |region|
    @skin_regions << region if region.size > 30
  end
end
merge(detected_regions, merge_regions) click to toggle source

function for merging detected regions

# File lib/nude.rb, line 177
def merge(detected_regions, merge_regions)
  new_detected_regions = []

  # merging detected regions
  merge_regions.each.with_index do |region, index|
    new_detected_regions[index] ||= []
    region.each do |r_index|
      region = new_detected_regions[index].concat(detected_regions[r_index])
      new_detected_regions[index] = region
      detected_regions[r_index] = []
    end
  end

  # push the rest of the regions to the detRegions array
  # (regions without merging)
  detected_regions.each do |region|
    new_detected_regions << region if region.size > 0
  end

  # clean up
  clear_regions(new_detected_regions)
end
to_hsv(r, g, b) click to toggle source
# File lib/nude.rb, line 305
def to_hsv(r, g, b)
  h = 0
  sum = (r + g + b).to_f
  max = [r, g, b].max.to_f
  min = [r, g, b].min.to_f
  diff = (max - min).to_f

  if max == r
    h = (g - b) / diff
  elsif max == g
    h = 2 + ((g - r) / diff)
  else
    h = 4 + ((r - g) / diff)
  end

  h *= 60
  h += 360 if h < 0

  [
    h,
    1.0 - (3.0 * (min / sum)),
    (1.0 / 3.0) * max
  ]
end
to_normalized_rgb(r, g, b) click to toggle source
# File lib/nude.rb, line 295
def to_normalized_rgb(r, g, b)
  sum = (r + g + b).to_f

  [
    r / sum,
    g / sum,
    b / sum
  ]
end