Konifar's WIP

親方!空からどらえもんが!

ポルノ写真投稿対策として肌色フィルタ作った話

写真投稿系のアプリだと必ず問題になると思うんですが、ユーザーが増えてくると ポルノを投稿をするユーザーが0.7〜1.0%くらいは出てくるんですよね。

本業のTaptripは海外のユーザーが99%以上なんですが、やはりポルノを投稿するユーザーが1%くらいいます。中でも中東地域のユーザーが投稿するポルノ写真は結構ヤバくて、 中学生が見たらトラウマになっちゃうんじゃないかってレベルです。

Googleの提唱するFamilySafetyの理念にも反しますし、電車の中でいきなり表示されると気まずいということもあって、昨年 『肌色フィルタ』というのを作って暫定対応しました。

ですが、最近もっと良い対策を入れたことで肌色フィルタは役割を終えることになったので、供養を兼ねてまとめておこうと思います。

先に言っておくと、AIやディープラーニングみたいな技術的なチャレンジはやっていません。あくまで工数かけずに50〜60%のポルノを捕捉するために実装してみたよという話なので、「なんか時間ない中で工夫してみたんだなぁ」という感じで生暖かく見ていただけると嬉しいです。

当時の状況

当時はやることが多い割に人が少なく、ポルノ対策にあまり工数をかけられませんでした。理想的にはディープラーニングでポルノ写真を抽出したり、24時間監視する体制を作って人力で撲滅したりしたいなぁと思いながらも、そこにリソースを割けなかったんですね。

そのため、 1〜2日くらいでなるべく効果の高い対策を打つ必要がありました

APIは高かった

まずはいい感じにポルノ写真を検出するAPIないかなぁと思ってざっと調べてみたところ、2つありました。

1. SightEngine

sightengine.com

実際にテストしてみても結構いい精度で検出されました。 Facebookがやってた乳首が映ってた判定みたいなのも簡単に実装できます。ただ、料金が高くて従量課金なのでちょっと怖さがありました。

2. Nudity Detection Service

www.mashape.com

Webページで気軽に試せるんですが、精度はあまりよくなかったです。料金は安めですが、個人で作ってるっぽいので今後どうなるかは不安でした。

アプリの規模にもよると思うんですが、1日5,000投稿を超えているような場合だと導入するにはちょっと高かったです。

1%以下の変態の投稿のためにこんなにお金使うのもなぁと思ったので、APIの使用は見送ることにしました。今はもしかしたら、フリーのイケてるAPIがあるかもしれません。

肌色フィルタを自作

どうしようかなぁと思って管理ツールでポルノ写真を眺めていたところ、 ポルノ写真は肌色の割合が多いという当たり前のことに気づきました。(管理ツールからポルノを投稿するユーザーを見ると 社内ポルノツールのような感じになっていました)

f:id:konifar:20150528000925p:plain

で、肌色の割合を見てフィルタリングすれば、結構ポルノを検出できるんじゃないかと感じたので作ってみることにしました。

1. サンプル収集

まず、20,000枚くらいのユーザー投稿写真をチェックして、180枚くらいのポルノ写真をローカルにダウンロードしました。 さらっと書きましたが、ここが一番大変でした。

2. ロジックを決める

肌色判定のロジックはたぶんどこかに落ちてるだろうなぁと思って調べてみたらやはりありました。

spam prevention - What is the best way to programatically detect porn images? - Stack Overflow

コードはPythonですがやってることは単純で、1pxずつRGBの値をチェックしてそれが肌色っぽいか判定して、肌色とみなされたものが全体の何%だったかを算出する、というものでした。

3. Rubyでテスト

とりあえずRailsスクリプト作ってテストしてみることにしました。RMagickを使って結構簡単に書けました。

require 'RMagick'
require 'open-uri'
 
class PornoFilter
 
  def self.run(attrs={})
    check attrs[:url]
  end
 
  private
  def self.check(url)
    url_image = open(url)
    img = Magick::ImageList.new.from_blob(url_image.read).first

    t = 0
    f = 0

    # 1pxずつ取り出して、RGBをチェックする
    img.color_histogram.inject({}) do |hash, key_val|
      red = key_val.first.red
      green = key_val.first.green
      blue = key_val.first.blue

      # ここが肌色判定のロジック
      if red > 60 && green < (red*0.85) && blue < (red*0.7) && green > (red*0.4) && blue > (red*0.2)
        t += 1
      else
        f += 1
      end
    end

    # 肌色が全体の50%以上だったらポルノ判定
    if t > f
      p "porno! #{t}, #{f} : #{url}"
    end  rescue => e
    p e.message
  end
end

rails runnerで実行すると結果が表示されます。

rails runner “PornoFilter.run(url: 'URL')"

4. チューニング

サンプルコードでは肌色率50%以上でポルノ判定としていたのですが、この設定値で180枚のわいせつ画像をグルグル回してチェックしてみると、20%くらいのポルノしか検出できませんでした。かといって、あまり比率を下げるとポルノ以外の画像もポルノ判定されてしまうので、少しずつチューニングして精度を上げていきました。

その結果、 肌色率30%が適正値っぽいという結論に至りました。これでポルノ画像の 50%〜60%くらいはフィルタリングできました。もちろん完璧ではなく、夕焼けの画像や寿司の写真が間違えてフィルタされてしまうこともありましたが、ポルノ捕捉率としては十分だったのでこのロジックで実装することに決めました。

5. Javaで実装

サーバーにあまり負荷をかけたくなかったので、Android(クライアント)側で実装することにしました。やってることは単純で、画像をサーバーにアップロードする前にBitmapを引数で渡してポルノ判定するだけです。

public static boolean isPorno(Bitmap bmp) {
    int t = 0;
    int f = 0;
 
    for (int i = 0; i < bmp.getHeight(); i++) {
        for (int j = 0; j < bmp.getWidth(); j++) {
            int pixel = bmp.getPixel(j, i);
            int red = Color.red(pixel);
            int green = Color.green(pixel);
            int blue = Color.blue(pixel);
 
            if (red > 60
                && green < (red * 0.85)
                && blue < (red * 0.7)
                && green > (red * 0.4)
                && blue > (red * 0.2)) {
                t++;
            } else {
                f++;
            }
        }
    }
    return t > (t + f) * 0.3;
}

結果とこれから

リリース後、想定通り50〜60%のポルノ画像を検出することができました。半分かぁと思われるかもしれませんが、 半分減るだけでもポルノ遭遇率はかなり減ります。あまり工数かけずに実装したにしては、それなりに効果の高い改善だったのではないかと思います。

もちろん完璧ではなく、ウニ丼の写真がフィルタされてしまったりしていました。また、とても色白な人や黒人のポルノ写真はこのロジックでは捕捉できなかったです。

今はこの肌色フィルタは役割を終えて、もっと精度の高いやり方でフィルタリングするようにしています。このあたりのロジックはまたいつか書こうと思います。

とりあえずこれまでポルノを捕捉してくれた肌色フィルタに感謝しつつ、これからもポルノ投稿ユーザーと戦うべく頑張ります。ディープラーニングやりたいです。