Hlavní navigace

Framework Torch: modul pro zpracování rastrových obrázků

Pavel Tišnovský

Dnes se seznámíme se základními možnostmi nabízenými modulem image. V něm nalezneme mnoho podpůrných funkcí pro zpracování rastrových obrázků, které v aplikacích tvoří vstup do neuronových sítí.

Obsah

1. Framework Torch: modul pro zpracování rastrových obrázků

2. Reprezentace rastrových obrázků v knihovně Torch

3. Vytvoření obrázku ve stupních šedi z běžného 2D tenzoru

4. Vytvoření RGB obrázku z 3D tenzoru

5. Načtení obrázku z externího souboru

6. Standardní testovací obrázky aneb „kam se poděla Lenna?“

7. Počet dimenzí a rozměry tenzoru s obrázkem

8. Specifikace počtu barvových kanálů a typu komponent tenzoru při načtení obrázku

9. Základní operace (transformace) nad obrázkem

10. Ořezání rastrového obrázku

11. Posun pixelů

12. Horizontální a vertikální zrcadlení

13. Změna měřítka a volba aplikovaného filtru při výpočtu nového obrázku

14. Rotace obrázku

15. Význam konvolučních filtrů při práci s rastrovým obrazem

16. Příklady jednoduchých 2D konvolučních filtrů

17. Reprezentace jádra filtru a aplikace konvolučních filtrů v knihovně Torch

18. Demonstrační příklad – použití konvolučních filtrů

19. Repositář s demonstračními příklady

20. Odkazy na Internetu

1. Framework Torch: modul pro zpracování rastrových obrázků

Ve frameworku Torch mají uživatelé k dispozici modul nazvaný image. Tento modul obsahuje funkce, které mohou být použity při zpracování rastrových obrazů, což je užitečné, protože právě rastrové obrázky se zpracovávají v mnoha aplikacích neuronových sítí, což znamená, že bývá nutné vstupní obrázky vhodným způsobem upravit. Interně jsou sice obrázky reprezentovány jako běžné tenzory (buď dvourozměrné či trojrozměrné), ovšem modul image nabízí specializované funkce určené například pro načítání a ukládání obrázků z a do známých souborových formátů, zejména pak do formátu PNG (Portable Network Graphics), JPEG, PPM (Portable PixelMap) a taktéž do formátu PGM (Portable GrayMap). Mezi další nabízené funkce patří zejména aplikace různých transformací (posun, zvětšení, zmenšení, rotace) obrazové informace a v neposlední řadě aplikace konvolučních filtrů, například za účelem detekce hran, zaostření obrázku či naopak pro odstranění šumu.

Kromě výše zmíněných operací nalezneme v modulu image i další funkce, které například umožňují konverzi hodnot pixelů při přechodu do jiného barvového prostoru (RGB, YUV, HSL a CIEL*a*b). Taktéž je možné s využitím specializovaných konstruktorů vytvořit testovací obrázky (ovšem s omezeními, které si popíšeme v šesté kapitole) a konvoluční filtry s předem specifikovanými vlastnostmi (není tedy nutné ručně konstruovat jádro filtru). Mezi další operace, které mohou být užitečné, zejména pro výstupní rastrové obrázky, patří možnost přikreslit do obrázku obdélník nebo textový řetězec. Tyto funkce se mohou hodit například ve chvíli, kdy je neuronovou sítí v obrázku nalezen hledaný objekt, který je zapotřebí zvýraznit a nějak popsat.

2. Reprezentace rastrových obrázků v knihovně Torch

Digitální rastrový obraz je představován obrazovými elementy (picture elements – pixels), které bývají uspořádány do určité vzorkovací mřížky. V dalším textu budeme uvažovat zdaleka nejpoužívanější čtvercovou nebo obdélníkovou mřížku, i když jsou ve skutečnosti možné i další varianty zmíněné v následujícím textu. Taková pravidelná mřížka je vlastně maticí, jejíž prvky odpovídají kvantovacím úrovním jasové či barvové funkce. Nejjednodušší reprezentací obrazových dat v paměti počítače je dvojrozměrná mřížka bodů. Každý bod mřížky odpovídá definičnímu oboru diskrétní obrazové funkce I(x, y) a příslušná hodnota pixelu (tj. jeho jas) hodnotě této funkce v daném bodě. Plošné uspořádání bodů může mít podobu buď čtvercové, hexagonální, trojúhelníkové, nebo i jiné mřížky. Technicky i programově se nejsnadněji realizuje čtvercová mřížka, i když pro účely filtrace je mnohdy výhodnější mřížka hexagonální (ta je použita v některých digitálních fotoaparátech).

Obrazová informace může být v mřížce uložena buď přímo hodnotou jasu (monochromatické obrázky), hodnotami barvových složek RGB, nebo indexem do barvové palety (LUT – Look-up table). Pokud by byl barevný rastrový obrázek s barvami ležícími v prostoru RGB takto reprezentován, vypadalo by to při zápisu v jazyka Lua zhruba následovně:

{
    {{R, G, B}, {R, G, B}, {R, G, B}, ...},    -- první řádek bitmapy
    {{R, G, B}, {R, G, B}, {R, G, B}, ...},    -- druhý řádek bitmapy
    {{R, G, B}, {R, G, B}, {R, G, B}, ...},    -- třetí řádek bitmapy
    {{R, G, B}, {R, G, B}, {R, G, B}, ...},    -- čtvrtý řádek bitmapy
    ...
    ...
    ...
}

Ve frameworku Torch, ale i v některých dalších knihovnách, se však používá opačný přístup: namísto 2D pole (mřížky) obsahujícího trojici barvových složek RGB se používá trojice stejně velkých mřížek spojená do 3D pole. Nejvyšší dimenze pak odpovídá barvové složce, samotné 2D mřížky pak již obsahují skalární hodnoty s intenzitami dané barvové složky pro všechny pixely:

