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
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
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
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/issues/854 a https://github.com/torch/image/commit/797fcb101b76c9b329b3ee83349b0f6adeacac94. 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ů:
Příklad | Adresa |
---|---|
01_image_from_tensor.lua | https://github.com/tisnik/torch-examples/blob/master/image/01_image_from_tensor.lua |
02_download_image.lua | https://github.com/tisnik/torch-examples/blob/master/image/02_download_image.lua |
03_image_get_size.lua | https://github.com/tisnik/torch-examples/blob/master/image/03_image_get_size.lua |
04_tensor_type.lua | https://github.com/tisnik/torch-examples/blob/master/image/04_tensor_type.lua |
05_crop.lua | https://github.com/tisnik/torch-examples/blob/master/image/05_crop.lua |
06_translate.lua | https://github.com/tisnik/torch-examples/blob/master/image/06_translate.lua |
07_flip.lua | https://github.com/tisnik/torch-examples/blob/master/image/07_flip.lua |
08_rescale.lua | https://github.com/tisnik/torch-examples/blob/master/image/08_rescale.lua |
09_rotate.lua | https://github.com/tisnik/torch-examples/blob/master/image/09_rotate.lua |
10_convolution.lua | https://github.com/tisnik/torch-examples/blob/master/image/10_convolution.lua |
20. Odkazy na Internetu
- Stránka projektu Torch
http://torch.ch/ - Torch: Serialization
https://github.com/torch/torch7/blob/master/doc/serialization.md - Torch: modul image
https://github.com/torch/image/blob/master/README.md - Torch na GitHubu (několik repositářů)
https://github.com/torch - Torch (machine learning), Wikipedia
https://en.wikipedia.org/wiki/Torch_%28machine_learning%29 - Torch Package Reference Manual
https://github.com/torch/torch7/blob/master/README.md - Torch Cheatsheet
https://github.com/torch/torch7/wiki/Cheatsheet - Plotting with Torch7
http://www.lighting-torch.com/2015/08/24/plotting-with-torch7/ - Plotting Package Manual with Gnuplot
https://github.com/torch/gnuplot/blob/master/README.md - An Introduction to Tensors
https://math.stackexchange.com/questions/10282/an-introduction-to-tensors - Differences between a matrix and a tensor
https://math.stackexchange.com/questions/412423/differences-between-a-matrix-and-a-tensor - Qualitatively, what is the difference between a matrix and a tensor?
https://math.stackexchange.com/questions/1444412/qualitatively-what-is-the-difference-between-a-matrix-and-a-tensor? - BLAS (Basic Linear Algebra Subprograms)
http://www.netlib.org/blas/ - Basic Linear Algebra Subprograms (Wikipedia)
https://en.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms - Comparison of deep learning software
https://en.wikipedia.org/wiki/Comparison_of_deep_learning_software - TensorFlow
https://www.tensorflow.org/ - Caffe2 (A New Lightweight, Modular, and Scalable Deep Learning Framework)
https://caffe2.ai/ - PyTorch
http://pytorch.org/ - Seriál o programovacím jazyku Lua
http://www.root.cz/serialy/programovaci-jazyk-lua/ - LuaJIT – Just in Time překladač pro programovací jazyk Lua
http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua/ - 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/ - 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/ - 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/ - 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/ - 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/ - 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/ - 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/ - 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/ - 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/ - 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/ - 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/ - Lua Profiler (GitHub)
https://github.com/luaforge/luaprofiler - Lua Profiler (LuaForge)
http://luaforge.net/projects/luaprofiler/ - ctrace
http://webserver2.tecgraf.puc-rio.br/~lhf/ftp/lua/ - The Lua VM, on the Web
https://kripken.github.io/lua.vm.js/lua.vm.js.html - Lua.vm.js REPL
https://kripken.github.io/lua.vm.js/repl.html - lua2js
https://www.npmjs.com/package/lua2js - lua2js na GitHubu
https://github.com/basicer/lua2js-dist - Lua (programming language)
http://en.wikipedia.org/wiki/Lua_(programming_language) - LuaJIT 2.0 SSA IRhttp://wiki.luajit.org/SSA-IR-2.0
- The LuaJIT Project
http://luajit.org/index.html - LuaJIT FAQ
http://luajit.org/faq.html - LuaJIT Performance Comparison
http://luajit.org/performance.html - LuaJIT 2.0 intellectual property disclosure and research opportunities
http://article.gmane.org/gmane.comp.lang.lua.general/58908 - LuaJIT Wiki
http://wiki.luajit.org/Home - LuaJIT 2.0 Bytecode Instructions
http://wiki.luajit.org/Bytecode-2.0 - Programming in Lua (first edition)
http://www.lua.org/pil/contents.html - Lua 5.2 sources
http://www.lua.org/source/5.2/ - REPL
https://en.wikipedia.org/wiki/Read%E2%80%93eval%E2%80%93print_loop - The LLVM Compiler Infrastructure
http://llvm.org/ProjectsWithLLVM/ - clang: a C language family frontend for LLVM
http://clang.llvm.org/ - LLVM Backend („Fastcomp“)
http://kripken.github.io/emscripten-site/docs/building_from_source/LLVM-Backend.html#llvm-backend - Lambda the Ultimate: Coroutines in Lua,
http://lambda-the-ultimate.org/node/438 - Coroutines Tutorial,
http://lua-users.org/wiki/CoroutinesTutorial - Lua Coroutines Versus Python Generators,
http://lua-users.org/wiki/LuaCoroutinesVersusPythonGenerators