ユーザーがアップロードした画像がアダルト画像(不適切な画像)か判定する処理の作成について解説していきます!

基本的には、Laravelで画像を判定するのではなくAmazonのRekognitionというサービスを利用して画像解析の結果を元にアダルト画像かを判定します。
まずは、画像分析サービスのAmazon Rekognitionについて解説してきます。

Amazon Rekognitionについて

Amazon Rekognition(アマゾン リコグニション)は、ディープラーニングやAIによって画像を分析するサービスする。画像の中から顔を抽出し他の画像から一致率を算出したりすることができます。

その他にも、画像に何が写っているかを文字情報として表示してくれるなどもあります。

Amazon Rekognitionの良いところは、難しいことをしなくても画像と欲しい情報を送信すれば結果が返ってくるところです。
本来は自分で人工知能を作成したり、ディープラーニングなどで画像を学習させたりする手間があったりします。
また、画像認識のライブラリには特許権が含まれているものもあり中々商用利用が難しかったりするのでかなり開発のコストを削減できます。

メリット
  • 画像と欲しい情報を送信するだけ結果が返ってくる(難しい作業は不要)
  • 料金が安い(0.0013USD〜)
  • 自分で画像認識AIを開発しなくて良くなる
  • 特許侵害の心配がなくなる(独自で開発すると特許侵害の可能性があるため)
デメリット
  • 大量の画像を分析する場合は開発した方が安くなる場合もある
  • インターネットで画像を送信するので、ネットワーク帯域が圧迫される可能性がある

メリット・デメリットを簡単にまとめました。正直そこまでデメリットがないように感じています。
個人的な見解にはなりますが、画像AIの開発は時間もお金もかかる他運用やアップグレードなどもあるためこういった外部のサービスを使う方がコスパが良いと思っています。

Amazon Rekognitionの料金について

公式サイトから引用しました。料金は以下のようになっています。
アダルト画像の判定APIは、「DetectFaces」になります。

グループAPI*最初の 1,000,000 ページ次の 4,000,000 ページ次の 30,000,000 ページ35,000,000 枚以上の画像
グループ 1CompareFaces
IndexFaces
SearchFacebyImage
SearchFaces
0.0013USD0.001USD0.0008USD0.0005USD
グループ 2DetectFaces
DetectModerationLabels
DetectLabels**
DetectText
RecognizeCelebrities
DetectPPE
0.0013USD0.001USD0.0008USD0.0003125USD
Image Properties***0.000975USD0.00075USD0.0006USD0.0002344USD
「アジアパシフィック(東京)」の料金表
https://aws.amazon.com/jp/rekognition/pricing/

Amazon Rekognitionを開始してから12カ月間は無料利用枠を利用でき、グループ 1 とグループ 2 の API で、それぞれで1カ月5000枚の画像を無料で分析することができます。

LaravelでAmazon Rekognitionを使って画像を分析する

環境構築やLaravelのインストールなどは割愛します。
ComposerインストールやLaravelコマンドを使用するなどして適当にプロジェクトを作成してください。

AWSアクセスキーを作成する

AWSのコンソールページへアクセスして、右のプルダウンメニューから「セキュリティ認証情報」をクリックしアクセス管理のユーザーをクリックします。

右上の「ユーザーを追加」ボタンをクリックしユーザーを作成します。

ユーザー名を指定して認証情報タイプを「アクセスキー・プログラムによるアクセス」にチェックを入れます。これにチェックを入れないとLaravelからのアクセスができません。

次のステップへ進むとアクセス許可の設定画面になります。
既定のポリシーを選択する形で「AmazonRekognitionFullAccess」の権限を設定します。

権限を追加したら、あとは確認画面なので適当に次へ進むのボタンをクリックしてユーザーを作成します。

ユーザーの作成が完了するとアクセスキーとシークレットキーが表示されます。
このキーは後ほどLaravelで使用するので忘れないようにメモしておきましょう。
.csv形式でダウンロードして保存しておくのが良いと思います。

AWS側の設定はこれで完了です。

AWS-SDKをインストールする

次はLaravel側で作業を行います。
まずは、Composerコマンドを使用してAWS-SDKをインストールします。

composer require aws/aws-sdk-php

envにアクセスキーを設定する