{
    {   -- matice pro modré složky pixelů
        {p1, p2, p3, p4, p5, p6, ...},    -- první řádek bitmapy
        {p1, p2, p3, p4, p5, p6, ...},    -- druhý řádek bitmapy
        {p1, p2, p3, p4, p5, p6, ...},    -- třetí řádek bitmapy
        {p1, p2, p3, p4, p5, p6, ...},    -- čtvrtý řádek bitmapy
        ...
        ...
        ...
    },
    {   -- matice pro zelené složky pixelů
        {p1, p2, p3, p4, p5, p6, ...},    -- první řádek bitmapy
        {p1, p2, p3, p4, p5, p6, ...},    -- druhý řádek bitmapy
        {p1, p2, p3, p4, p5, p6, ...},    -- třetí řádek bitmapy
        {p1, p2, p3, p4, p5, p6, ...},    -- čtvrtý řádek bitmapy
        ...
        ...
        ...
    },
    {   -- matice pro modré složky pixelů
        {p1, p2, p3, p4, p5, p6, ...},    -- první řádek bitmapy
        {p1, p2, p3, p4, p5, p6, ...},    -- druhý řádek bitmapy
        {p1, p2, p3, p4, p5, p6, ...},    -- třetí řádek bitmapy
        {p1, p2, p3, p4, p5, p6, ...},    -- čtvrtý řádek bitmapy
        ...
        ...
        ...
    }
}

Velmi často se pracuje s monochromatickými obrazy, u nichž je způsob uložení ještě jednodušší, než u plnobarevných obrazů. V tomto případě se totiž skutečně jedná o „pouhou“ 2D matici hodnot, tj. chybí nejvyšší dimenze:

{   -- matice intenzit pixelů
    {p1, p2, p3, p4, p5, p6, ...},    -- první řádek bitmapy
    {p1, p2, p3, p4, p5, p6, ...},    -- druhý řádek bitmapy
    {p1, p2, p3, p4, p5, p6, ...},    -- třetí řádek bitmapy
    {p1, p2, p3, p4, p5, p6, ...},    -- čtvrtý řádek bitmapy
    ...
    ...
    ...
}

3. Vytvoření obrázku ve stupních šedi z běžného 2D tenzoru

Poznámka: na začátku všech příkladů je nutné načíst modul image:

require("image")

ten načte všechny funkce z tohoto modulu (v interaktivním prostředí se vypíšou na terminál).

Vzhledem k tomu, že z předchozího popisu již poměrně dobře známe způsob reprezentace rastrových obrázků v knihovně Torch a jejím modulu image, můžeme si vyzkoušet programové vytvoření jednoduchého obrázku obsahujícího černou mřížku na bílém pozadí. Černobílá mřížka může být reprezentována monochromatickým obrázkem uloženým do 2D matice. Nejprve tedy zkonstruujeme tenzor druhého řádu odpovídající velikosti (počet sloupců a řádků), dále naplníme jeho prvky konstantou reprezentující bílou barvu a následně ve dvojici programových smyček do tenzoru vložíme nulové prvky představující černou mřížku. Jedno z možných řešení může vypadat následovně (poznámka: teoreticky se můžeme smyček zcela zbavit použitím metody repeatTensor, výsledek však nebude příliš čitelný):

function createImageWithGrid(size, grid)
    img=torch.Tensor(SIZE, SIZE)
    img:fill(255)
 
    for row = 1, SIZE, GRID do
        img:narrow(1, row, 1):zero()
    end
 
    for column = 1, SIZE, GRID do
        img:narrow(2, column, 1):zero()
    end
 
    return img
end

Na konci příkladu je samozřejmě nutné vytvořený obrázek buď vykreslit na obrazovku s využitím knihovny Qt (rozhraní musí být nainstalováno společně s Torchem) nebo ho uložit do souboru. Formát souboru je automaticky odvozen z koncovky:

img = createImageWithGrid(SIZE, GRID)
 
image.save("grid.png", img)

Obrázek 1: Rastrový obrázek vygenerovaný uživatelskou funkcí createImageWithGrid.

4. Vytvoření RGB obrázku z 3D tenzoru

Způsob programového vytvoření monochromatického obrázku jsme si popsali v předchozí kapitole, takže se nyní podívejme na to, jakým postupem je možné vygenerovat plnobarevný (truecolor) rastrový obrázek s využitím standardního barvového prostoru RGB. Víme již, že takový obrázek je v Torchi reprezentován tenzorem třetího řádu, konkrétně trojicí 2D matic, přičemž každá trojice nese informaci o jedné barvové složce pixelů (všechny tři matice samozřejmě budou mít shodné rozměry). Konstrukci tenzoru třetího řádu je možné v knihovně Torch provést různým způsobem, například z jednodimenzionálního vektoru funkcí reshape atd. My si však ukážeme pravděpodobně nejčitelnější způsob spočívající ve vytvoření 2D matic pro každou barvovou složku zvlášť s následným složením matic do tenzoru. Červená a modrá složka bude tvořena gradientním přechodem, zelená složka bude pro všechny pixely nulová:

function createRGBImage(size)
    img = torch.Tensor(3, size, size)
 
    line = torch.linspace(0, 1, size)
 
    red = line:repeatTensor(size):reshape(size, size)
    green = torch.zeros(size, size)
    blue = red:transpose(1, 2)
 
    img[1] = red
    img[2] = green
    img[3] = blue
 
    return img
end

Obrázek po jeho vytvoření opět uložíme do souboru:

img2 = createRGBImage(SIZE)
 
image.save("rgb.png", img2)

Obrázek 2: Rastrový obrázek vygenerovaný uživatelskou funkcí createRGBImage.

