BOOTHのサムネをスクレイピングしてOpenCVで顔検出したら精度がよすぎてビビった

　[BOOTH](https://booth.pm/ja)はたくさんの販売アバターが売られていることで知られています。2019/12/09時点で、[「3Dモデル」タグ](https://booth.pm/ja/browse/3D%E3%83%A2%E3%83%87%E3%83%AB)には11,527点のモデルがあります。もちろんこれはアバターに関係ない素材なども大量に含まれているので、これがそのままアバターの数という訳ではありません。[KingYoSun](https://twitter.com/knotted221)さんが公開してくださっている[VRCモデルデータベース](https://www.vrc-model-database.com/)には約1,600体のモデルが登録されていますが、現状これが最も妥当だと思います。

　これを、サムネイルの画像から判別することはできないでしょうか。顔を認識させることでできそうですが、顔だけを独立した画像として取得することはできないでしょうか。

# スクレイピング

　という訳でまずはスクレイピングします。さすがにコピペで動くコードを載せるのはアレなのでURLだけ隠蔽しています。

```Python
import urllib.request as ur
from bs4 import BeautifulSoup
import requests

images = []

def img_save(img_url,title):
    url = img_url
    file_name = str(len(images)) + ".jpg"
    labeled_name = str(len(images)) + "___" + title + ".jpg"
    response = requests.get(url)
    image = response.content
    #こっちはただの連番
    with open("data/" + file_name, "wb") as o:
        o.write(image)
    #こっちはタイトルつき
    with open("labeled_data/" + labeled_name, "wb") as o:
        o.write(image)

def img_search(url_data):
    url = url_data
    html = ur.urlopen(url)
    soup = BeautifulSoup(html, "html.parser")
    title = str(soup.title.text)
    char_list = ["/","'",'"',"*","|","<",">","?","\\"," - BOOTH"]
    for c in char_list:
        title = title.replace(c,"")
    print(title)
    for s in soup.find_all("img"):
        if str(s).find("market") > 0:
            img_url = s.get("src")
            if img_url is not None:
                print(img_url)
                images.append(img_url)
                img_save(img_url,title)
                break

def page_access(page_number):
    url = page_number
    html = ur.urlopen(url)
    soup = BeautifulSoup(html, "html.parser")
    for s in soup.find_all("a"):
        if str(s).find("item-card__title-anchor") > 0:
            print (s.get("href"))
            url = s.get("href")
            img_search(url)

for i in range(1,240):
    url = "***載せられないよ***" + str(i)
    page_access(url)
```

　こうして得られた結果が以下です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/e61ce442-54a0-2362-b792-14500635ca0b.png)

　約11,000枚あります。

#顔検出

　OpenCVのライブラリを使って顔検出をします。

```Python
import cv2

sample = 11000

for i in range(sample):
    file_name = 'data/' + str(i+1) + '.jpg'
    img = cv2.imread(file_name)
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    cascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
    faces = cascade.detectMultiScale(img_gray,minSize=(100, 100))
    color = (0, 0, 0)
    print(faces)
    if len(faces) > 0:
        for rect in faces:
            cv2.rectangle(img, tuple(rect[0:2]),tuple(rect[0:2]+rect[2:4]), color, thickness=10)
        output_path = "face_detect/" + str(i+1) + ".jpg"
        cv2.imwrite(output_path, img)
```

　顔検出モデルは別途DLしてローカルに並べる必要があります。上のコードでいう`haarcascade_frontalface_default.xml`ですね。[OpenCVのgithub](https://github.com/opencv/opencv)からDLできます。

　結果は以下です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/b9d7a928-c047-4d40-4636-90746874c999.png)

　全然精度が良くないよ！　顔を見逃していたり、逆に違うものを誤認識していたりしています。

#アニメ顔専用モデル

　これは顔検出モデルが実写の顔を想定したものであるためです。探してみると[アニメ顔検出用モデル](https://github.com/nagadomi/lbpcascade_animeface)を作成されている方がいました。神か。それで再実行してみます。

```Python
import cv2

sample = 11000

for i in range(sample):
    file_name = 'data/' + str(i+1) + '.jpg'
    img = cv2.imread(file_name)
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    cascade = cv2.CascadeClassifier("lbpcascade_animeface.xml") #ここが変わっている
    faces = cascade.detectMultiScale(img_gray,minSize=(100, 100))
    color = (0, 0, 0)
    print(faces)
    if len(faces) > 0:
        for rect in faces:
            cv2.rectangle(img, tuple(rect[0:2]),tuple(rect[0:2]+rect[2:4]), color, thickness=10)
    output_path = "face_detect/real" + str(i+1) + ".jpg"
    cv2.imwrite(output_path, img)
```

　実行結果。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/66f6a1f1-dffc-e639-af1d-46802cff8b4b.png)

　精度高すぎィ！

　この検出結果を元にトリミングします。

```Python
import cv2

sample = 11000
count = 1

for i in range(sample):
    file_name = 'data/' + str(i+1) + '.jpg'
    img = cv2.imread(file_name)
    img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    classifier = cv2.CascadeClassifier("lbpcascade_animeface.xml")
    faces = classifier.detectMultiScale(img_gray, minSize=(100, 100))
    print(faces)
    if len(faces) > 0:
        for x,y,w,h in faces:
            face_image = img[y:y+h, x:x+w]
            output_path = 'face_trim/' + str(count) + '.jpg'
            cv2.imwrite(output_path,face_image)
            count += 1
```
　実行結果。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/3094f4d5-d326-836b-5a99-8064b16a1e3f.png)

　……アバターが多すぎてめまいがしてきた。

# 今後の展望

　せっかく大量の顔アイコンが得られたので、[先日やった手法](https://qiita.com/hibit/items/8f0525ab1b616061c630)を用いたらやはり亡霊しかできなかったので、GAN等の手法を使わないと面白い絵は出てこなさそうです。勉強します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/288125/4f9e1c00-24bc-ff4d-257c-c7d72ef0ea12.png)

　画像は約3,000枚ほど生成されましたが、1つのサムネに複数の顔があったり、結構な数の専用服（つまりサムネには顔がある）が売られていたりするので、実際のアバターはもっと少ないはずです。約半分として、冒頭に述べた約1,600点というのが妥当な数値かなと思われます。文字認識（サムネには売り文句とかも結構かかれています）と組み合わせたら面白そうだなと思いましたが、将来の課題としたいです。

　あと、ランダムに顔だけを表示して、大量の販売アバターの中から好みの顔のアバターを検索しやすいWebサービスとか作れたら面白いかもしれないですね。

# お世話になった記事

[【入門者向け解説】openCV顔検出の仕組と実践(detectMultiScale)](https://qiita.com/FukuharaYohei/items/ec6dce7cc5ea21a51a82)
[OpenCVでアニメの顔検出](https://qiita.com/mczkzk/items/fda37ac4f9ddab2d7f45)