envファイルに先ほどAWSで作成したアクセスキー・シークレットキーを書き込みます。

AWS_ACCESS_KEY_ID=#ここにアクセスキーを入れます#
AWS_SECRET_ACCESS_KEY=#ここにシークレットキーを入れます#
AWS_DEFAULT_REGION=ap-northeast-1 #東京リージョンを指定します。

これでLaravelからAWSのAPIを叩く準備ができました。

Storageディレクトリに解析したい画像を設置する

解析した画像をStorageディレクトリに何枚か設置します。
今回はアダルト画像と一般的な風景画像の2枚を用意します。

出典:大人の素材
出典:pixabay

この2つの画像を使用してどうような結果が返ってくるか確認します。

Amazon Rekognitionに画像を送信するプログラムを記述する

まずは、コントローラーを作成します。

php artisan make:controller RekognitionController

作成したコントローラーにAWSへ画像分析をリクエストする処理を記述します。

<?php

namespace App\Http\Controllers;

use Aws\Rekognition\RekognitionClient;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;

class RekognitionController extends Controller
{


    public function upload(Request $request)
    {
        // バリデーション
        $request->validate(
            [
                'file' => 'required|file',
            ]
        );

        $file = $request->file('file');

        // ファイル保存する際の名前を作成する
        $filename = time() . '_' . $file->getClientOriginalName();

        // ファイルを保存する
        $result = $file->move(storage_path('app'), $filename);

        // アップロードの成功判定
        if ($result) {
            return 'アップロード成功';
        }else {
            return 'アップロード失敗';
        }
    }

    //
    public function rekognition($filename)
    {

        // 画像データを取得
        $image = Storage::disk('local')->get($filename);

        // オプションを定義
        $option = [
            'version' => 'latest',  // バージョン
            'region' => 'ap-northeast-1'    // APIのリージョン
        ];

        $client = new RekognitionClient($option);

        $result = $client->detectModerationLabels([
            'Image' => [
                'Bytes' => $image,
            ],
        ]);

        return view('result', compact('result'));
    }
    
    
}

結果を日本語にするためにconfigファイルを作成します。

<?php


return [

    /*
    |--------------------------------------------------------------------------
    | Top-Level Category
    |--------------------------------------------------------------------------
    |
    */
    'top_level_category' => [
        'Explicit Nudity' => '露骨なヌード',
        'Suggestive' => '挑発的な',
        'Violence' => '暴力',
        'Visually Disturbing' => '視覚的に邪魔',
        'Rude Gestures' => '失礼なジェスチャー',
        'Drugs' => '薬物',
        'Tobacco' => 'タバコ',
        'Alcohol' => 'アルコール',
        'Gambling' => 'ギャンブル',
        'Hate Symbols' => 'ヘイトシンボル',
    ],


    /*
    |--------------------------------------------------------------------------
    | Second-Level Category
    |--------------------------------------------------------------------------
    |
    */
    'second_level_category' => [
        'Explicit Nudity' => [
            'Nudity' => 'ヌード',
            'Graphic Male Nudity' => '生々しい男性のヌード',
            'Graphic Female Nudity' => '写実的な女性のヌード',
            'Sexual Activity' => '性行為',
            'Illustrated Explicit Nudity' => '露骨なヌードのイラスト',
            'Adult Toys' => '大人のおもちゃ',
        ],
        'Suggestive' => [
            'Female Swimwear Or Underwear' => '女性の水着や下着',
            'Male Swimwear Or Underwear' => '男性の水着または下着',
            'Partial Nudity' => '部分的なヌード',
            'Barechested Male' => 'はだしの男性',
            'Revealing Clothes' => '露出服',
            'Sexual Situations' => '性的状況',
        ],
        'Violence' => [
            'Graphic Violence Or Gore' => '生々しい暴力またはゴア',
            'Physical Violence' => '身体的暴力',
            'Weapon Violence' => '武器による暴力',
            'Weapons' => '兵器',
            'Self Injury' => '自己傷害',
        ],
        'Visually Disturbing' => [
            'Emaciated Bodies' => '衰弱した体',
            'Corpses' => '死体',
            'Hanging' => 'ハンギング',
            'Air Crash' => 'エアクラッシュ',
            'Explosions And Blasts' => '爆発と爆発',
        ],
        'Rude Gestures' => [
            'Middle Finger' => '中指',
        ],
        'Drugs' => [
            'Drug Products' => '医薬品',
            'Drug Use' => '薬物使用',
            'Pills' => '丸薬',
            'Drug Paraphernalia' => '麻薬関連器具',
        ],
        'Tobacco' => [
            'Tobacco Products' => 'たばこ製品',
            'Smoking' => '喫煙',
        ],
        'Alcohol' => [
            'Drinking' => '飲酒',
            'Alcoholic Beverages' => 'アルコール飲料',
        ],
        'Gambling' => [
            'Gambling' => 'ギャンブル',
        ],
        'Hate Symbols' => [
            'Nazi Party' => 'ナチ党',
            'White Supremacy' => '白人至上主義',
            'Extremist' => '過激派',
        ],
    ],


];