Poznámka: pokud si pamatujete, jak byl uložen obraz v 16barevných režimech VGA, nebude pro vás způsob reprezentace RGB obrázků v Torchi ničím zvláštní.

5. Načtení obrázku z externího souboru

Rastrové obrázky se samozřejmě nemusí tvořit jen programově, ale je možné je načíst z externích souborů (což je v praxi mnohem častější). Načtení obrázku se provede funkcí image.load, které se v nejjednodušším případě předá jméno souboru. Z koncovky se odvozí jeho formát a pokud koncovka není specifikována, tak je formát odhadnut z obsahu souboru (to však nemusí být spolehlivá metoda, ostatně většinou není žádný důvod koncovky neuvádět). V dalších demonstračních příkladech budeme pracovat se slavným obrázkem Lenny, který však již z licenčních důvodů není přímo v Torchi dostupný (viz další kapitoly). Můžeme si však snadno pomoci následující trojicí funkcí, které zjistí, zda se v aktuálním adresáři obrázek Lenny nachází a pokud nikoli, dojde k jeho stažení z předem známé adresy. Samotné stažení je provedeno externím nástrojem wget:

require("image")
 
 
original_image_address = "https://upload.wikimedia.org/wikipedia/en/2/24/Lenna.png"
image_name = "lenna.png"
 
 
function file_exists(filename)
    local fin = io.open(filename,"r")
    if fin then
        io.close(fin)
        return true
    else
        return false
    end
end
 
 
function download_file(address, filename)
    local command = "wget -v -O " .. filename .. " " .. address
    os.execute(command)
end
 
 
function setup(address, filename)
    if not file_exists(filename) then
        download_file(original_image_address, filename)
    end
end
 
 
setup(original_image_address, image_name)

Poznámka: tyto tři funkce jsou vloženy do všech dalších příkladů a zajišťují, že se obrázek Lenny stáhne jen jedenkrát, samozřejmě za předpokladu, že ho nesmažete.

Obrázek 3: Obrázek Lenny stažený předchozím příkladem.

6. Standardní testovací obrázky aneb „kam se poděla Lenna?“

Modul image obsahuje dvojici konstruktorů, které by – alespoň teoreticky – po svém zavolání měly vrátit tenzor obsahující standardní testovací obrázek. Tyto konstruktory se jmenují lena a fabio, ve skutečnosti však první konstruktor již v novějších verzích Torche nevrací standardní obrázek Lenny! Tento obrázek totiž byl z repositáře Torche odstraněn kvůli problémům s licencováním. Bližší informace je možné nalézt na GitHubu: https://github.com/torch/nn/is­sues/854 a https://github.com/torch/i­mage/commit/797fcb101b76c9b329b3­ee83349b0f6adeacac94. Pokud tedy spustíte příkaz:

image.save("lenna?.png", image.lena())

dostanete obrázek Grace Hopper:

Obrázek 4: Obrázek vytvořený konstruktorem image.lena().

7. Počet dimenzí a rozměry tenzoru s obrázkem

Modul image obsahuje i pomocnou funkci nazvanou image.getSize. Této funkci se v řetězci předává jméno souboru s uloženým obrázkem a výsledkem jsou rozměry tohoto obrázku, a to bez nutnosti nahrání celého obrázku a jeho dekomprimace do operační paměti. Tato funkce se používá velmi snadno, což je patrné z dalšího příkladu:

print(image.getSize(image_name))

Po spuštění této funkce s předáním jména souboru s Lennou by se na standardní výstup měly vypsat tyto informace:

   3
 512
 512
[torch.LongTensor of size 3]

Samozřejmě je možné získat i další informace o obrázku, protože již víme, že se nejedná o nic jiného, než o rozměrný tenzor:

function print_image_info(img)
    print("Element type:")
    print(img:type())
 
    print("Dimensions:")
    print(img:dim())
 
    print("Tensor shape:")
    print(img:size())
end

Pro testovací obrázek Lenny by tato funkce měla vypsat:

Element type:
torch.DoubleTensor
Dimensions:
3
Tensor shape:
   3
 512
 512
[torch.LongStorage of size 3]

8. Specifikace počtu barvových kanálů a typu komponent tenzoru při načtení obrázku

S funkcí image.load jsme se již setkali v předchozích kapitolách. Zbývá nám však popis dvou nepovinných parametrů této funkce. Pokud totiž za jménem obrázku uvedeme ještě celé číslo (typicky 1 nebo 3), bude obrázek po načtení a dekódování upraven takovým způsobem, aby obsahovat dané množství kanálů. Jeden kanál je použit pro monochromatické obrázky reprezentované 2D maticí, tři kanály se používají především pro plnobarevné obrázky s barvami z prostoru RGB. Co to v praxi znamená? Obrázek Lenny je plnobarevný, takže se implicitně načte do tenzoru třetího řádu:

image1 = image.load(image_name)

Pokud však při načítání předáme funkci nepovinný parametr 1, provede se konverze na monochromatický obrázek:

image2 = image.load(image_name, 1)

To však není vše, protože je podporován ještě třetí nepovinný parametr, kterým je možné specifikovat typy komponent (prvků) výsledného tenzoru s bitmapou. Typ se předává jako řetězec, takže následující příkaz umožní načtení plnobarevného obrázku a jeho následnou konverzi na obrázek monochromatický, přičemž každý pixel je reprezentován jediným bajtem (a nikoli například hodnotou typu double):

image3 = image.load(image_name, 1, 'byte')

Obrázek 5: Původně RGB obrázek, který byl konvertován do stupňů šedi.

O výsledku se můžeme snadno přesvědčit spuštěním těchto příkazů:

 
print_image_info(image1)
print_image_info(image2)
print_image_info(image3)

