# Import potrebných balíčkov
import os
import sys
from IPython.display import Image, clear_output
from PIL import Image as PilImage
import xml.etree.ElementTree as ET
import shutil
import re
re_image = re.compile(".+\.(jpeg|jpg|gif|png)$")
sys.path.append('yolov3')
# Dátovú množinu stiahneme z github repozitára
!git clone https://github.com/experiencor/raccoon_dataset
# Naklonujeme potrebný kód z github repozitára
!git clone https://github.com/ultralytics/yolov3
# Uistíme sa, že máme všetky potrebné dáta
!mkdir -p samples_raccoon
!wget -nc -O samples_raccoon/raccoon_example.jpg https://www.dropbox.com/s/qca8pk3x7ouvoo3/raccoon_example.jpg?dl=1
!mkdir -p outputs_raccoon
!mkdir -p samples_vehical
!wget -nc -O samples_vehical/vehical_example.jpg https://www.dropbox.com/s/c5097zbvr7xeibv/vehical_example.jpg?dl=1
!mkdir -p outputs_vehical
!wget -nc -O yolov3-spp.weights https://www.dropbox.com/s/ditxme19ikyggdt/yolov3-spp.weights?dl=1
!mkdir -p cfg; mkdir -p data; mkdir -p weights
!cp yolov3/data/coco.data data/coco.data; \
cp yolov3/data/coco.names data/coco.names; \
cp yolov3/cfg/yolov3-spp.cfg cfg/yolov3-spp.cfg
#@title --- Pomocný zdrojový kód ---
from IPython.core.magic import register_line_cell_magic
import cv2
import random
from utils.utils import plot_one_box
from google.colab.patches import cv2_imshow
def show_images(directory):
for filename in os.listdir(directory):
if re_image.match(filename):
print("{}:".format(filename))
img = Image(filename=os.path.join(directory, filename), width=600)
display(img)
@register_line_cell_magic
def writetemplate(line, cell):
with open(line, 'w') as f:
f.write(cell.format(**globals()))
def show_annot_image(img_filename, annot_filename, classes):
colors = [[random.randint(0, 255) for _ in range(3)] for _ in range(len(classes))]
objs = []
with open(annot_filename) as file:
for line in file.readlines():
line = line.strip()
if not len(line):
continue
parts = line.split(' ')
if len(parts) != 5:
raise RuntimeError('The annotation file is malformed.')
c, [xc, yc, w, h] = int(parts[0]), [float(xx) for xx in parts[1:]]
objs.append((c, xc, yc, w, h))
img = cv2.imread(img_filename)
width, height = img.shape[1], img.shape[0]
for (c, xc, yc, w, h) in objs:
xmin = (xc - w/2) * width
ymin = (yc - h/2) * height
xmax = (xc + w/2) * width
ymax = (yc + h/2) * height
plot_one_box((xmin, ymin, xmax, ymax),
img, label=classes[c],
color=colors[c])
cv2_imshow(img)
YOLO_CFG = """
[net]
# Testing
#batch=1
#subdivisions=1
# Training
batch=16
subdivisions=1
width=416
height=416
channels=3
momentum=0.9
decay=0.0005
angle=0
saturation = 1.5
exposure = 1.5
hue=.1
learning_rate={LEARNING_RATE}
burn_in={BURN_IN}
max_batches = 500200
policy=steps
steps=400000,450000
scales=.1,.1
[convolutional]
batch_normalize=1
filters=32
size=3
stride=1
pad=1
activation=leaky
# Downsample
[convolutional]
batch_normalize=1
filters=64
size=3
stride=2
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=32
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
# Downsample
[convolutional]
batch_normalize=1
filters=128
size=3
stride=2
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=128
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=64
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=128
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
# Downsample
[convolutional]
batch_normalize=1
filters=256
size=3
stride=2
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
# Downsample
[convolutional]
batch_normalize=1
filters=512
size=3
stride=2
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
# Downsample
[convolutional]
batch_normalize=1
filters=1024
size=3
stride=2
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=1024
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=1024
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=1024
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=1024
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
######################
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky
[convolutional]
batch_normalize=1
filters=512
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=1024
activation=leaky
[convolutional]
size=1
stride=1
pad=1
filters={YOLO_FILTERS}
activation=linear
[yolo]
mask = 6,7,8
anchors = {ANCHORS}
classes={NUM_CLASSES}
num={YOLO_NUM}
jitter=.3
ignore_thresh = {IGNORE_THRESH}
truth_thresh = 1
random=1
[route]
layers = -4
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[upsample]
stride=2
[route]
layers = -1, 61
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky
[convolutional]
batch_normalize=1
filters=256
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=512
activation=leaky
[convolutional]
size=1
stride=1
pad=1
filters={YOLO_FILTERS}
activation=linear
[yolo]
mask = 3,4,5
anchors = {ANCHORS}
classes={NUM_CLASSES}
num={YOLO_NUM}
jitter=.3
ignore_thresh = {IGNORE_THRESH}
truth_thresh = 1
random=1
[route]
layers = -4
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[upsample]
stride={STRIDE}
[route]
layers = {ROUTE_LAYERS}
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=256
activation=leaky
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=256
activation=leaky
[convolutional]
batch_normalize=1
filters=128
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
size=3
stride=1
pad=1
filters=256
activation=leaky
[convolutional]
size=1
stride=1
pad=1
filters={YOLO_FILTERS}
activation=linear
[yolo]
mask = 0,1,2
anchors = {ANCHORS}
classes={NUM_CLASSES}
num={YOLO_NUM}
jitter=.3
ignore_thresh = {IGNORE_THRESH}
truth_thresh = 1
random=1
"""
V tomto notebook-u si uvedieme jednoduchý praktický príklad na vizuálnu detekciu objektov pomocou metódy YOLOv3. Použijeme existujúcu implementáciu metódy z GitHub repozitára https://github.com/ultralytics/yolov3. Implementácia nevyužíva balíčky keras
a tensorflow
s ktorými pracujeme zvyčajne ale balíček torch
. Keďže sú však všetky funkcie, s ktorými budeme pracovať, obalené v ďalších rozhraniach špecifických pre YOLO, nebude na tom príliš záležať.
Na začiatok využijeme sieť s váhami predtrénovanými na dátovej množine COCO. Ide o známu dátovú množinu určenú na vizuálnu detekciu objektov.
Súbory, ktoré chceme otestovať, sú uložené v adresári samples_vehical
. Môžeme medzi ne v tomto kroku pridať aj ľubovoľné ďalšie súbory. Na začiatok si ich zobrazme:
show_images("samples_vehical")
Výstupom detekcie budú súbory s vyznačenými objektami, ktoré sa uložia v priečinku output_vehical
. V prípade, že výsledky detekcie plánujeme ďalej použiť, je samozrejme možné informáciu o objektoch a ich pozícii v obraze získať aj vo forme textového súboru alebo – po miernej úprave zdrojového kódu – ju použiť aj ľubovoľným iným spôsobom.
Aby sme vykonali detekciu, spustíme skript detect.py
z priečinka yolov3
s príslušnými argumentmi:
!{sys.executable} yolov3/detect.py \
--source samples_vehical \
--output output_vehical \
--cfg cfg/yolov3-spp.cfg --data data/coco.data \
--weight yolov3-spp.weights \
--img-size 608
Výsledok detekcie si opäť môžeme zobraziť pomocou pomocnej funkcie show_images
:
show_images("output_vehical")
Dátová množina COCO obsahuje 80 rôznych typov objektov. Nie je to málo, ale je to podstatne menej než napríklad v prípade dátovej množiny ImageNet, ktorá je určená na klasifikáciu. Ak skúsime predtrénovanú sieť aplikovať aj na ďalšie obrázky, s veľkou pravdepodobnosťou narazíme na triedy, ktoré sieť nebude poznať:
!{sys.executable} yolov3/detect.py \
--source samples_raccoon \
--output output_raccoon \
--cfg cfg/yolov3-spp.cfg --data data/coco.data \
--weight yolov3-spp.weights \
--img-size 608
Ako uvidíme, na obrázku s medvedíkom čistotným (raccoon) bude síce medvedík čistotný označený, ale bude nesprávne klasifikovaný ako medveď (bear), keďže táto trieda sa sieti zdá najbližšia.
show_images("output_raccoon")
Ďalej si ukážeme, ako je možné sieť YOLOv3 natrénovať na vlastnej dátovej množine. Použijeme dátovú množinu s obrázkami medvedíka čistotného z repozitára raccoon dataset.
Keďže existuje väčšie mmnožstvo formátov, ktoré sa používajú pri anotovaní dát pre vizuálnu detekciu, pri práci s vlastnou dátovou množinou typicky narazíme na to, že anotácie sú v inom formáte než aký podporuje implementácia s ktorou pracujeme. Anotácie preto môže byť potrebné prekódovať. Našťastie ide o pomerne jednoduchú úlohu: formáty sú najčastejšie založené na dobre známych typoch štruktúrovaných súborov ako sú XML, JSON alebo CSV. Existuje veľa balíčkov, ktoré ich umožňujú načítať a ukladať.
V rámci našej dátovej množiny sú anotácie uložené vo formáte Pascal VOC. Ide o formát založený na jazyku XML. Anotácia môže vyzerať napríklad takto:
<annotation verified="yes">
<folder>images</folder>
<filename>raccoon-1.jpg</filename>
<path>/Users/datitran/Desktop/raccoon/images/raccoon-1.jpg</path>
<source>
<database>Unknown</database>
</source>
<size>
<width>650</width>
<height>417</height>
<depth>3</depth>
</size>
<segmented>0</segmented>
<object>
<name>raccoon</name>
<pose>Unspecified</pose>
<truncated>0</truncated>
<difficult>0</difficult>
<bndbox>
<xmin>81</xmin>
<ymin>88</ymin>
<xmax>522</xmax>
<ymax>408</ymax>
</bndbox>
</object>
</annotation>
Každému obrázku zodpovedá jeden XML súbor s anotáciami. Každý objekt je anotovaný svojou triedou (atribút name
) a koordinátmi ohraničujúceho obdĺžnika (xmin
, xmax
, ymin
, ymax
).
Naša implementácia pracuje namiesto toho s anotáciami vo formáte YOLO:
0 0.463846 0.594724 0.678462 0.767386
Tu je každý objekt anotovaný vo formáte
identifikátor_triedy horizontálny_stred vertikálny_stred šírka výška
pričom identifikátorom triedy je jej poradované číslo (zoznam tried je v osobitnom súbore), ďalej nasledujú koordináty stredu ohraničujúceho obdĺžnika a jeho šírka a výška. Koordináty, šírka aj výška sú normalizované vo vzťahu ku veľkosti obrázka.
Ďalej pokračujme tým, že špecifikujeme cesty ku jednotlivým častiam dátovej množiny: ku obrázkom, ku anotáciám vo formáte Pascal VOC a ku priečinku, kde sa majú uložiť nové anotácie vo formáte YOLO. Ak posledne zmienený priečinok ešte neexistuje, vytvoríme ho.
xml_path = "raccoon_dataset/annotations"
img_path = "raccoon_dataset/images"
labels_path = "raccoon_dataset/labels"
# uistíme sa, že priečinok existuje
!mkdir -p {labels_path}
Ďalej si musíme zadefinovať funkciu, ktorá z Pascal VOC súboru extrahuje záznamy o objektoch v štruktúre podľa formátu YOLO:
def extract_yolo_annot(xml_filename):
classes = set()
annotations = []
tree = ET.parse(xml_filename)
root = tree.getroot()
img_width = int(root.find('size/width').text)
img_height = int(root.find('size/height').text)
for member in root.findall('object'):
c = member.find('name').text
classes.add(c)
# extrahujeme koordináty
xmin = int(member.find('bndbox/xmin').text)
xmax = int(member.find('bndbox/xmax').text)
ymin = int(member.find('bndbox/ymin').text)
ymax = int(member.find('bndbox/ymax').text)
# vynútime, aby neboli menšie než 0
# ani väčšie než rozmery obrázka
xmin = max(0, min(xmin, img_width))
xmax = max(0, min(xmax, img_width))
ymin = max(0, min(ymin, img_height))
ymax = max(0, min(ymax, img_height))
# uistíme sa, že xmin <= xmax; ymin <= ymax
xmin, xmax = min(xmin, xmax), max(xmin, xmax)
ymin, ymax = min(ymin, ymax), max(ymin, ymax)
# vypočítame stredy a rozmery a normalizujeme ich
xc = (xmin + xmax) / 2 / img_width
yc = (ymin + ymax) / 2 / img_height
w = (xmax - xmin) / img_width
h = (ymax - ymin) / img_height
annotations.append((c, xc, yc, w, h))
# navrátime anotácie a množinu všetkých existujúcich tried
return annotations, classes
Vytvorenú funkciu aplikujeme na všetky súbory a výsledky uložíme v osobitnom priečinku. Vytvoríme si pritom aj zoznam všetkých tried:
file_annots = []
classes = set()
for filename in os.listdir(xml_path):
basename, ext = os.path.splitext(filename)
if ext == '.xml':
annotations, cl = extract_yolo_annot(os.path.join(xml_path, filename))
file_annots.append((basename, annotations))
classes.update(cl)
class_dict = {c: i for i, c in enumerate(classes)}
class_list = [c for c in classes]
NUM_CLASSES = len(class_list)
for basename, annotations in file_annots:
with open(os.path.join(labels_path, basename + ".txt"), "w") as file:
for annot in annotations:
file.write("{} {:.6f} {:.6f} {:.6f} {:.6f}\n".format(
class_dict[annot[0]], *annot[1:]))
Aby sme sa uistili, že sme konverziu implementovali správne, môžeme si zobraziť výsledné anotácie pre jeden obrázok:
show_annot_image(os.path.join(img_path, "raccoon-1.jpg"),
os.path.join(labels_path, "raccoon-1.txt"),
["raccoon"])
Ďalej si vytoríme zoznam všetkých obrázkov, ktoré sú k dispozícii a rozdelíme si ich na tréningové a validačné.
img_filenames = []
for filename in sorted(os.listdir(img_path)):
if re_image.match(filename):
img_filenames.append(filename)
train_filenames = img_filenames[:-8]
valid_filenames = img_filenames[-8:]
Ďalej je potrebné vytvoriť niekoľko konfiguračných súborov. Ako prvé vytvoríme zoznamy tréningových a validačných obrázkov:
with open("data/raccoon_train.txt", "w") as file:
for fname in train_filenames:
file.write(os.path.join(img_path, fname) + "\n")
with open("data/raccoon_valid.txt", "w") as file:
for fname in valid_filenames:
file.write(os.path.join(img_path, fname) + "\n")
Zoznam všetkých tried zapíšeme do osobitného súboru:
with open("data/raccoon.names", "w") as file:
for c in class_list:
file.write(c + "\n")
Vytvoríme hlavný súbor, ktorý sa odkazuje na jednotlivé konfiguračné súbory.
%%writetemplate data/raccoon.data
classes={NUM_CLASSES}
train=./data/raccoon_train.txt
valid=./data/raccoon_valid.txt
names=./data/raccoon.names
backup=backup/
eval=raccoon
Pomocou posledného súboru konfigurujeme architektúru a hyperparametre siete. Keďže súbor je veľmi dlhý, vytvoríme ho podľa preddefinovanej šablóny a meniť budeme len niektoré parametre:
BURN_IN = 1000
ANCHORS = "10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326"
YOLO_NUM = 9
YOLO_FILTERS = (4 + 1 + NUM_CLASSES) * 3
ROUTE_LAYERS = "-1, 36"
STRIDE = 2
IMG_SIZE= 416
IGNORE_THRESH = 0.5
LEARNING_RATE = 1e-4
with open("cfg/raccoon_yolo.cfg", "w") as file:
file.write(YOLO_CFG.format(**globals()))
Po vytvorení všetkých potrebných konfiguračných súborov sme pripravení natrénovať detektor. Špecifikovať treba najmä počet epoch a veľkosť dávky. Okrem toho chceme jadro siete inicializovať pomocou váh predtrénovaných na dátovej množine ImageNet, preto špecifikujeme, že sa majú použiť váhy "darknet53.conv.74". Súbor s váhami sa stiahne automaticky.
!{sys.executable} yolov3/train.py \
--batch-size 25 --epochs 40 \
--weights "darknet53.conv.74" \
--img-size {IMG_SIZE} --cache-images \
--cfg cfg/raccoon_yolo.cfg \
--data data/raccoon.data \
--rect --accumulate=1
V ďalšom kroku si validačné obrázky okopírujeme do osobitného adresára a skúsime na ne aplikovať výsledný vizuálny detektor:
!mkdir -p raccoon_test; mkdir -p raccoon_res
for fname in valid_filenames:
shutil.copy(os.path.join(img_path, fname),
os.path.join("raccoon_test", fname))
!{sys.executable} yolov3/detect.py \
--weight weights/best.pt \
--img-size {IMG_SIZE} \
--source=raccoon_test \
--output raccoon_res \
--cfg cfg/raccoon_yolo.cfg \
--data data/raccoon.data
Výsledok si zobrazíme:
show_images("raccoon_res")
Zatiaľ čo pri klasifikácii sa každému obrázku priraďuje len trieda, pri vizuálnej detekcii môže každý obrázok obsahovať jeden alebo viacero objektov a udáva sa nielen ich trieda, ale aj pozícia (väčšinou vo forme ohraničujúceho obdĺžnika) v obraze. Na anotáciu obrázkov pri vytváraní dátovej množiny je preto potrebné použiť špeciálny nástroj, ktorý umožní objekty zodpovedajúcim spôsobom označiť.
Takých nástrojov existuje samozrejme väčšie množstvo – napríklad nástroj VIA od skupiny VGG založený na HTML a javascripte. Existuje viacero formátov, v ktorých môžu byť anotácie uložené – najčastejšie sú založené na súboroch typu XML, JSON alebo CSV. V praxi je typicky potrebné napísať jednoduchý kód, ktorý skonvertuje anotácie do formátu, ktorý podporuje systém na vizuálnu detekciu.