[Python] J'ai créé un LINE Bot qui détecte les visages et effectue le traitement de la mosaïque.

Je veux un robot LINE capable de détecter le visage reflété en arrière-plan et d'effectuer le traitement de la mosaïque! J'ai donc créé un Bot avec Python qui vous permet de choisir le visage que vous souhaitez mosaïquer.

Cette fois, je l'ai implémenté en utilisant Flask, line-bot-sdk, heroku, OpenCV, Pillow, etc.

Si vous avez des problèmes ou des améliorations, veuillez commenter!

Environnement d'exploitation


Flask==1.1.2
gunicorn==20.0.4
line-bot-sdk==1.16.0
matplotlib==3.3.2
numpy==1.18.5
opencv-python-headless==4.2.0.32
Pillow==7.2.0

python-3.8.5

Ce qu'il ne faut pas écrire dans cet article

・ Créer un canal de bot LINE ・ Déployer sur heroku

La procédure est très simple à comprendre dans l'article suivant, veuillez donc vous y référer. J'ai créé un LINE BOT avec Python + Heroku --Qiita

Ceci est un article sur parrot return LINE Bot, mais la plupart des LINE Bot seront complétés si vous modifiez le code source et déployez avec la même procédure. Si vous êtes nouveau dans la création de LINE Bot avec Python, vous devriez essayer de faire retourner un parrot Bot une fois et voir si cela fonctionne correctement. Si vous êtes bloqué dans une erreur lorsque vous "reflétez les changements" dans l'article ci-dessus, ce site -be-a-git-repository /) peut être utile.

Ce que j'ai fait

Lorsque vous envoyez une image, elle détecte votre visage, l'indexe et la liste. Essayons la photo des affaires du personnel ministériel du cabinet Kan <source ici>. S__54403095.jpg

Si vous spécifiez un index, seule la face spécifiée sera renvoyée avec une mosaïque. Appliquons une mosaïque au ministre de l'Environnement Koizumi. S__54403096.jpg S__54403102.jpg

Vous pouvez également appliquer une mosaïque à une face autre que celle spécifiée. Appliquons une mosaïque à des ministres autres que le premier ministre Kan. S__54403099.jpg S__54403100.jpg Vous pouvez appliquer une mosaïque sur n'importe quel visage de cette manière.

Veuillez utiliser le code QR ci-dessous pour ajouter des amis à ce bot LINE. messageImage_1603772392474.jpg

Le code source a été envoyé sur GitHub et peut être téléchargé à partir de l'URL ci-dessous. GitHub - Kitsuya0828/face_mosaic_linebot: A LINE Bot which recognizes faces on the picture and blur any of them you like

Description du code source

Détection de visage et affichage de la liste de plusieurs visages

Préparez le chemin de l'image à lire et le chemin de la destination d'enregistrement.

src :Chemin de l'image à lire
desc :Chemin de destination

Enfin le visage est détecté. Cela dit, tout ce que vous avez à faire est de transmettre le fichier en cascade au classificateur de cascade OpenCV et de spécifier l'image en niveaux de gris.


#Spécification du chemin du fichier en cascade (classificateur des données d'apprentissage des fonctionnalités)
cascade_file = './cascade/haarcascade_frontalface_alt2.xml'

#Chargement d'image
image = cv2.imread(str(src))
#Convertir en échelle de gris
image_gs = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)

#Lecture du fichier de quantité de fonction pour la reconnaissance faciale
cascade = cv2.CascadeClassifier(cascade_file)
#Reconnaissance de visage
face_list = cascade.detectMultiScale(image_gs,
                                     scaleFactor=1.1,
                                     minNeighbors=1,
                                     minSize=(20,20))  #Ignorez les plages inférieures à 20 x 20 pixels. Pour éviter que l'arrière-plan ne soit confondu avec un visage