Na standardní výstup by se mělo vypsat:

Element type:
torch.DoubleTensor
Dimensions:
3
Tensor shape:
   3
 512
 512
[torch.LongStorage of size 3]
 
 
 
Element type:
torch.DoubleTensor
Dimensions:
2
Tensor shape:
 512
 512
[torch.LongStorage of size 2]
 
 
 
Element type:
torch.ByteTensor
Dimensions:
2
Tensor shape:
 512
 512
[torch.LongStorage of size 2]

9. Základní operace (transformace) nad obrázkem

Nad obsahem obrázku, tedy nad maticemi s údaji o jednotlivých pixelech, je možné provádět různé transformace. Základní operací je uřezání obrázku funkcí crop, která vrací nový obrázek s odlišným rozlišením (velikostí). Velikost obrázku se obecně mění i operací scale. Další podporované operace, zejména posun pixelů, zrcadlení obrázku ve svislé či vodorovné rovině a rotace obrázku, již implicitně velikost obrázku nemění, což si ukážeme na demonstračních příkladech. Všechny základní transformace podporované modulem image jsou vypsány v následující tabulce:

Funkce Kapitola Význam
image.crop 10 ořezání obrázku osově orientovaným obdélníkem
image.translate 11 posun všech pixelů v obrázku zadaným směrem
image.hflip 12 horizontální zrcadlení (okolo osy y)
image.vflip 12 vertikální zrcadlení (okolo osy x)
image.flip 12 zrcadlení okolo zvolené osy či dokonce „rotace“ barev
image.scale 13 zvětšení či zmenšení obrázku se specifikací rozlišení výsledku
image.rotate 14 rotace obsahu obrázku se zachování jeho původního rozlišení

Všechny operace jako svůj první volitelný parametr akceptují tenzor, do něhož se má uložit výsledek. Pokud tento parametr nepoužijeme, budou se všechny funkce chovat jako skutečné funkce, tj. výsledkem bude nový tenzor (návratová hodnota). Záleží jen na programátorovi, který způsob volání použije – buď „funkcionální“ (obecně větší spotřeba paměti) či procedurální.

10. Ořezání rastrového obrázku

Základní operací určenou pro zpracování rastrových obrázků je jejich ořez funkcí image.crop. Ve své základní podobě se této funkci předává pětice parametrů – zdrojový rastrový obrázek, souřadnice [x,y] levého horního rohu ořezového obdélníku a souřadnice [x,y] pravého dolního rohu. Ořezový obdélník je vždy orientován stejně, jako souřadné osy.

Podívejme se na příklad použití:

image1 = image.load(image_name)
image2 = image.load(image_name, 1)
 
image1crop = image.crop(image1, 200, 150, 400, 350)
image2crop = image.crop(image2, 200, 150, 400, 350)
 
print_image_info(image1crop)
print_image_info(image2crop)
 
image.save("cropped1box.png", image1crop)
image.save("cropped2box.png", image2crop)

Obrázek 6: Oříznutí rastrového obrázku obdélníkem.

Alternativně je možné funkci image.crop předat pouze rozměry ořezového obdélníku a pomocí krátkého řetězce specifikovat, zda má být obdélník ve zdrojovém obrázku vycentrován či zda má být umístěn do některého z rohů:

croppedImage = image.crop(sourceImage, format, width, height)

Řetězec format může obsahovat text: „c“, „tl“, „tr“, „bl“, „br“ (c=centre, t=top, b=bottom, l=left, r=right).

Opět se podívejme na příklad použití:

function cropAndSaveImage(source, format, width, height, order)
    croppedImage = image.crop(source, format, width, height)
    filename = "cropped" .. order .. format .. ".png"
    print(filename)
    print_image_info(croppedImage)
    image.save(filename, croppedImage)
end
 
formats = {"c", "tl", "tr", "bl", "br"}
 
for _, format in ipairs(formats) do
    cropAndSaveImage(image1, format, 250, 150, 1)
    cropAndSaveImage(image2, format, 250, 150, 2)
end

Obrázek 7: Oříznutí rastrového obrázku okolo středu.

Obrázek 8: Levý horní roh obrázku.

Obrázek 9: Pravý dolní roh obrázku.

11. Posun pixelů

Další jednoduchou operací prováděnou nad rastrovými obrázky je posun pixelů v zadaném směru. Směr je specifikován dvojicí skalárních hodnot dx, dy, tedy nikoli tenzorem. Tyto hodnoty určují vektor posunu. Velikost výsledného obrázku je shodná s obrázkem původním, což znamená, že hodnoty některých pixelů se ztratí a nové pixely budou mít implicitně černou barvu. Ostatně se o tom můžeme velmi snadno přesvědčit posunem obrázku o 64 pixelů doprava a dolů:

lenna = image.load(image_name)
 
translated = image.translate(lenna, 64, 64)
image.save("translated.png", translated)

Obrázek 10: Testovací bitmapa posunutá o 64 pixelů dolů a doprava.

Pokud obrázek posuneme směrem zpět, původní hodnoty pixelů se neobnoví a celý okraj o šířce 32 pixelů bude černý:

translated_back = image.translate(translated, -32, -32)
image.save("translated_back.png", translated_back)

Obrázek 11: Při posunu zpět zjistíme, že se hodnoty původních pixelů ztratily.

12. Horizontální a vertikální zrcadlení

Další jednoduchou operací je horizontální a vertikální zrcadlení (či možná lépe řečeno převrácení) obrazu. To lze provést funkcemi nazvanými image.hflip (horizontal flip) a image.vflip (vertical flip). Interně se skutečně prohodí komponenty tenzorů, na rozdíl od některých jiných knihoven, které si pouze zapamatují příznak zrcadlení. Otestování chování těchto dvou funkcí je jednoduché:

lenna = image.load(image_name)
 
hflip = image.hflip(lenna)
image.save("horizontal_flip.png", hflip)
 
vflip = image.vflip(lenna)
image.save("vertical_flip.png", vflip)

Obrázek 12: Výsledek operace image.hflip.

Obrázek 13: Výsledek operace image.vflip.

Ve skutečnosti existuje ještě jedna „zrcadlící“ funkce nazvaná image.flip. Této funkci je kromě zdrojového obrázku nutné předat i dimenzi, okolo které se má zrcadlení provést. Tato funkce je tedy obecnější, než obě výše zmíněné funkce image.hflip a image.vflip, ovšem navíc je ještě možné provést zrcadlení okolo první dimenze, což u barevných obrázků (RGB) znamená prohození červené a modré barové složky! Ostatně, můžeme si to snadno vyzkoušet:

lenna = image.load(image_name)
 
flip_d1 = image.flip(lenna, 1)
image.save("flip_dimension1.png", flip_d1)
 
flip_d2 = image.flip(lenna, 2)
image.save("flip_dimension2.png", flip_d2)
 
flip_d3 = image.flip(lenna, 3)
image.save("flip_dimension3.png", flip_d3)

Obrázek 14: Výsledek zrcadlení okolo první dimenze (prohození barvových složek).

Obrázek 15: Výsledek zrcadlení okolo druhé dimenze.

Obrázek 16: Výsledek zrcadlení okolo třetí dimenze.

Poznámka: samozřejmě je možné použít i další funkce pro práci s tenzory, například transpose atd. pro dosažení dalších podobných transformací.

13. Změna měřítka a volba aplikovaného filtru při výpočtu nového obrázku

Poněkud složitější operace jsou prováděny při změně měřítka obrázku, protože je možné si zvolit, jaký algoritmus se bude při změně měřítka aplikovat. Uživatel si může zvolit jeden ze tří základních algoritmů: výběr nejbližšího bodu, bilineární filtraci či filtraci bikubickou (všechny tři možnosti jsou ukázány na výše linkované stránce). Při změně měřítka obrázku je nutné specifikovat jeho nové rozměry (jednotkou jsou vždy pixely) a taktéž je možné řetězcem specifikovat použitý algoritmus/filtr. Vyzkoušejme si tedy z obrázku Lenny získat jeho středovou část o rozměrech 100×66 pixelů, kterou následně šestkrát zvětšíme na obrázky o rozměrech 600×400 pixelů (povšimněte si, že vertikálně není zvětšení přesně šestinásobné, což však vůbec nevadí):

lenna = image.load(image_name)
 
cropped = image.crop(lenna, "c", 100, 66)
 
scaled_simple = image.scale(cropped, 600, 400, "simple")
scaled_bilinear = image.scale(cropped, 600, 400, "bilinear")
scaled_bicubic = image.scale(cropped, 600, 400, "bicubic")
 
print_image_info(scaled_simple)
print_image_info(scaled_bilinear)
print_image_info(scaled_bicubic)
 
image.save("scaled_simple.png", scaled_simple)
image.save("scaled_bilinear.png", scaled_bilinear)
image.save("scaled_bicubic.png", scaled_bicubic)

Výsledky:

Obrázek 17: Zvětšení při použití algoritmu/filtru „nearest“.

Obrázek 18: Zvětšení při použití bilineární filtrace.

Obrázek 19: Zvětšení při použití bikubické filtrace.

Zajímavější bude zjistit, jak se zvětší jednoduchá mřížka, která bude mít šířku stran nastavenou na dva pixely (sami si vyzkoušejte, co se stane ve chvíli, kdy je šířka pouhý jeden pixel):

grid = createImageWithGrid(SIZE, GRID)
 
croppedGrid = image.crop(grid, "c", SIZE/4, SIZE/4)
 
scaled_simple = image.scale(croppedGrid, SIZE, SIZE, "simple")
scaled_bilinear = image.scale(croppedGrid, SIZE, SIZE, "bilinear")
scaled_bicubic = image.scale(croppedGrid, SIZE, SIZE, "bicubic")
 
image.save("grid_scaled_simple.png", scaled_simple)
image.save("grid_scaled_bilinear.png", scaled_bilinear)
image.save("grid_scaled_bicubic.png", scaled_bicubic)

Výsledky:

Obrázek 20: Zvětšení mřížky při použití algoritmu/filtru „nearest“.

Obrázek 21: Zvětšení mřížky při použití bilineární filtrace.

Obrázek 22: Zvětšení mřížky při použití bikubické filtrace.

14. Rotace obrázku

Nejsložitější (základní) operací, kterou lze s obrázkem provádět, je jeho rotace. Obrázek se otáčí okolo svého středu a úhel se zadává v radiánech. Kromě toho je možné specifikovat, jakým způsobem se vypočtou barvy pixelů výsledného obrázku. Buď se použije barva nejbližšího pixelu nebo se aplikuje (pomalejší) bilineární filtrace na základě barev sousedních pixelů původního obrázku. Podívejme se nyní na rotaci testovacího obrázku Lenny o 5° a o 45°. Pro převod mezi stupni a radiány se použije standardní funkce math.rad:

lenna = image.load(image_name)
 
angle5 = math.rad(5)
angle45 = math.rad(45)
 
rotated5_simple = image.rotate(lenna, angle5, "simple")
rotated5_bilinear = image.rotate(lenna, angle5, "bilinear")
 
rotated45_simple = image.rotate(lenna, angle45, "simple")
rotated45_bilinear = image.rotate(lenna, angle45, "bilinear")
 
image.save("rotated5_simple.png", rotated5_simple)
image.save("rotated5_bilinear.png", rotated5_bilinear)
 
