Zapojte detekci objektů do své webové aplikace – 2. díl

6. 2. 2021 Azure, Programming

Předmětem předešlého článku bylo představení naší aplikace a využití služby Custom Vision pro detekci objektů. Zde se hlouběji ponoříme do kódu naší aplikace a ukážeme si klíčové části, které se vám při tvorbě podobné aplikace mohou hodit – implementace exportovaného modelu, tvorba boxů kolem objektů a práce s frameworkem Flask.

Požadavky

Instalace nezbytných balíčků

Námi zvolený exportovaný model používá jeden z nejznámějších frameworků pro hluboké učení tensorflow. Výhodný je pro nás především kvůli tomu, že takto exportovaný model je napsán v jazyku Python. Ten používá také framework pro tvorbu webových aplikací Flask, jenž využijeme na serveru aplikace. Zmíněné balíčky spolu s pár dalšími nainstalujeme v konzoli pomocí pip.

pip install tensorflow 
pip install flask
pip install Pillow
pip install numpy

Flask

Předností Flasku je především jeho jednoduchost. Jako první vytvoříme soubor applicaiton.py a uvnitř instanci Flask aplikace.

import flask
app = Flask(__name__)

Na konec souboru musíme ještě umístit podmínku, která bude aplikaci spouštět.

if __name__ == '__main__': 
      app.run() 

Mezi ně teď můžeme definovat routes – cesty, pomocí kterých můžeme v aplikaci uživatele přesměrovat. Každá z nich pak dokáže vykonávat jiný účel. Jako první musíme definovat základní cestu, jejíž instrukce se vykonají při příchodu uživatele na stránku.

@app.route('/'):
def home():
      ...
      return render_template('index.html') #vykreslí html dokument

Poslední důležitá věc, která musí zaznít, je fakt, že Flask požaduje pevně danou strukturu složek.

To bylo to nejpodstatnější, co budeme pro tento projekt potřebovat. Do budoucna se vám může hodit detailní dokumentace Flasku v angličtině. Teď se pojďme podívat, jak lze Flasku předat data uživatele.

Odeslání dat

V duchu dodržení rozumné délky tohoto článku se zde nebudu věnovat tomu, jakým způsobem používat na stránce webkameru tak, jak to dělá stránka naše. Jedná se jen o jeden z mnoha způsobů, jak snímek od uživatele získat. Pokud by vás tato nebo kterákoli jiná nezmíněná část aplikace zajímala, najdete kompletní okomentovaný kód na našem GitHubu.

Jako nejjednodušší metoda předání dat se nabízí odeslání přes formulář.

<form action="/detect" method="POST" id="send">
<input type="hidden" id="image" name="image"> </form>

Atribut action říká, kam bude uživatel po odeslání přesměrován. V našem případě to bude bude route, kterou jsme si definovali v backendu aplikace. Další atribut method pak definuje, jestli chceme data odeslat –⁠ „POST“ nebo přijmout –⁠ „GET“.

Form pak můžeme odeslat v HTML přidáním tlačítka:

 <button type="submit" form="send" value="image">Submit</button>

Pokud potřebujete před odesláním provést nějakou formu logiky nebo úpravu dat, nabízí se odeslání přes JavaScript. V případě naší aplikace je náš snímek uložen jako prvek canvas. Ten jako takový odeslat nejde, proto ho v JavaScriptu upravíme na vhodný formát a formulář odešleme přes něj. Nejdříve si vyžádáme potřebné prvky podle jejich id.

<script>
const inputImage = document.getElementById('image');
const formImage = document.getElementById('send'); 
const submit = document.getElementById('submit'); //element tlačítka

Abychom canvas mohli odeslat na server, musíme ho převést na informaci ve formátu base64, který převádí binární informaci na řetězec znaků. Tuto informaci přiřadíme prvku input uvnitř formuláře a odešleme.

submit.addEventListener("click", function(){ //událost se zavolá po stisknutí tlačítka
var dataURL = canvas.toDataURL(); //převede data 
inputImage.value = dataURL; //převedená data přiřadíme
formImage.submit(); }); //odešle vyplněný form

Přijmutí dat

Poté, co server přijme snímek v podobě řetězce, musíme nejprve odříznout metadata –⁠ zprávu popisující druh informace. Následně musíme dekódovat nazpět náš snímek tak, aby bylo možné s ním dále pracovat.

@app.route("/detect", methods=['POST', 'GET']) #methods říká jestli cheme data přijímat nebo odesílat
def detection():

    if request.method == 'POST':
        image_b64 = request.values['image'] #vyžádání obrázku z html form jako base64 data
        image_b64 = re.sub('^data:image/.+;base64,', '', image_b64)#nahradí metadata prazdným řetězcem
        image_data = BytesIO(base64.b64decode(image_b64)) #dekódovaní base64 dat

Poté můžeme snímek otevřít v modulu Pillow (PIL), jenž nabízí jednoduchou práci s obrázky.

image = Image.open(image_data)

Budeme ho používat i při vykreslovaní boxů kolem detekovaných objektů. Předtím ale musíme použít náš exportovaný model k získání predikcí o našem snímku.

Práce s exportovaným modelem

Po exportování a následném extrahování .zip souboru uvidíme následující soubory:

Složka s modelem