実行するため、ルートを指定します。

<?php

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Storage;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

// 画像アップロードページとアップロードされた画像一覧を表示する
Route::get('/', function () {
    $fileList = Storage::disk('local')->allFiles();
    return view('index', compact('fileList'));
});

// アップロード処理
Route::post('/upload', [\App\Http\Controllers\RekognitionController::class, 'upload'])->name('upload');

// 指定されたファイルをAmazon Rekognitionへ送信して画像分析結果を表示する
Route::get('/rekognition/{filename}', [\App\Http\Controllers\RekognitionController::class, 'rekognition'])->name('rekognition');

// Storage内の画像データをレスポンスする
Route::get('/image/{filename}', function ($filename) {
    return Storage::disk('local')->get($filename);
})->name('image');

表示部分を作成します。

トップページ

<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>ファイルアップロード</title>
</head>
<body>
<form action="{{ route('upload') }}" method="post" enctype="multipart/form-data">
    @csrf
    <input type="file" name="file" id="">
    <input type="submit" value="アップロード">
</form>
<ul>
    @foreach($fileList as $item)
        <li><a href="{{ route('rekognition', ['filename' => $item]) }}">{{ $item }}</a></li>
    @endforeach
</ul>
</body>
</html>

分析結果の表示ページ

<!doctype html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>画像分析結果</title>
</head>
<body>
    @if($result['ModerationLabels'])
        <h1 style="background-color: red; color: #fff;">
            @foreach($result['ModerationLabels'] as $key => $item)
                @if($item['ParentName'] == "")
                    {{ config('rekognition.top_level_category.' . $item['Name']) }}
                @else
                    {{ config('rekognition.second_level_category.' . $item['Name']) }}
                    {{ config('rekognition.top_level_category.' . $item['ParentName']) }}
                @endif
            @endforeach
        </h1>
    @else
        <h1 style="background-color: green;color: #fff;">不適切なコンテンツではありません。</h1>
    @endif
    <div style="display: flex">
        <img src="{{ route('image', ['filename' => request('filename')]) }}" alt="" style="width: 300px;">
        <textarea name="" id="" cols="100" rows="20">{{ var_dump($result, true) }}</textarea>
    </div>
</body>
</html>

ファイルをアップロードして分析結果を表示する

http://127.0.0.1:8000/にアクセスして画像ファイルをアップロードします。

トップページに戻ると先ほどアップロードしたファイルが表示されます。

そのファイル(リンク)をクリックして分析結果を表示します。

画像と結果が表示されれば完了です!

アダルト要素が含まれない画像の分析結果

出典:pixabay

object(Aws\Result)#493 (2) {
  ["data":"Aws\Result":private]=>
  array(3) {
    ["ModerationLabels"]=>
    array(0) {
    }
    ["ModerationModelVersion"]=>
    string(3) "6.0"
    ["@metadata"]=>
    array(4) {
      ["statusCode"]=>
      int(200)
      ["effectiveUri"]=>
      string(48) "https://rekognition.ap-northeast-1.amazonaws.com"
      ["headers"]=>
      array(4) {
        ["x-amzn-requestid"]=>
        string(36) "050a27db-3933-48b7-8256-9100ed271df9"
        ["content-type"]=>
        string(26) "application/x-amz-json-1.1"
        ["content-length"]=>
        string(2) "54"
        ["date"]=>
        string(29) "Thu, 15 Dec 2022 09:37:38 GMT"
      }
      ["transferStats"]=>
      array(1) {
        ["http"]=>
        array(1) {
          [0]=>
          array(0) {
          }
        }
      }
    }
  }
  ["monitoringEvents":"Aws\Result":private]=>
  array(0) {
  }
}