image.save("rotated45_simple.png", rotated45_simple)
image.save("rotated45_bilinear.png", rotated45_bilinear)

Výsledky:

Obrázek 23: Rotace obrázku Lenny o 45° při použití algoritmu pro nalezení nejbližšího pixelu.

Obrázek 24: Rotace obrázku Lenny o 45° při použití bikubické filtrace.

Obrázek 25: Rotace obrázku Lenny o 5° při použití algoritmu pro nalezení nejbližšího pixelu.

Obrázek 26: Rotace obrázku Lenny o 5° při použití bikubické filtrace.

Rozdíl mezi oběma algoritmy (nejbližší pixel versus bilineární filtrace) je lépe patrný u rotace mřížky:

grid = createImageWithGrid(SIZE, GRID)
 
rotated5_simple = image.rotate(grid, angle5, "simple")
rotated5_bilinear = image.rotate(grid, angle5, "bilinear")
 
rotated45_simple = image.rotate(grid, angle45, "simple")
rotated45_bilinear = image.rotate(grid, angle45, "bilinear")
 
image.save("grid_rotated5_simple.png", rotated5_simple)
image.save("grid_rotated5_bilinear.png", rotated5_bilinear)
 
image.save("grid_rotated45_simple.png", rotated45_simple)
image.save("grid_rotated45_bilinear.png", rotated45_bilinear)

Obrázek 27: Rotace obrázku s mřížkou o 45° při použití algoritmu pro nalezení nejbližšího pixelu.

Obrázek 28: Rotace obrázku s mřížkou o 45° při použití bikubické filtrace.

Obrázek 29: Rotace obrázku s mřížkou o 5° při použití algoritmu pro nalezení nejbližšího pixelu.

Obrázek 30: Rotace obrázku s mřížkou o 5° při použití bikubické filtrace.

15. Význam konvolučních filtrů při práci s rastrovým obrazem

Velmi užitečná funkce z modulu image se skrývá pod jménem image.convolve. Ta slouží k aplikaci konvolučního filtru na obrázky. Pod pojmem filtrace si přitom můžeme představit soubor lokálních transformací rastrového obrazu, kterými se v případě monochromatických obrazů převádí hodnoty jasu původního obrazu na nové hodnoty jasu obrazu výstupního. Barevný obraz si můžeme pro účely filtrace představit jako tři monochromatické obrazy, z nichž každý obsahuje jas jedné barvové složky (povšimněte si, že to přesně odpovídá způsobu uložení barevných obrázků ve frameworku Torch). Podle vlastností funkčního vztahu pro výpočet jasu výsledného okolí na základě okolí O ve vstupním obrazu můžeme rozdělit metody filtrace na lineární a nelineární.

Lineární operace vyjadřují výslednou hodnotu jasu jako konvoluci okolí O příslušného bodu [i, j] a takzvaného konvolučního jádra (kernel). Mezi postupy předzpracování obrazu patří i algoritmy obnovení, které se obvykle také vyjadřují ve formě konvoluce. Okolím O, které se používá k výpočtu, je ale obecně celý obraz. Jedná se tedy o výpočetně velmi náročnou operaci. Obnovení se používá pro odstranění poruch s předem známými vlastnostmi jako například rozostření objektivu nebo rozmazání vlivem pohybu při snímání.

V dalším textu se však budeme zabývat pouze velmi jednoduchými konvolučními filtry, které pracují nad poměrně malým okolím zpracovávaných pixelů. Velikost konvolučního jádra určuje i velikost zpracovávaného okolí.

Nejpoužívanějším konvolučním filtrem je při práci s rastrovými obrazy bezesporu dvojdimenzionální konvoluční filtr. Jeho užitečnost spočívá především ve velkých možnostech změny rastrového obrazu, které přesahují možnosti jednodimenzionálních filtrů. Pomocí dvojdimenzionálních konvolučních filtrů je možné provádět ostření obrazu, rozmazávání, zvýrazňování hran nebo tvorbu reliéfů (vytlačeného vzoru) ze zadaného rastrového obrazu. Navíc je možné filtry řetězit a dosahovat tak různých složitějších efektů.

16. Příklady jednoduchých 2D konvolučních filtrů

Podívejme se na několik často používaných konvolučních filtrů, které většinou mají jádro o velikosti 3×3.

Obyčejné průměrování filtruje obraz tím, že nová hodnota jasu se spočítá jako aritmetický průměr jasu čtvercového nebo (méně často) obdélníkového okolí. Velikost skvrn šumu by měla být menší než velikost okolí a to by mělo být menší než nejmenší významný detail v obrazu, což je sice pěkná teorie, ovšem těžko dosažitelná. Při aplikaci tohoto filtru vždy dojde k rozmazání hran.

Konvoluční jádro filtru velikosti 3×3 pro obyčejné průměrování má tvar:

   1 |1 1 1|
h= - |1 1 1|
   9 |1 1 1|

Jednoduchým rozšířením obyčejného průměrování je průměrování s Gaussovským rozložením. Toto rozložení samozřejmě nelze použít bez dalších úprav, protože by velikost konvoluční masky byla nekonečná. Proto se konvoluční maska filtru vytvoří tak, že se zvýší váhy středového bodu masky a/nebo i jeho 4-okolí (tj. bodů, které mají se středovým bodem společnou jednu souřadnici, druhá se o jednotku liší). Jedna z možných podob konvoluční masky má tvar:

    1 |1 2 1|
h= -- |2 4 2|
   16 |1 2 1|

Všimněte si, že součet všech položek konvoluční matice dává po vynásobení vahou před maticí výslednou hodnotu 1. To zjednodušeně znamená, že se nemění celková světlost obrázku.