Soubor object_detection.py obsahuje definici třídy ObjectDetection, kterou dále používají funkce v predict.py k detekci a generování nejpravděpodobnějších značek uvedených v labels.txt. Soubor predict_v1.py je funkčně totožný s predict.py, ale je určen pro starší verze tensorflow 1.x. Proto ho můžeme smazat. Pokud vás zajímá podrobný rozbor toho, co se v souborech děje, najdete ho v tomto článku. Nám ale bude stačit funkce main ze souboru predict.py.

import sys #přidá složku python k prohledávaným složkám
sys.path.insert(1, python)
from predict import main

Ta jako svůj parametr přijme náš snímek a vrátí nám predikce v podobě „seznamu“ (list) „slovníků“ (dictionaries).

      image = Image.open(image_data)
      predictions = main(image_data)
      print("predictions: ", predictions)
      """
      predictions: [{'probability': 0.12775692, 
      'tagId': 0, 
      'tagName': 'Lightning', 
      'boundingBox': {'left': 0.29989651, 'top': 0.65226842, 'width': 
       0.3119592, 'height': 0.37785795}}]
      """

Abychom se dostali k určité hodnotě, musíme specifikovat pořadí detekovaného objektu, jehož je hodnota součástí a klíč hodnoty je: predictions[0]['tagId']

Nyní máme všechny potřebné informace k vykreslení boxů kolem objektů, které model detekoval.

Vykreslování boxů

Jako souřadnice pro nakreslení obdélníku nám poslouží hodnoty ve vnořeném slovníku s klíčem 'boundingBox'.

Ty jsou vyjádřeny v procentuálním poměru k rozměrům původního snímku. Abychom dostali jejich absolutní hodnoty, musíme vynásobit hodnoty ‚left‘ a ‚width‘ šířkou a ‚top‘ a ‚height‘ výškou snímku.

Pro vykreslení použijeme modul ImageDraw, který je stejně jako Image součástí modulu Pillow. Ten pak nabízí metodu rectangle, která přijímá jako souřadnice dvojici bodů (x0, y0) –⁠ levý horní roh a (x1, y1) –⁠ pravý dolní roh. Následně přidáme popisek s názvem předpokládaného objektu. Stejnou proceduru zopakujeme pro každý objekt v seznamu.

from PIL import ImageDraw
def draw_boxes(image, predictions):
      img_width, img_height = image.size #předání šířky výšky
      img = ImageDraw.Draw(image)

      for obj in predictions:
            #výpočet souřadnic
            x0 = obj['boundingBox']['left'] * img_width #horní levý roh
            y0 = obj['boundingBox']['top'] * img_height
            x1 = x0 + (obj['boundingBox']['width'] * img_width) #dolní pravý roh
            y1 = y0 + (obj['boundingBox']['height'] * img_height)
            shape = [(x0, y0), (x1, y1)]

            img.rectangle(shape, outline ="red") #nakreslí box
            img.text((x0 + 10, y0 + 10), obj['tagName'], fill="red") #přidá popisek

      return image
Výsledný obrázek

Upravený snímek uložíme a spolu s jmény dvou nejpravděpodobnějších portů pošleme na další stránku.

      image = draw_boxes(image, predictions)
      image = image.save(os.path.join(app.config['IMAGE_UPLOADS'], '{}.png'.format(predictions[0]['probability']))) #'probability' slouží jako unikatní jméno pro uložení.
    
      return render_template('choice.html', user_image=os.path.join(app.config['IMAGE_UPLOADS'], '{}.png'.format(predictions[0]['probability']))
,port1=predictions[0]['tagName'], port2=predictions[1]['tagName'])

Práce s proměnnými

Jako příklad toho, jak následně pracovat s odeslanými proměnnými, jsem zvolil opět kousek kódu naší aplikace. Flask používá pro proměnné posílané ze serveru na straně HTML symboly „{{ }}“. Ty se po spuštění nahradí proměnnými specifikovanými uvnitř.

<img class= "img" src="{{user_image}}" alt="fotka s oznacenymi boxy" >

<input type="hidden" id="port1" value="{{port1}}">
<input type="hidden" id="port2" value="{{port2}}">

Když teď máme data uložena uvnitř prvků input, můžeme je v JavaScriptu spojit spolu s údajem o požadované délce kabelu zadaným uživatelem a vyhledat řetězec automaticky v prohlížeči.

const port1 = document.getElementById('port1');
const port2 = document.getElementById('port2');
const search_text = document.getElementById('search_text');
const search = document.getElementById('search');

search.addEventListener("click", function(){ searsearch.addEventListener("click", function(){
search_text.value = port1.value + " to " + port2.value + ' ' + length.value + "m";
window.open("https://www.bing.com/search?q="+search_text.value);
});

Závěr

Tímto článkem jsem se snažil vypíchnout to nejdůležitější, co by se mohlo při vytváření webové aplikace s modelem Custom Vision hodit. Mnohé jsem z důvodu délky článku vynechal, a proto znovu doporučuji navštívit GitHub našeho projektu, kde najdete kód kompletní. Další důležitá část vytváření webových aplikací, kterou jsem vynechal, je hostování. Ta naše využívá možnost nasazení z GitHubu přímo do služby Azure Web Apps. O tomto tématu už jeden článek na našem blogu vznikl, proto ho taktéž doporučuji navštívit.

Custom Vision spolu s ostatními kognitivními službami skýtá jistě velký potenciál, jak využít moderní AI systémy bez jakékoliv hluboké expertizy. Stačí tak mít jen ten správný nápad. Doufám, že vám článek posloužil jako návod, jak svůj nápad proměnit s pár řádky kódu v realitu.