J'utilise généralement beaucoup Flask, mais ma connaissance a dit: "Fast API is good!", J'ai donc décidé de créer une simple API de reconnaissance d'image. Cependant, je n'ai pas vu beaucoup d'articles japonais sur FastAPI et ML, j'ai donc décidé de créer cet article au lieu d'un mémo!
Dans cet article, après avoir préparé l'environnement de développement, nous donnerons une brève explication du serveur API et du front-end.
Tout le code utilisé cette fois-ci est publié sur Github. ** (La structure des dossiers de l'implémentation ci-dessous est décrite sur l'hypothèse de Github. Le téléchargement de l'exemple de modèle est également décrit dans README.md.) **
C'est l'un des frameworks Python comme Flask.
Pour un aperçu simple et un résumé de son utilisation, veuillez vous référer à l'article suivant. (Merci beaucoup pour votre aide dans cet article aussi!)
https://qiita.com/bee2/items/75d9c0d7ba20e7a4a0e9
Pour ceux qui veulent en savoir plus, nous vous recommandons les tutoriels officiels Fast API!
https://fastapi.tiangolo.com/tutorial/
Je n'ai pas eu le temps cette fois, donc je vais le construire en utilisant le modèle de tensorflow.keras!
Plus précisément, nous utiliserons ResNet50 appris par imagenet tel quel et en déduire à laquelle des 1000 classes appartient l'image d'entrée.
(Le modèle que je voulais vraiment utiliser n'était pas à temps car j'apprenais à être acclamé en ce moment ...)
https://www.tensorflow.org/versions/r1.15/api_docs/python/tf/keras?hl=ja
Mac OS X Mojave Python3.7.1(Anaconda)
Installez la bibliothèque Python requise.
$pip install tensorflow==1.15
$pip install fastapi
$pip install uvicorn
Puisqu'il existe les conditions suivantes, installez également les bibliothèques nécessaires. --Rendu index.html
$pip install Jinja
$pip install aiofiles
$pip install python-multipart
$pip install opencv-python
La mise en œuvre du serveur API est la suivante.
# -*- coding: utf-8 -*-
import io
from typing import List
import cv2
import numpy as np
import tensorflow as tf
from tensorflow.keras.applications.resnet50 import decode_predictions
from fastapi import FastAPI, Request, File, UploadFile
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
#Préparation du modèle de reconnaissance d'image
global model, graph
graph = tf.get_default_graph()
model = tf.keras.models.load_model("./static/model/resnet_imagenet.h5")
#Préparation de Fast API
app = FastAPI()
# static/js/post.index js.Obligatoire pour appeler à partir de HTML
app.mount("/static", StaticFiles(directory="static"), name="static")
#Index stocké sous des modèles.Requis pour rendre le HTML
templates = Jinja2Templates(directory="templates")
def read_image(bin_data, size=(224, 224)):
"""Charger l'image
Arguments:
bin_data {bytes} --Données binaires d'image
Keyword Arguments:
size {tuple} --Taille d'image que vous souhaitez redimensionner(default: {(224, 224)})
Returns:
numpy.array --image
"""
file_bytes = np.asarray(bytearray(bin_data.read()), dtype=np.uint8)
img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, size)
return img
@app.post("/api/image_recognition")
async def image_recognition(files: List[UploadFile] = File(...)):
"""API de reconnaissance d'image
Keyword Arguments:
files {List[UploadFile]} --Informations sur le fichier téléchargé(default: {File(...)})
Returns:
dict --Résultat d'inférence
"""
bin_data = io.BytesIO(files[0].file.read())
img = read_image(bin_data)
with graph.as_default():
pred = model.predict(np.expand_dims(img, axis=0))
result_label = decode_predictions(pred, top=1)[0][0][1]
return {"response": result_label}
@app.get("/")
async def index(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
@app.post("/api/image_recognition")
async def image_recognition(files: List[UploadFile] = File(...)):
"""API de reconnaissance d'image
Keyword Arguments:
files {List[UploadFile]} --Informations sur le fichier téléchargé(default: {File(...)})
Returns:
dict --Résultat d'inférence
"""
bin_data = io.BytesIO(files[0].file.read())
img = read_image(bin_data)
with graph.as_default():
pred = model.predict(np.expand_dims(img, axis=0))
result_label = decode_predictions(pred, top=1)[0][0][1]
return {"response": result_label}
Cette fois, nous utilisons le fichier de téléchargement Fast API pour obtenir l'image POSTée.
bin_data = io.BytesIO(files[0].file.read())
Étant donné qu'un seul fichier est POSTÉ, il est défini en tant que fichiers [0], et comme il est passé au format BASE64 depuis le recto, il a été converti en un tableau d'octets du côté API.
def read_image(bin_data, size=(224, 224)):
"""Charger l'image
Arguments:
bin_data {bytes} --Données binaires d'image
Keyword Arguments:
size {tuple} --Taille d'image que vous souhaitez redimensionner(default: {(224, 224)})
Returns:
numpy.array --image
"""
file_bytes = np.asarray(bytearray(bin_data.read()), dtype=np.uint8)
img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
img = cv2.resize(img, size)
return img
Avec l'aide d'opencv, il convertit un tableau d'octets en une image uint8. À ce stade, comme le format par défaut d'opencv est BGR, je l'ai converti en RVB et l'ai redimensionné.
global model, graph
graph = tf.get_default_graph()
model = tf.keras.models.load_model("./static/model/resnet_imagenet.h5")
...
with graph.as_default():
pred = model.predict(np.expand_dims(img, axis=0))
result_label = decode_predictions(pred, top=1)[0][0][1]
J'ai créé resnet_imagenet.h5 à l'avance et l'ai lu en haut du fichier. Le processus d'inférence lui-même est déduit avec la fonction prédire en fixant le contexte de ce thread au graphe TensorFlow défini globalement avec with graph.as_default ().
Puisque nous utilisons ResNet50 de tf.keras cette fois, nous utilisons decode_predictions pour convertir le résultat de prédire en une étiquette pour obtenir le résultat de l'inférence.
Je pense que d'autres modèles et modèles personnalisés peuvent être utilisés comme cette implémentation en enregistrant le fichier .h5 quelque part dans le répertoire du projet et en le chargeant_model.
Je l'ai utilisé comme référence. (Je vous remercie!)
https://qiita.com/katsunory/items/9bf9ee49ee5c08bf2b3d
<html>
<head>
<meta http-qeuiv="Content-Type" content="text/html; charset=utf-8">
<title>Test de reconnaissance d'image Fastapi</title>
<script src="//code.jquery.com/jquery-2.2.3.min.js"></script>
<script src="/static/js/post.js"></script>
</head>
<body>
<!--Bouton de sélection de fichier-->
<div style="width: 500px">
<form enctype="multipart/form-data" method="post">
<input type="file" name="userfile" accept="image/*">
</form>
</div>
<!--Zone d'affichage de l'image-->
<canvas id="canvas" width="0" height="0"></canvas>
<!--Télécharger le bouton de démarrage-->
<button class="btn btn-primary" id="post">Publier</button>
<br>
<h2 id="result"></h2>
</body>
</html>
//Redimensionner l'image et l'afficher en HTML
$(function () {
var file = null;
var blob = null;
const RESIZED_WIDTH = 300;
const RESIZED_HEIGHT = 300;
$("input[type=file]").change(function () {
file = $(this).prop("files")[0];
//Contrôle de fichier
if (file.type != "image/jpeg" && file.type != "image/png") {
file = null;
blob = null;
return;
}
var result = document.getElementById("result");
result.innerHTML = "";
//Redimensionner l'image
var image = new Image();
var reader = new FileReader();
reader.onload = function (e) {
image.onload = function () {
var width, height;
//Redimensionner pour s'adapter au plus long
if (image.width > image.height) {
var ratio = image.height / image.width;
width = RESIZED_WIDTH;
height = RESIZED_WIDTH * ratio;
} else {
var ratio = image.width / image.height;
width = RESIZED_HEIGHT * ratio;
height = RESIZED_HEIGHT;
}
var canvas = $("#canvas").attr("width", width).attr("height", height);
var ctx = canvas[0].getContext("2d");
ctx.clearRect(0, 0, width, height);
ctx.drawImage(
image,
0,
0,
image.width,
image.height,
0,
0,
width,
height
);
//Obtenez des données d'image base64 à partir du canevas et créez un objet blob pour POST
var base64 = canvas.get(0).toDataURL("image/jpeg");
var barr, bin, i, len;
bin = atob(base64.split("base64,")[1]);
len = bin.length;
barr = new Uint8Array(len);
i = 0;
while (i < len) {
barr[i] = bin.charCodeAt(i);
i++;
}
blob = new Blob([barr], { type: "image/jpeg" });
console.log(blob);
};
image.src = e.target.result;
};
reader.readAsDataURL(file);
});
//Lorsque le bouton de démarrage du téléchargement est cliqué
$("#post").click(function () {
if (!file || !blob) {
return;
}
var name,
fd = new FormData();
fd.append("files", blob);
//POST à l'API
$.ajax({
url: "/api/image_recognition",
type: "POST",
dataType: "json",
data: fd,
processData: false,
contentType: false,
})
.done(function (data, textStatus, jqXHR) {
//Si la communication réussit, afficher le résultat
var response = JSON.stringify(data);
var response = JSON.parse(response);
console.log(response);
var result = document.getElementById("result");
result.innerHTML = "Cette image...「" + response["response"] + "Yanke";
})
.fail(function (jqXHR, textStatus, errorThrown) {
//Émet un message d'erreur si la communication échoue
var result = document.getElementById("result");
result.innerHTML = "La communication avec le serveur a échoué...";
});
});
});
Le POST est effectué sur l'API de reconnaissance d'image à l'aide d'ajax et le résultat est affiché.
En conséquence, cela fonctionne comme ça!
(Je voulais rendre la réception un peu plus à la mode ...)
J'ai créé une API de reconnaissance d'image pour étudier Fast API. Je ne pense pas que la mise en œuvre que j'ai faite cette fois-ci soit la meilleure pratique, mais je suis content d'avoir pu faire quelque chose qui fonctionne.
Je ne sais pas quel framework utiliser pour travailler à l'avenir, mais je pensais que FastAPI est relativement facile à utiliser et que je devrais passer de Flask.
** Enfin et surtout, merci à tous ceux qui nous ont aidés! ** **
Recommended Posts