ComfyUIにおいてNSFW（不適切な画像）を検出してモザイク処理などする方法

# はじめに

Stable Diffusionで画像を生成する際に時々18禁など不適切な画像が生成されますね。これは一般に「[NSFW](https://ja.wikipedia.org/wiki/NSFW)」（Not safe for workの略）と呼ばれています。

diffusersで生成する場合は自動的に（勝手に！）NSFW検出が行われて真っ暗な画像になって困ることもあります。

逆にComfyUIを使う場合は普段そういう処理が一切行われないので、突然画像が真っ暗になる心配はないのですが、NSFWが出てきて不愉快になることもありますね。

これが自分で使う時ならまだしも、ComfyUIを核心としてウェブアプリに実装して人に使わせたら大変な場合もあるでしょう。

こういう時NSFWみたいに検出が必要となる場合が多少あるので、今回はComfyUIにおいてNSFWを検出するノードの実装をする方法を紹介したいと思います。


# 使用可能パッケージ

実は検索してみたらComfyUIにおいてNSFWの処理をするためのパッケージが沢山あるようです。ここで一部表示します。

<table>
<tr>
<td><strong>名前</strong></td>
<td><strong>githubリンク</strong></td>
</tr>
<tr>
<td>ComfyUI-NSFW-Detection</td>
<td><a href="https://github.com/trumanwong/ComfyUI-NSFW-Detection" target="_blank" rel="noopener">https://github.com/trumanwong/ComfyUI-NSFW-Detection</a></td>
</tr>
<tr>
<td>ComfyUI YetAnotherSafetyChecker</td>
<td><a href="https://github.com/BetaDoggo/ComfyUI-YetAnotherSafetyChecker" target="_blank" rel="noopener">https://github.com/BetaDoggo/ComfyUI-YetAnotherSafetyChecker</a></td>
</tr>
<tr>
<td>ComfyUI-safety-checker</td>
<td><a href="https://github.com/42lux/ComfyUI-safety-checker" target="_blank" rel="noopener">https://github.com/42lux/ComfyUI-safety-checker</a></td>
</tr>
<tr>
<td>ComfyUI Nodes for External Tooling</td>
<td><a href="https://github.com/Acly/comfyui-tooling-nodes" target="_blank" rel="noopener">https://github.com/Acly/comfyui-tooling-nodes</a></td>
</tr>
</table>

どれも似ているものですが、その中で今回「ComfyUI-NSFW-Detection」を紹介したいです。

他のパッケージではNSFWを検出すると真っ暗な画像に入れ替えられますが、ComfyUI-NSFW-Detectionでは入れ替える画像を自分で準備することができるので便利です。


# ComfyUI-NSFW-Detectionの使い方

まずはインストールです。簡単にComfyUI Managerでインストールできます。似ている名前のパッケージがあるので間違えないように注意です。

![comfyui-nsfw04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/88d4992c-e596-0453-b41b-14fe2380390f.png)

インストールが終わった後、次は実際に使ってみましょう。

ComfyUI-NSFW-Detectionを組み込んだ簡単なtxt2imgのワークフローはこんな感じとなります。

![comfyui-nsfw01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/7a2b6297-9165-4f23-9fa9-53ec271420ab.png)

このワークフローでは私が以前の記事で紹介した[自動翻訳`DeepTranslatorCLIPTextEncodeNode`ノード](https://qiita.com/phyblas/items/0c69ac370daa0aa01d15)と[jpgで保存できる`SaveImagePlus`ノード](https://qiita.com/phyblas/items/2f686cee2e0b82af274c)も含まれますが、勿論これを必要でない場合はデフォルトの翻訳無しの`CLIPTextEncode`とpng保存の`SaveImage`を使っても構いません。

ここで一番重要なところは`NSFWDetection`ノードです。

![comfyui-nsfw03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/4fdd9105-bf9f-05a2-13b1-a9b801721621.png)

`score`はどれくらい厳しく判断すると決める閾値です。0の場合はいつも通れることになりますが、1なら全部通れなくなります。普段は0.5くらいでいいですが、厳しくしたい場合は1に近い値にしてもいいです。

入力は2つ
- `image`: NSFWを審査したい画像
- `alternative_image`: NSFWだと判断された場合代わりに使う画像

代わりの画像はどの画像でもいいのですが、ここではモザイク処理が行われた画像を使うことにします。作り方は簡単です。組み込みの`ImageScale`ノードで16×16ピクセルに縮小させて、`nearest-exact`方法でもう一度拡大するのです。

こうすることで生成する時にもしNSFWが検出されたら自動的にモザイク処理されるが、普通に害のない画像だったらそのまま出力されます。


# 文字で伝える

ウェブアプリに使う時などただモザイク処理だけではどうしてこうなるかわからない場合もあるでしょう。ちょっと説明文を入れられたらいいと思って、おまけに文字を入れる方法も紹介しておきたいです。

そのために[Text Overlay](https://github.com/Munkyfoot/ComfyUI-TextOverlay)というノードを使うと便利です。これもまずインストールする必要があります。

こんな感じで`ImageScale`と`NSFWDetection`ノードの間に挿入します。

![comfyui-nsfw02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/e8877473-d1e1-6f93-e798-0287d4cf488a.png)

尚、日本語などを表示するためにまず使うフォントをComfyUIの`custom_nodes/ComfyUI-TextOverlay/fonts`フォルダに入れる必要があります。ここでは[`meiryoub.ttc`](https://github.com/yidas/fonts/blob/master/Meiryo/MEIRYOB.TTC)を使っています。


# ワークフロー

最後に以上の画像のワークフローです。これをコピーして.jsonに保存してComfyUIの中にロードして使ってみてください。

```json
{
  "1": {
    "inputs": {
      "ckpt_name": "meinamix_meinaV11.safetensors"
    },
    "class_type": "CheckpointLoaderSimple"
  },
  "2": {
    "inputs": {
      "width": 512,
      "height": 512,
      "batch_size": 1
    },
    "class_type": "EmptyLatentImage"
  },
  "3": {
    "inputs": {
      "from_translate": "japanese",
      "to_translate": "english",
      "add_proxies": false,
      "proxies": "",
      "auth_data": "",
      "service": "GoogleTranslator [free]",
      "text": "裸の猫耳少女",
      "Show proxy": "proxy_hide",
      "Show authorization": "authorization_hide",
      "clip": ["1", 1]
    },
    "class_type": "DeepTranslatorCLIPTextEncodeNode"
  },
  "4": {
    "inputs": {
      "from_translate": "japanese",
      "to_translate": "english",
      "add_proxies": false,
      "proxies": "",
      "auth_data": "",
      "service": "GoogleTranslator [free]",
      "text": "低品質",
      "Show proxy": "proxy_hide",
      "Show authorization": "authorization_hide",
      "clip": ["1", 1]
    },
    "class_type": "DeepTranslatorCLIPTextEncodeNode"
  },
  "5": {
    "inputs": {
      "seed": 696759150096688,
      "steps": 20,
      "cfg": 7,
      "sampler_name": "ddim",
      "scheduler": "ddim_uniform",
      "denoise": 1,
      "model": ["1", 0],
      "positive": ["3", 0],
      "negative": ["4", 0],
      "latent_image": ["2", 0]
    },
    "class_type": "KSampler"
  },
  "6": {
    "inputs": {
      "samples": ["5", 0],
      "vae": ["1", 2]
    },
    "class_type": "VAEDecode"
  },
  "7": {
    "inputs": {
      "upscale_method": "nearest-exact",
      "width": 16,
      "height": 16,
      "crop": "disabled",
      "image": ["6", 0]
    },
    "class_type": "ImageScale"
  },
  "8": {
    "inputs": {
      "upscale_method": "nearest-exact",
      "width": 512,
      "height": 512,
      "crop": "disabled",
      "image": ["7", 0]
    },
    "class_type": "ImageScale"
  },
  "9": {
    "inputs": {
      "text": "不適切な内容が\n検出されたため\nモザイク処理\nさせていただきます",
      "font_size": 50,
      "font": "meiryoub.ttc",
      "fill_color_hex": "#FFFFFF",
      "stroke_color_hex": "#FF0000",
      "stroke_thickness": 0.2,
      "padding": 16,
      "horizontal_alignment": "center",
      "vertical_alignment": "middle",
      "x_shift": 0,
      "y_shift": 0,
      "image": ["8", 0]
    },
    "class_type": "Text Overlay"
  },
  "10": {
    "inputs": {
      "score": 0.5,
      "image": ["6", 0],
      "alternative_image": ["9", 0]
    },
    "class_type": "NSFWDetection"
  },
  "11": {
    "inputs": {
      "filename_prefix": "txt2img",
      "file_type": "JPEG",
      "remove_metadata": false,
      "images": ["10", 0]
    },
    "class_type": "SaveImagePlus"
  }
}
```