Cloud Vision API はエロ画像をどの程度フィルタできるのか見てみた

この記事は[クソアプリ Advent Calendar 2015](http://www.adventar.org/calendars/763) の21日目の記事です。

話題の [Google Cloud Vision API のこの記事](http://qiita.com/kazunori279/items/768c7fdf96cdf45a9d16)を読んでいたら、なかなかエキサイティングなことがサラッとかかれていました。

> 「Safe Search検知」を使うと、Google検索のセーフサーチと同様に、**画像の内容が安全かどうかを検知**できます。これを使えば、例えばユーザーから画像のアップロードを受け付けてシェアしたりアイコンに使っているあらゆるサービスで、**手作業による不適切画像のチェックの労力を大幅に下げられます**。

ユーザー生成コンテンツ(UGC)を扱うサービスを作ると、必ず問題になるエロ＆グロ。これがある程度自動判別できるとしたら、(料金次第ですけど)魅力的です。
なんとかアドベントカレンダー期日前にリミテッドプレビューへの参加申請が通ったので、エロ画像をどの程度フィルタできるか、雰囲気をつかめるコードを書いてみました。

## やったこと

- Twitter Streaming API でエロ画像を検索
- 画像を Cloud Vision API に流す
- APIに「いやらしいよー！」と言われたらフィルタする

おことわり：「検出率○パーセント」とか数字を出したわけじゃないです。

## こんな感じ

![output.gif](https://qiita-image-store.s3.amazonaws.com/0/17722/6bcbbabb-d15b-2b9b-e581-eb68c6882009.gif)

これは"sexy"という単語で検索した時の結果です。エロ判定された画像は禁止マークになります。エロを除いてても水着のおねえさんは表示されたり、比較的かしこい気がします。
なお、"sexy"だと流速が速すぎて、途中から Vision API が 429 Too Many Requests を返し始めます。お金払うと解消できるのかは不明。
また、hentai (エロアニメのスラング)と入れるとほとんど画像がでてきませんでした。どうやら2次元画像も判別できるようです。

## サーバ側

仕様がまだ confidential であんまりコードは明かせず。そのうち GitHub にあげようかなと思います。

ザックリ説明すると、filter というモジュールが、渡された画像URLをダウンロードしてCloud Vision APIにリクエストし、結果を返す処理をやっています。
フィルタ結果は "VERY_UNLIKELY", "UNLIKELY", "LIKELY", "VERY_LIKELY" の4段階で返ってくるので、上位2つに該当する場合は禁止画像の URL を、そうでなければ普通に画像の URL を socket.io でクライアントに流してます。

```js
var Twit = require('twit');
var _ = require('lodash');
var filter = require('./filter');
var express = require('express');
var app = express();

app.use(express.static('public'));

var server = app.listen(3000, function () {
  var host = server.address().address;
  var port = server.address().port;

  console.log('Example app listening at http://%s:%s', host, port);
});

var io = require('socket.io')(server);

io.on('connection', function (socket) {
  var T = new Twit({
    // APIキーとか ...
  });

  var stream = T.stream('statuses/filter', { track: 'sexy' });

  stream.on('tweet', function(tweet) {
    var imgUrl = _.get(tweet, 'entities.media[0].media_url');
    if (imgUrl) {
      filter(imgUrl, function(result) {
        if (result === 'LIKELY' || result === 'VERY_LIKELY') {
          imgUrl = '/stop.png';
        }
        console.log(imgUrl);
        socket.broadcast.emit('tweet', { img: imgUrl });
      });
    }
  });
});
```

## クライアント側のjs

socket.io で受けて mithril で画像を順々に表示しているだけです。適当に m.redraw() とか呼んじゃってるんで、多分パフォーマンスはよくないです。

```js
var socket = io.connect();
var tweets = [];

socket.on('tweet', function (data) {
  tweets.unshift(data);
  if (tweets.length > 50) {
    tweets.pop();
  }
  m.redraw();
}.bind(this));

var Tweets = {
  controller: function() {
    this.tweets = tweets;
  },
  view: function(ctrl) {
    return ctrl.tweets.map(function(tweet) {
      return m('img', { src: tweet.img, style: 'height: 100px; margin: 10px' });
    });
  }
};

m.mount(document.body, Tweets);
```

## まとめ

2次画像も3次画像も判定できて、なかなか使えそうな予感がしています。
ただ、明らかにエロくても弾かれなかったり、逆に間違ってエロだと判定されるものもあるので、流石に人の手が一切不要というわけにはいかなさそうです。
VERY_LIKELY なものは自動で処理しちゃって、LIKELY は一応目視して、くらいにするといいのかも。
また、ラベル付けや顔認識など、他の機能も組み合わせて使うと精度を上げたりできるかもしれません。話題がそれますが、ラベル付けヤバイです。知り合いの結婚式の写真を入れてみたら「結婚式の写真ですね」とか言われてハンパねーって思いました。

それにしても、リミテットプレビュー通って早々にエロ画像を大量に流しこんだ私は、どう思われているのか不安です。いや、公式で「そういうことが出来る」って書いてるんだから、大丈夫だとは思いますが。