アダルトコンテンツ(不適切なコンテンツ)の分析結果

出典:大人の素材

object(Aws\Result)#492 (2) {
  ["data":"Aws\Result":private]=>
  array(3) {
    ["ModerationLabels"]=>
    array(3) {
      [0]=>
      array(3) {
        ["Confidence"]=>
        float(95.30232238769531)
        ["Name"]=>
        string(15) "Sexual Activity"
        ["ParentName"]=>
        string(15) "Explicit Nudity"
      }
      [1]=>
      array(3) {
        ["Confidence"]=>
        float(95.30232238769531)
        ["Name"]=>
        string(15) "Explicit Nudity"
        ["ParentName"]=>
        string(0) ""
      }
      [2]=>
      array(3) {
        ["Confidence"]=>
        float(67.88583374023438)
        ["Name"]=>
        string(6) "Nudity"
        ["ParentName"]=>
        string(15) "Explicit Nudity"
      }
    }
    ["ModerationModelVersion"]=>
    string(3) "6.0"
    ["@metadata"]=>
    array(4) {
      ["statusCode"]=>
      int(200)
      ["effectiveUri"]=>
      string(48) "https://rekognition.ap-northeast-1.amazonaws.com"
      ["headers"]=>
      array(4) {
        ["x-amzn-requestid"]=>
        string(36) "992c89b7-077b-42f4-97a6-9d685e36652a"
        ["content-type"]=>
        string(26) "application/x-amz-json-1.1"
        ["content-length"]=>
        string(3) "296"
        ["date"]=>
        string(29) "Thu, 15 Dec 2022 09:40:14 GMT"
      }
      ["transferStats"]=>
      array(1) {
        ["http"]=>
        array(1) {
          [0]=>
          array(0) {
          }
        }
      }
    }
  }
  ["monitoringEvents":"Aws\Result":private]=>
  array(0) {
  }
}

分析結果の比較

比較してみると不適切なコンテンツか否かは「ModerationLabels」内に配列が有るか無いかで決まります。
アダルト要素が含まれる画像には以下のように配列があります。

["ModerationLabels"]=>
    array(3) {
      [0]=>
      array(3) {
        ["Confidence"]=>
        float(95.30232238769531)
        ["Name"]=>
        string(15) "Sexual Activity"
        ["ParentName"]=>
        string(15) "Explicit Nudity"
      }
      [1]=>
      array(3) {
        ["Confidence"]=>
        float(95.30232238769531)
        ["Name"]=>
        string(15) "Explicit Nudity"
        ["ParentName"]=>
        string(0) ""
      }
      [2]=>
      array(3) {
        ["Confidence"]=>
        float(67.88583374023438)
        ["Name"]=>
        string(6) "Nudity"
        ["ParentName"]=>
        string(15) "Explicit Nudity"
      }
    }

各々のパラメーターについて解説します。

パラメーター説明
Confidence95.30232238769531ラベルが正しく識別されたという Amazon Rekognition の信頼度を指定します。
NameSexual Activity画像で検出された安全でないコンテンツのタイプのラベル名。
ParentNameExplicit Nudityラベルの名前。
階層の最上位のラベルには親ラベル
引用:Moderating content

ラベルについてはアダルトコンテンツ以外にタバコや飲酒・薬物・ヘイトなどがあります。
詳しくは公式ページをご確認ください。

画像解析は完全ではないので注意が必要

例えば、露出が高い競技などはアダルト画像であることが判定されることがあります。
具体的に相撲の画像が挙げられます。Amazon Rekognitionでの判定結果は以下です。

このようにアダルト要素が含まれていなくてもアダルト判定になってしまう場合があります。
精度は高いものの、まだ完全ではないので過信しすぎないようにするのが良いと見ています。

まとめ

やや長くなってしまいましたが、いかがでしょうか?Laravelで会員制サイトを作成する際にユーザーが画像をアップロードできる環境下である場合はAmazon Rekognitionなどの画像分析サービスを利用するのが良いと思います。ご不明な点などあれば是非コメントまたはお問い合わせください!