Mezi filtry používané pro zvýraznění hran patří Sobelův operátor. Pomocí tohoto operátoru jsou aproximovány první parciální derivace 2D funkce představované obrazem, jedná se tedy o operátor směrově závislý. Směr se u těchto operátorů udává podle světových stran. Sobelův operátor ve směru „sever“ má například tvar:

   | 1  2  1|
h= | 0  0  0|
   |-1 -2 -1|

Sobelův operátor v jiném směru lze získat rotací této matice.

17. Reprezentace jádra filtru a aplikace konvolučních filtrů v knihovně Torch

Jádro konvolučního filtru je, podobně jako samotný obrázek, reprezentováno tenzorem. Nejběžnější filtry mají jádro o velikosti 3×3, které lze nastavit například takto:

kernel = torch.Tensor({{ 1, 1, 1},
                       { 1, 1, 1},
                       { 1, 1, 1}})

Součet vah by měl být roven jedné (a nikoli devíti), což napravíme velmi snadno s využitím přetíženého operátoru / a funkce torch.sum (viz předchozí části seriálu):

kernel = kernel / torch.sum(kernel)

Nyní již můžeme načíst obrázek:

lenna = image.load(image_name)

Dále můžeme na načtený obrázek aplikovat konvoluční filtr:

target_image = image.convolve(source_image, kernel)

A uložit výsledek do nového souboru:

image.save(filename, target_image)

18. Demonstrační příklad – použití konvolučních filtrů

V dnešním posledním demonstračním příkladu si ukážeme použití vybraných konvolučních filtrů. Pro aplikaci filtru, výpis jádra filtru a uložení výsledného obrázku bude sloužit tato pomocná funkce:

function convoluteAndSaveImage(source_image, kernel, filename)
    target_image = image.convolve(source_image, kernel)
    print("Applying the following kernel to create " .. filename)
    print(kernel)
    image.save(filename, target_image)
end
 
 
setup(original_image_address, image_name)
 
lenna = image.load(image_name)

Aplikace nejjednoduššího rozmazávacího filtru s jádrem 3×3:

kernel = torch.Tensor({{ 1, 1, 1},
                       { 1, 1, 1},
                       { 1, 1, 1}})
 
kernel = kernel / torch.sum(kernel)
 
convoluteAndSaveImage(lenna, kernel, "box_blur_3x3.png")

Obrázek 31: Výsledek aplikace rozmazávacího filtru s jádrem 3×3.

Aplikace nejjednoduššího rozmazávacího filtru s jádrem 5×5:

kernel = torch.Tensor(5, 5)
kernel:fill(1)
 
kernel = kernel / torch.sum(kernel)
 
convoluteAndSaveImage(lenna, kernel, "box_blur_5x5.png")

Obrázek 32: Výsledek aplikace rozmazávacího filtru s jádrem 5×5.

Rozmazání pomocí Gaussovského filtru:

kernel = torch.Tensor({{ 1, 2, 1},
                       { 2, 4, 2},
                       { 1, 2, 1}})
 
kernel = kernel / torch.sum(kernel)
 
convoluteAndSaveImage(lenna, kernel, "gaussian_blur.png")

Obrázek 33: Výsledek aplikace Gaussovského rozmazávacího filtru s jádrem 3×3.

Zaostřovací filtr:

kernel = torch.Tensor({{ 0,-1, 0},
                       {-1, 5,-1},
                       { 0,-1, 0}})
 
convoluteAndSaveImage(lenna, kernel, "sharpen.png")

Obrázek 34: Výsledek aplikace zaostřovacího filtru.

Směrově nezávislá detekce hran:

kernel = torch.Tensor({{-1,-1,-1},
                       {-1, 8,-1},
                       {-1,-1,-1}})
 
convoluteAndSaveImage(lenna, kernel, "edge_detect.png")

Obrázek 35: Výsledek směrově nezávislé detekce hran.

Sobelův operátor:

kernel = torch.Tensor({{ 1, 2, 1},
                       { 0, 0, 0},
                       {-1,-2,-1}})
 
convoluteAndSaveImage(lenna, kernel, "sobel.png")

Obrázek 36: Výsledek aplikace Sobelova operátoru.

Detekce vertikálních hran (v jednom směru):

kernel = torch.Tensor({{-1, 1}})
 
convoluteAndSaveImage(lenna, kernel, "vertical_edge_detect.png")

Obrázek 37: Výsledek detekce vertikálních hran (v jednom směru).

Detekce horizontálních hran (v jednom směru):

kernel = torch.Tensor({{-1},
                       { 1}})
 
convoluteAndSaveImage(lenna, kernel, "horizontal_edge_detect.png")

Obrázek 38: Výsledek detekce horizontálních hran (v jednom směru).

19. Repositář s demonstračními příklady

Všechny demonstrační příklady, které jsme si popsali v předchozích kapitolách, najdete v GIT repositáři dostupném na adrese https://github.com/tisnik/torch-examples.git. Následují odkazy na zdrojové kódy jednotlivých příkladů:

20. Odkazy na Internetu

  1. Stránka projektu Torch
    http://torch.ch/
  2. Torch: Serialization
    https://github.com/torch/tor­ch7/blob/master/doc/seria­lization.md
  3. Torch: modul image
    https://github.com/torch/i­mage/blob/master/README.md
  4. Torch na GitHubu (několik repositářů)
    https://github.com/torch
  5. Torch (machine learning), Wikipedia
    https://en.wikipedia.org/wi­ki/Torch_%28machine_learnin­g%29
  6. Torch Package Reference Manual
    https://github.com/torch/tor­ch7/blob/master/README.md
  7. Torch Cheatsheet
    https://github.com/torch/tor­ch7/wiki/Cheatsheet
  8. Plotting with Torch7
    http://www.lighting-torch.com/2015/08/24/plotting-with-torch7/
  9. Plotting Package Manual with Gnuplot
    https://github.com/torch/gnu­plot/blob/master/README.md
  10. An Introduction to Tensors
    https://math.stackexchange­.com/questions/10282/an-introduction-to-tensors
  11. Differences between a matrix and a tensor
    https://math.stackexchange­.com/questions/412423/dif­ferences-between-a-matrix-and-a-tensor
  12. Qualitatively, what is the difference between a matrix and a tensor?
    https://math.stackexchange­.com/questions/1444412/qu­alitatively-what-is-the-difference-between-a-matrix-and-a-tensor?
  13. BLAS (Basic Linear Algebra Subprograms)
    http://www.netlib.org/blas/
  14. Basic Linear Algebra Subprograms (Wikipedia)
    https://en.wikipedia.org/wi­ki/Basic_Linear_Algebra_Sub­programs
  15. Comparison of deep learning software
    https://en.wikipedia.org/wi­ki/Comparison_of_deep_lear­ning_software
  16. TensorFlow
    https://www.tensorflow.org/
  17. Caffe2 (A New Lightweight, Modular, and Scalable Deep Learning Framework)
    https://caffe2.ai/
  18. PyTorch
    http://pytorch.org/
  19. Seriál o programovacím jazyku Lua
    http://www.root.cz/serialy/pro­gramovaci-jazyk-lua/
  20. LuaJIT – Just in Time překladač pro programovací jazyk Lua
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua/
  21. LuaJIT – Just in Time překladač pro programovací jazyk Lua (2)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-2/
  22. LuaJIT – Just in Time překladač pro programovací jazyk Lua (3)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-3/
  23. LuaJIT – Just in Time překladač pro programovací jazyk Lua (4)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-4/
  24. LuaJIT – Just in Time překladač pro programovací jazyk Lua (5 – tabulky a pole)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-5-tabulky-a-pole/
  25. LuaJIT – Just in Time překladač pro programovací jazyk Lua (6 – překlad programových smyček do mezijazyka LuaJITu)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-6-preklad-programovych-smycek-do-mezijazyka-luajitu/
  26. LuaJIT – Just in Time překladač pro programovací jazyk Lua (7 – dokončení popisu mezijazyka LuaJITu)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-7-dokonceni-popisu-mezijazyka-luajitu/
  27. LuaJIT – Just in Time překladač pro programovací jazyk Lua (8 – základní vlastnosti trasovacího JITu)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-8-zakladni-vlastnosti-trasovaciho-jitu/
  28. LuaJIT – Just in Time překladač pro programovací jazyk Lua (9 – další vlastnosti trasovacího JITu)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-9-dalsi-vlastnosti-trasovaciho-jitu/
  29. LuaJIT – Just in Time překladač pro programovací jazyk Lua (10 – JIT překlad do nativního kódu)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-10-jit-preklad-do-nativniho-kodu/
  30. LuaJIT – Just in Time překladač pro programovací jazyk Lua (11 – JIT překlad do nativního kódu procesorů s architekturami x86 a ARM)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-11-jit-preklad-do-nativniho-kodu-procesoru-s-architekturami-x86-a-arm/
  31. LuaJIT – Just in Time překladač pro programovací jazyk Lua (12 – překlad operací s reálnými čísly)
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua-12-preklad-operaci-s-realnymi-cisly/
  32. Lua Profiler (GitHub)
    https://github.com/luafor­ge/luaprofiler
  33. Lua Profiler (LuaForge)
    http://luaforge.net/projec­ts/luaprofiler/
  34. ctrace
    http://webserver2.tecgraf.puc-rio.br/~lhf/ftp/lua/
  35. The Lua VM, on the Web
    https://kripken.github.io/lu­a.vm.js/lua.vm.js.html
  36. Lua.vm.js REPL
    https://kripken.github.io/lu­a.vm.js/repl.html
  37. lua2js
    https://www.npmjs.com/package/lua2js
  38. lua2js na GitHubu
    https://github.com/basicer/lua2js-dist
  39. Lua (programming language)
    http://en.wikipedia.org/wi­ki/Lua_(programming_langu­age)
  40. LuaJIT 2.0 SSA IRhttp://wiki.luajit.org/SSA-IR-2.0
  41. The LuaJIT Project
    http://luajit.org/index.html
  42. LuaJIT FAQ
    http://luajit.org/faq.html
  43. LuaJIT Performance Comparison
    http://luajit.org/performance.html
  44. LuaJIT 2.0 intellectual property disclosure and research opportunities
    http://article.gmane.org/gma­ne.comp.lang.lua.general/58908
  45. LuaJIT Wiki
    http://wiki.luajit.org/Home
  46. LuaJIT 2.0 Bytecode Instructions
    http://wiki.luajit.org/Bytecode-2.0
  47. Programming in Lua (first edition)
    http://www.lua.org/pil/contents.html
  48. Lua 5.2 sources
    http://www.lua.org/source/5.2/
  49. REPL
    https://en.wikipedia.org/wi­ki/Read%E2%80%93eval%E2%80%93prin­t_loop
  50. The LLVM Compiler Infrastructure
    http://llvm.org/ProjectsWithLLVM/
  51. clang: a C language family frontend for LLVM
    http://clang.llvm.org/
  52. LLVM Backend („Fastcomp“)
    http://kripken.github.io/emscripten-site/docs/building_from_source/LLVM-Backend.html#llvm-backend
  53. Lambda the Ultimate: Coroutines in Lua,
    http://lambda-the-ultimate.org/node/438
  54. Coroutines Tutorial,
    http://lua-users.org/wiki/CoroutinesTutorial
  55. Lua Coroutines Versus Python Generators,
    http://lua-users.org/wiki/LuaCorouti­nesVersusPythonGenerators
Našli jste v článku chybu?