La liste face_list '' contient désormais les coordonnées du visage détecté. Ensuite, découpez l'image du visage en fonction des coordonnées où le visage a été détecté et affichez la liste à l'aide de matplotlib. Puisqu'il est agencé de manière à devenir une image carrée dans son ensemble, la partie restante sera remplie d'une image vierge ( white.jpg` ``).


length = len(face_list)
#Disposer les feuilles pm x pm dans une tuile
pm = 1
while pm**2 < length:
    pm += 1

#Lister les images dans les tuiles
fig, ax = plt.subplots(pm, pm, figsize=(10, 10))
fig.subplots_adjust(hspace=0, wspace=0)

for k in range(pm**2):
    i = k // pm  #Verticale
    j = k % pm  #côté
    ax[i, j].xaxis.set_major_locator(plt.NullLocator())
    ax[i, j].yaxis.set_major_locator(plt.NullLocator())
    if k < length:
        x,y,w,h = face_list[k]
        #Découpez une face à l'aide de l'accès au tableau
        #Le type d'image est un tableau de Numpy(numpy.ndarray).. Facile à utiliser
        face_img = image[y:y+h,x:x+w]
        face_img = np.asarray(face_img)
        face_img = cv2.resize(face_img, (300, 300), cv2.INTER_LANCZOS4)
        face_img = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB)
        ax[i, j].text(30, 60, str(pm*i+j), fontsize=30, color='red')
        ax[i, j].imshow(face_img)
    else:
        img = cv2.imread('./white.jpg')
        img = np.asarray(img)
        img = cv2.resize(img, (300, 300), cv2.INTER_LANCZOS4)
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        ax[i, j].imshow(img)

plt.savefig(desc)

Si la détection de visage est effectuée plusieurs fois, l'ordre des visages sortis sera différent à chaque fois, de sorte que le visage auquel l'utilisateur ne s'attendait pas sera mosaïqué. Par conséquent, il est nécessaire de sauvegarder les coordonnées détectées la première fois dans le fichier texte (spécifié par `` face_coordinates_path '') distingué par l'ID utilisateur dans l'ordre.


#Enregistrer le fichier texte des coordonnées de face
with open(face_coordinates_path, "w", encoding='utf-8') as f:
    for i in range(len(face_list)):
        f.write(" ".join([str(x) for x in face_list[i]]) + "\n")

Traitement de la mosaïque

Préparez les éléments suivants.

src :Chemin de l'image à lire
desc :Chemin de destination
numberslist :Liste des numéros saisis par l'utilisateur
face_list :  detect_and_Liste des coordonnées de visage reconnues par la gamme

Maintenant, appliquons une mosaïque en utilisant OpenCV. S'il correspond au numéro de visage saisi par l'utilisateur, le processus de mosaïque sera exécuté.


for i,f in enumerate(face_list):
    x,y,w,h = f
    if i not in numberslist:
        continue
    #Réduisez l'image recadrée au grossissement spécifié
    face_img = image[y:y+h,x:x+w]
    face_img = cv2.resize(face_img, (min(10,w//10),min(10,h//10)))
    #Restaurer l'image réduite à sa taille d'origine
    #Spécifiez comment redimensionner avec l'argument interpolation (cv.INTER_LINEAR a des coins en mosaïque discrets)
    face_img = cv2.resize(face_img,(w,h),interpolation=cv2.INTER_AREA)
    #Coller à l'image d'origine
    image[y:y+h,x:x+w] = face_img   

image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
im = Image.fromarray(image)
im.save(desc)

Envoyer l'image

Lors de l'envoi d'un message image avec LINE Bot, vous devez préparer les URL de l'image d'origine et de l'image d'aperçu.


main_image_path = MAIN_IMAGE_PATH.format(user_id)
preview_image_path = PREVIEW_IMAGE_PATH.format(user_id)

La méthode de base pour envoyer des images est la suivante.


#Envoyer l'image
image_message = ImageSendMessage(original_content_url=f"https://<Le nom de mon application>.herokuapp.com/{main_image_path}",
                                 preview_image_url=f"https://<Le nom de mon application>.herokuapp.com/{preview_image_path}",)

Code source complet

.
│  Aptfile
│  detect_and_lineup.py
│  main.py
│  mosaic.py
│  Procfile
│  requirements.txt
│  runtime.txt
│  white.jpg
│
├─cascade
│      haarcascade_frontalface_alt2.xml
│
└─static
    └─images
            Hoge
main.py
import os
from pathlib import Path
from typing import List
from flask import Flask, abort, request
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import (ImageMessage, ImageSendMessage, MessageEvent, TextMessage, TextSendMessage)

from detect_and_lineup import detect_and_lineup
from mosaic import mosaic

app = Flask(__name__,static_url_path="/static")

YOUR_CHANNEL_ACCESS_TOKEN = os.environ["YOUR_CHANNEL_ACCESS_TOKEN"]
YOUR_CHANNEL_SECRET = os.environ["YOUR_CHANNEL_SECRET"]

line_bot_api = LineBotApi(YOUR_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(YOUR_CHANNEL_SECRET)

SRC_IMAGE_PATH = "static/images/{}.jpg "
MAIN_IMAGE_PATH = "static/images/{}_main.jpg "
PREVIEW_IMAGE_PATH = "static/images/{}_preview.jpg "
FACE_COORDINATES_PATH = "{}.txt"

@app.route("/")
def hello_world():
    return "hello world!"


@app.route("/callback", methods=["POST"])
def callback():
    # get X-Line-Signature header value
    signature = request.headers["X-Line-Signature"]

    # get request body as text
    body = request.get_data(as_text=True)
    app.logger.info("Request body: " + body)

    # handle webhook body
    try:
        handler.handle(body, signature)
    except InvalidSignatureError:
        abort(400)
    return "OK"


@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
    profile = line_bot_api.get_profile(event.source.user_id)
    user_id = profile.user_id
    if event.message.text == 'La revue':
        line_bot_api.reply_message(
        event.reply_token, messages=[TextSendMessage(text="<Examiner l'URL du site>")]
        )
    else:    
        src_image_path = Path(SRC_IMAGE_PATH.format(user_id)).absolute()
        main_image_path = MAIN_IMAGE_PATH.format(user_id*2)
        preview_image_path = PREVIEW_IMAGE_PATH.format(user_id*2)
        face_coordinates_path = FACE_COORDINATES_PATH.format(user_id)

        numberslist = list(map(int,str(event.message.text).split()))

        with open(face_coordinates_path) as f:
            face_list = [list(map(int,s.strip().split())) for s in f.readlines()]

        mosaic(src=src_image_path, desc=Path(main_image_path).absolute(),numberslist=numberslist,face_list=face_list)
        mosaic(src=src_image_path, desc=Path(preview_image_path).absolute(),numberslist=numberslist,face_list=face_list)
        image_message = ImageSendMessage(
            original_content_url=f"https://<Le nom de mon application>.herokuapp.com/{main_image_path}",
            preview_image_url=f"https://<Le nom de mon application>.herokuapp.com/{preview_image_path}",
        )
        app.logger.info(f"https://<Le nom de mon application>.herokuapp.com/{main_image_path}")
        line_bot_api.reply_message(
            event.reply_token, messages=[image_message,TextSendMessage(text="Je suis désolé si tu n'aimes pas ça")]
        )
        src_image_path.unlink()
    

@handler.add(MessageEvent, message=ImageMessage)
def handle_image(event):
    message_id = event.message.id
    profile = line_bot_api.get_profile(event.source.user_id)
    user_id = profile.user_id

    src_image_path = Path(SRC_IMAGE_PATH.format(user_id)).absolute()
    main_image_path = MAIN_IMAGE_PATH.format(user_id)
    preview_image_path = PREVIEW_IMAGE_PATH.format(user_id)
    face_coordinates_path = FACE_COORDINATES_PATH.format(user_id)

    #Enregistrer l'image
    save_image(message_id, src_image_path)

    try:
        face_list = detect_and_lineup(src=src_image_path, desc=Path(main_image_path).absolute())
        detect_and_lineup(src=src_image_path, desc=Path(preview_image_path).absolute())
        #Envoyer l'image
        image_message = ImageSendMessage(
        original_content_url=f"https://<Le nom de mon application>.herokuapp.com/{main_image_path}",
        preview_image_url=f"https://<Le nom de mon application>![S__54403102.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/832523/9269110c-f218-c528-858d-e3223283b588.jpeg)
.herokuapp.com/{preview_image_path}",
        )
        app.logger.info(f"https://alvinface2.herokuapp.com/{main_image_path}")
        line_bot_api.reply_message(event.reply_token, messages=[image_message, TextSendMessage(text="Pouvez-vous entrer le numéro du visage que vous souhaitez mosaïquer, séparé par des espaces demi-largeur?\n Exemple) Je souhaite appliquer une mosaïque sur les 1ère et 3ème faces\n ☞ Entrez "1 3"\n\n'-1'Vous pouvez également spécifier le numéro du visage que vous souhaitez conserver en le préfixant.\n Exemple) Je souhaite appliquer une mosaïque à des faces autres que N ° 0 et N ° 2.\n☞「-Entrez 1 0 2 "")])

        #Enregistrer le fichier texte des coordonnées de face
        with open(face_coordinates_path, "w", encoding='utf-8') as f:
            for i in range(len(face_list)):
                f.write(" ".join([str(x) for x in face_list[i]]) + "\n")
        
    except Exception:
        line_bot_api.reply_message(
        event.reply_token, TextSendMessage(text='Je n'ai pas pu trouver un visage reconnaissable')
        )


def public_attr(obj) -> List[str]:
    return [x for x in obj.__dir__() if not x.startswith("_")]


def save_image(message_id: str, save_path: str) -> None:
    """sauvegarder"""
    message_content = line_bot_api.get_message_content(message_id)
    with open(save_path, "wb") as f:
        for chunk in message_content.iter_content():
            f.write(chunk)


if __name__ == "__main__":
    port = int(os.environ.get("PORT", 5000))
    app.run(host="0.0.0.0", port=port)

detect_and_lineup.py
import numpy as np
from matplotlib import pyplot as plt
from PIL import Image, ImageDraw, ImageFont
import cv2

def detect_and_lineup(src: str, desc: str) -> None:
    """Rechercher et lister les visages
    :params src:
Chemin de l'image à lire
    :params desc:
Chemin de destination
    """

    #Spécification du chemin du fichier en cascade (classificateur des données d'apprentissage des fonctionnalités)
    cascade_file = './cascade/haarcascade_frontalface_alt2.xml'

    #Chargement d'image
    image = cv2.imread(str(src))
    #Convertir en échelle de gris
    image_gs = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)

    #Lecture du fichier de quantité de fonction pour la reconnaissance faciale
    cascade = cv2.CascadeClassifier(cascade_file)
    #Reconnaissance de visage
    face_list = cascade.detectMultiScale(image_gs,
                                        scaleFactor=1.1,
                                        minNeighbors=1,
                                        minSize=(20,20))  #Ignorez les plages inférieures à 20 x 20 pixels. Pour éviter que l'arrière-plan ne soit confondu avec un visage
    
    length = len(face_list)
    #Disposer les feuilles pm x pm dans une tuile
    pm = 1
    while pm**2 < length:
        pm += 1
    
    #Lister les images dans les tuiles
    fig, ax = plt.subplots(pm, pm, figsize=(10, 10))
    fig.subplots_adjust(hspace=0, wspace=0)

    for k in range(pm**2):
        i = k // pm  #Verticale
        j = k % pm  #côté
        ax[i, j].xaxis.set_major_locator(plt.NullLocator())
        ax[i, j].yaxis.set_major_locator(plt.NullLocator())
        if k < length:
            x,y,w,h = face_list[k]
            #Découpez une face à l'aide de l'accès au tableau
            #Le type d'image est un tableau de Numpy(numpy.ndarray).. Facile à utiliser
            face_img = image[y:y+h,x:x+w]
            face_img = np.asarray(face_img)
            face_img = cv2.resize(face_img, (300, 300), cv2.INTER_LANCZOS4)
            face_img = cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB)
            ax[i, j].text(30, 60, str(pm*i+j), fontsize=30, color='red')
            ax[i, j].imshow(face_img)
        else:
            img = cv2.imread('./white.jpg')
            img = np.asarray(img)
            img = cv2.resize(img, (300, 300), cv2.INTER_LANCZOS4)
            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
            ax[i, j].imshow(img)

    plt.savefig(desc)
    return face_list
mosaic.py
import numpy as np
from matplotlib import pyplot as plt
from PIL import Image, ImageDraw, ImageFont
import cv2

def mosaic(src: str, desc: str, numberslist=[], face_list=[]) -> None:
    """
    :params src:
Chemin de l'image à lire
    :params desc:
Chemin de destination
    :numberslist:
Liste des numéros saisis par l'utilisateur
    :face_list:
        detect_and_Liste des coordonnées de visage reconnues par la gamme
    """

    #Chargement d'image
    image = cv2.imread(str(src))

    #Lorsque l'utilisateur spécifie le numéro du visage qu'il souhaite conserver
    new_numberslist = []
    if numberslist[0] == -1:
        for num in range(len(face_list)):
            if num not in numberslist:
                new_numberslist.append(num)
    numberslist = new_numberslist

    for i,f in enumerate(face_list):
        x,y,w,h = f
        if i not in numberslist:
            continue
        #Réduisez l'image recadrée au grossissement spécifié
        face_img = image[y:y+h,x:x+w]
        face_img = cv2.resize(face_img, (min(10,w//10),min(10,h//10)))
        #Restaurer l'image réduite à sa taille d'origine
        #Spécifiez comment redimensionner avec l'argument interpolation (cv.INTER_LINEAR a des coins en mosaïque discrets)
        face_img = cv2.resize(face_img,(w,h),interpolation=cv2.INTER_AREA)
        #Coller à l'image d'origine
        image[y:y+h,x:x+w] = face_img   

    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    im = Image.fromarray(image)
    im.save(desc)
    return True

Où j'ai trébuché

Erreur Heroku + OpenCV

Lorsque j'ajoute opencv-python à requirements.txt et que je le déploie sur Heroku même si le programme a pu s'exécuter dans l'environnement local

ImportError: libSM.so.6: cannot open shared object file: No such file or directory

J'obtiens une erreur comme celle-ci. J'ai pu résoudre l'erreur en faisant deux choses:

① Ajouter des buildpacks (Heroku) et Aptfile

Ajoutez https://github.com/heroku/heroku-buildpack-apt aux buildpacks de Heroku et ajoutez le fichier Aptfile à votre dossier de projet. ..

Aptfile


libsm6
libxrender1
libfontconfig1
libice6

Il n'y a aucun problème si vous suivez l'article ci-dessous. Utilisation d'OpenCV avec heroku [Python3] --Qiita

② Utilisez opencv-python-headless

opencv-python-headless==4.2.0.32

J'ai trouvé une personne qui a le même problème sur le site suivant et a pu le résoudre. https://stackoverflow.com/questions/49469764/how-to-use-opencv-with-heroku/51004957

Sommaire

J'étais très ennuyé par l'incompatibilité entre Heroku et OpenCV, mais j'ai réussi à y arriver. Pourquoi ne créez-vous pas un bot LINE qui vous semble utile si vous avez des idées? Ayez une bonne vie de programmation.

Références

☟ Il est vraiment facile de comprendre comment créer un bot LINE en utilisant Python. J'ai créé un LINE BOT avec Python + Heroku --Qiita

☟ La méthode d'enregistrement et d'envoi des images a été très utile. [Python] J'ai créé un robot LINE qui date les photos --Qiita

Recommended Posts

[Python] J'ai créé un LINE Bot qui détecte les visages et effectue le traitement de la mosaïque.
J'ai essayé de faire LINE BOT avec Python et Heroku
J'ai créé un Line Bot qui utilise Python pour récupérer les e-mails non lus de Gmail!
En Python, j'ai créé un LINE Bot qui envoie des informations sur le pollen à partir des informations de localisation.
J'ai créé Chatbot en utilisant l'API LINE Messaging et Python
J'ai créé Chatbot en utilisant l'API LINE Messaging et Python (2) ~ Server ~
J'ai créé un robot Line qui devine le sexe et l'âge d'une personne à partir de l'image
J'ai créé un bot Discord en Python qui se traduit quand il réagit
J'ai fait un robot de remplacement de tampon avec une ligne
J'ai créé un Bot LINE avec Serverless Framework!
Made Mattermost Bot avec Python (+ Flask)
[Python] J'ai créé un robot qui me dit la température actuelle lorsque j'entre un nom de lieu sur LINE
J'ai créé un robot LINE qui envoie des images recommandées tous les jours à l'heure
[AWS] J'ai créé un BOT de rappel avec LINE WORKS
J'ai fait un Twitter BOT avec GAE (python) (avec une référence)
J'ai créé un bot de livre de compte de ménage avec LINE Bot
J'ai créé une VM qui exécute OpenCV pour Python
J'ai fait un texte Python
Traitement d'image avec Python (j'ai essayé de le binariser en art mosaïque 0 et 1)
J'ai fait un robot discord
unixtime ← → J'ai essayé de créer une classe qui effectue facilement la conversion datetime
Je souhaite envoyer un message de Python à LINE Bot
[AWS] J'ai créé un BOT de rappel avec LINE WORKS (implémentation)
J'ai fait un Line-bot avec Python!
J'ai fait une loterie avec Python.
J'ai créé un démon avec Python
J'ai fait un package qui peut comparer des analyseurs morphologiques avec Python
J'ai créé un robot Twitter qui marmonne le Pokémon capturé par #PokemonGO
J'ai fait un shuffle qui peut être réinitialisé (inversé) avec Python
[LINE Messaging API] Créez un BOT qui se connecte à quelqu'un avec Python
J'ai fait un bot mou qui m'informe de la température
[python] J'ai créé une classe qui peut écrire rapidement une arborescence de fichiers
[Python] J'ai fait un décorateur qui ne semble pas avoir d'utilité.
J'ai fait un programme de gestion de la paie en Python!
J'ai créé une application Web en Python qui convertit Markdown en HTML
J'ai fait un compteur de caractères avec Python
Création d'un bot LINE ~ Création, déploiement et lancement ~
[Python] J'ai créé un utilitaire qui peut accéder au type dict comme un chemin
J'ai fait une carte hexadécimale avec Python
J'ai créé un outil qui facilite un peu la décompression avec CLI (Python3)
[IOS] J'ai créé un widget qui affiche la tendance de Qiita dans Pythonista3. [Python]
Après avoir étudié Python3, j'ai créé un Slackbot
J'ai fait un jeu rogue-like avec Python
J'ai créé un bot LINE qui me dit le type et la force de Pokémon dans la région de Garal avec Heroku + Flask + PostgreSQL (Heroku Postgres)
J'ai fait un simple blackjack avec Python
J'ai créé un fichier de configuration avec Python
J'ai fait un simulateur de neurones avec Python
J'ai fait un module PyNanaco qui peut charger des crédits nanaco avec python
[Python] J'ai créé un code de scraping web qui acquiert automatiquement le titre de l'actualité et l'URL de Nihon Keizai Shimbun.
J'ai créé un programme en Python qui lit les données FX CSV et crée un grand nombre d'images de graphiques
J'ai créé un système qui décide automatiquement de s'exécuter demain avec Python et l'ajoute à Google Agenda.
[Python] J'ai créé un script qui coupe et colle automatiquement les fichiers du PC local sur un SSD externe.
J'ai créé une image Docker qui peut appeler FBX SDK Python à partir de Node.js
Je veux exécuter et distribuer un programme qui redimensionne les images Python3 + pyinstaller
[Pour les débutants] J'ai fait un capteur humain avec Raspberry Pi et notifié LINE!
Une histoire à laquelle j'étais accro après la communication SFTP avec python
J'ai créé un fichier de dictionnaire python pour Neocomplete