Hlavní navigace

Framework Torch: modul pro zpracování rastrových obrázků (dokončení)

Pavel Tišnovský

Dnes dokončíme popis funkcí sloužících pro manipulaci s rastrovými obrázky. Kromě složitějších filtrů aplikovatelných na jakýkoli typ obrázků si ukážeme i operace binární morfologie, které jsou aplikovatelné na bitmapy.

Obsah

1. Framework Torch: modul pro zpracování rastrových obrázků (dokončení)

2. Konstrukce Gaussova filtru

3. Použití Gaussova filtru pro filtraci zašuměného obrazu

4. Laplaceův filtr

5. Použití Laplaceova filtru pro zvýraznění změn v obrazu

6. Konstrukce Gaussovské pyramidy

7. Použití Gaussovské pyramidy na zvolený obrázek

8. Jednodimenzionální Gaussovský filtr

9. Zobrazení hodnot jádra jednorozměrného Gaussovského filtru

10. Zobrazení dvourozměrného Gaussovského filtru

11. Aplikace Gaussova filtru pro odstranění šumu v jednorozměrném signálu

12. Demonstrační příklad: aplikace Gaussova filtru se zobrazením výsledků

13. Další pokročilejší filtry nabízené modulem image

14. Zpracování binárních obrázků – operace binární morfologie

15. Operace dilatace (binární morfologie)

16. Operace eroze (binární morfologie)

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

18. Odkazy na Internetu

1. Framework Torch: modul pro zpracování rastrových obrázků (dokončení)

Dnešní článek o frameworku Torch bude rozdělen do tří nestejně rozsáhlých částí. V úvodní části si ukážeme některé způsoby aplikace Gaussova a Laplaceova filtru na rastrové obrázky. Tyto filtry jsou podrobněji popsány z toho důvodu, že se v praxi používají velmi často, a to na začátku celého zpracování (typicky se musí odstranit šum, zpracovat hrany, provést segmentaci obrazu, použít neuronovou síť na rozpoznání objektů atd.). Ve druhé části se budeme podrobněji zabývat použitím Gaussovského filtru, a to i pro filtraci jednodimenzionálního signálu.

Obrázek 1: Obrázek Lenny, který budeme používat v dalších demonstračních příkladech.

Část třetí je věnována operacím binární (matematické) morfologie (mathematical morphology). Tyto operace jsou aplikovány na bitmapy, tj. na takové rastrové obrázky, v nichž je každý pixel reprezentován jediným bitem. Mezi dvě základní operace podporované i frameworkem Torch patří dilatace a eroze, které je možné v případě potřeby kombinovat s dalšími operacemi, například s odečtením dvou obrázků od sebe atd.

Obrázek 2: Bitmapa (binární rastrový obraz) s mřížkou, který budeme používat při testování morfologických operací dilatace a eroze.

2. Konstrukce Gaussova filtru

Dvoudimenzionální Gaussův (někdy též nazývaný Gaussovský) filtr se vytvoří velmi snadno pomocí metody gaussian:

kernel = image.gaussian(size, sigma, amplitude, normalize)

Význam jednotlivých parametrů této funkce je následující:

Parametr Stručný popis
size šířka a výška výsledné matice s jádrem filtru
sigma směrodatná odchylka, řídí strmost křivky
amplitude amplituda, výchozí hodnota je rovna jedné
normalize povolení či zákaz normalizace hodnot tak, aby byl součet roven jedné

Příklad použití:

th> image.gaussian(3, 0.2, 1, true)
 0.0277  0.1110  0.0277
 0.1110  0.4452  0.1110
 0.0277  0.1110  0.0277
[torch.DoubleTensor of size 3x3]

Větší jádro filtru a strmější křivka. Hodnoty jsou normalizovány, takže jejich součet bude roven jedné:

th> image.gaussian(5, 0.1, 1, true)
 6.9625e-08  2.8089e-05  2.0755e-04  2.8089e-05  6.9625e-08
 2.8089e-05  1.1332e-02  8.3731e-02  1.1332e-02  2.8089e-05
 2.0755e-04  8.3731e-02  6.1869e-01  8.3731e-02  2.0755e-04
 2.8089e-05  1.1332e-02  8.3731e-02  1.1332e-02  2.8089e-05
 6.9625e-08  2.8089e-05  2.0755e-04  2.8089e-05  6.9625e-08
[torch.DoubleTensor of size 5x5]

Zákaz normalizace, takže prostřední prvek bude mít hodnotu 1:

th> image.gaussian(5, 0.1, 1, false)
 1.1254e-07  4.5400e-05  3.3546e-04  4.5400e-05  1.1254e-07
 4.5400e-05  1.8316e-02  1.3534e-01  1.8316e-02  4.5400e-05
 3.3546e-04  1.3534e-01  1.0000e+00  1.3534e-01  3.3546e-04
 4.5400e-05  1.8316e-02  1.3534e-01  1.8316e-02  4.5400e-05
 1.1254e-07  4.5400e-05  3.3546e-04  4.5400e-05  1.1254e-07
[torch.DoubleTensor of size 5x5]

V praxi se nejdříve vytvoří filtr, který se ihned použije pro filtraci obrazu. V tomto případě většinou budeme chtít, aby byly prvky jádra normalizovány (ostatně zkuste si sami, co se stane, pokud poslední parametr změníte na false):

kernel = image.gaussian(size, sigma, 1, true)
target_image = image.convolve(source_image, kernel)
image.save(filename, target_image)

3. Použití Gaussova filtru pro filtraci zašuměného obrazu

Výše uvedenou funkci pro vytvoření jádra Gaussovského filtru použijeme v dnešním prvním příkladu. Ten začíná podobně, jako příklady z předchozí části seriálu – pomocnými funkcemi pro stažení obrázku „pravé Lenny“ namísto použití konstruktoru image.lena():

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)

Obrázek 3: Aplikace Gaussova filtru s velikostí jádra 3×3 prvky a směrodatnou odchylkou 0,2 (obrázek se zmenšil o pixel na každé straně).

Obrázek Lenny máme stažený, takže ho načteme:

lenna = image.load(image_name)

Obrázek 4: Aplikace Gaussova filtru s velikostí jádra 5×5 prvků a směrodatnou odchylkou 0,2 (obrázek se zmenšil o dva pixely na každé straně).

Jádro příkladu – funkce pro vytvoření filtru a jeho následnou aplikaci na testovací obrázek:

function convoluteAndSaveImage(source_image, size, sigma, filename)
    kernel = image.gaussian(size, sigma, 1, true)
    target_image = image.convolve(source_image, kernel)
    print("Applying the following kernel to create " .. filename)
    print(kernel)
    image.save(filename, target_image)
end

Obrázek 5: Aplikace Gaussova filtru s velikostí jádra 7×7 prvků a směrodatnou odchylkou 0,1.

Připravíme si parametry Gaussova filtru – velikost jádra (kernelu) a směrodatnou odchylku (sigma):

sizes = {3, 5, 7}
sigmas = {0.1, 0.2, 1.1}

Obrázek 6: Aplikace Gaussova filtru s velikostí jádra 7×7 prvků a směrodatnou odchylkou 0,2.

Nyní vygenerujeme všechny kombinace parametrů nutných pro konstrukci filtru, vytvoříme filtr, aplikujeme ho na testovací obrázek a uložíme výsledek do externího souboru:

for _, size in ipairs(sizes) do
    for __, sigma in ipairs(sigmas) do
        filename = "gaussian_" .. size .. "x" .. size .. "_sigma_" .. sigma .. ".png"
        convoluteAndSaveImage(lenna, size, sigma, filename)
    end
end

Obrázek 7: Aplikace Gaussova filtru s velikostí jádra 7×7 prvků a směrodatnou odchylkou 1,1.

4. Laplaceův filtr

Laplaceův filtr (Laplacián, i když striktně chápáno je to nesprávné jméno) slouží ke zvýraznění změn v obrázku („doostření“), popř. jako jeden z kroků při detekci hran, které se v obrázku nachází. Taktéž se jedná o konvoluční filtr, což znamená, že se jádro filtru (kernel) postupně aplikuje na všechny pixely zdrojového obrázku i na jejich okolí. Při konstrukci jádra filtru se opět volí rozměry výsledné matice (typicky 3×3 prvky), směrodatná odchylka, amplituda 2D funkce a taktéž lze povolit či zakázat normalizaci prvků kernelu. Ukažme si příklady, podobně jako jsme to udělali ve druhé kapitole v případě Gaussova filtru:

Filtr s jádrem o rozměrech 3×3 prvky. Povšimněte si, že kromě prostředního prvku mají okolní prvky zápornou hodnotu, což je důvod, proč tento filtr zvýrazňuje změny a nezahlazuje je (jako filtr Gaussův):

th> image.laplacian(3, 0.1, 1, true)
-0.0002 -0.0190 -0.0002
-0.0190  1.0765 -0.0190
-0.0002 -0.0190 -0.0002
[torch.DoubleTensor of size 3x3]

Filtr o rozměru jádra 5×5 prvků se zakázanou normalizací (prostřední prvek je roven jedné):

th> image.laplacian(5, 0.1, 1, false)
-1.6880e-06 -4.0860e-04 -2.3482e-03 -4.0860e-04 -1.6880e-06
-4.0860e-04 -5.4947e-02 -1.3534e-01 -5.4947e-02 -4.0860e-04
-2.3482e-03 -1.3534e-01  1.0000e+00 -1.3534e-01 -2.3482e-03
-4.0860e-04 -5.4947e-02 -1.3534e-01 -5.4947e-02 -4.0860e-04
-1.6880e-06 -4.0860e-04 -2.3482e-03 -4.0860e-04 -1.6880e-06
[torch.DoubleTensor of size 5x5]

Od určité hodnoty směrodatné odchylky již funkce nestačí „překmitnout“ do záporných hodnot a filtr přestane doostřovat:

th> image.laplacian(7, 0.45, 1, false)
 0.0375  0.1792  0.2997  0.3472  0.2997  0.1792  0.0375
 0.1792  0.3988  0.5814  0.6527  0.5814  0.3988  0.1792
 0.2997  0.5814  0.8130  0.9029  0.8130  0.5814  0.2997
 0.3472  0.6527  0.9029  1.0000  0.9029  0.6527  0.3472
 0.2997  0.5814  0.8130  0.9029  0.8130  0.5814  0.2997
 0.1792  0.3988  0.5814  0.6527  0.5814  0.3988  0.1792
 0.0375  0.1792  0.2997  0.3472  0.2997  0.1792  0.0375
[torch.DoubleTensor of size 7x7]

5. Použití Laplaceova filtru pro zvýraznění změn v obrazu

Opět si ukažme praktické použití Laplaceova filtru pro zvýraznění změn v obrazu. Nejprve si připravíme funkci pro konstrukci jádra filtru, aplikaci filtru na testovací obrázek a uložení výsledku do souboru:

function convoluteAndSaveImage(source_image, size, sigma, filename)
    kernel = image.laplacian(size, sigma, 1, true)
    target_image = image.convolve(source_image, kernel)
    print("Applying the following kernel to create " .. filename)
    print(kernel)
    image.save(filename, target_image)
end

Obrázek 8: Aplikace Laplaceova filtru s velikostí jádra 3×3 prvky a směrodatnou odchylkou 0,2.

Načteme testovací obrázek Lenny:

setup(original_image_address, image_name)
 
lenna = image.load(image_name)

Obrázek 9: Aplikace Laplaceova filtru s velikostí jádra 5×5 prvků a směrodatnou odchylkou 0,1.

Připravíme si parametry Laplaceova filtru – velikost jádra (kernelu) a směrodatnou odchylku (sigma):

sizes = {3, 5, 7}
sigmas = {0.1, 0.2, 0.25, 0.3, 0.5}

Obrázek 10: Aplikace Laplaceova filtru s velikostí jádra 5×5 prvků a směrodatnou odchylkou 0,2.

Nyní vygenerujeme všechny kombinace parametrů nutných pro konstrukci filtru, vytvoříme filtr, aplikujeme ho na testovací obrázek a uložíme výsledek do externího souboru:

for _, size in ipairs(sizes) do
    for __, sigma in ipairs(sigmas) do
        filename = "laplacian_" .. size .. "x" .. size .. "_sigma_" .. sigma .. ".png"
        convoluteAndSaveImage(lenna, size, sigma, filename)
    end
end

Obrázek 11: Aplikace Laplaceova filtru s velikostí jádra 7×7 prvků a směrodatnou odchylkou 0,1.

Obrázek 12: Aplikace Laplaceova filtru s velikostí jádra 7×7 prvků a směrodatnou odchylkou 0,2.

6. Konstrukce Gaussovské pyramidy

V modulu image nalezneme i další užitečnou funkci nazvanou gaussianpyramid. Tato funkce slouží ke konstrukci takzvané Gaussovské pyramidy, což je – zjednodušeně řečeno – soubor rastrových obrázků, které vzniknou z původního zdrojového obrázku postupným zmenšováním a aplikací Gaussova filtru. Při konstrukci pyramidy se musí specifikovat měřítka vytvářených obrázků, která většinou mívají hodnotu 2-n, tedy například 1/2, 1/4, 1/8 atd. Podívejme se na příklad postupného zmenšování zdrojového obrázku Lenny, jehož původní rozlišení je 512×512 pixelů:

Obrázek 13: První úroveň pyramidy (nultá úroveň je představována obrázkem číslo 1 na začátku článku).

Obrázek 14: Druhá úroveň pyramidy.

Obrázek 15: Třetí úroveň pyramidy.

Obrázek 16: Čtvrtá úroveň pyramidy.

7. Použití Gaussovské pyramidy na zvolený obrázek

Konstrukce Gaussovské pyramidy, tedy několika rastrových obrázků s postupně se zmenšujícím rozlišením, je velmi snadná, jak je to ostatně patrné z následujícího demonstračního příkladu (část, která se stará o stažení obrázku Lenny jsme již vynechali):

setup(original_image_address, image_name)
 
lenna = image.load(image_name)
 
pyramid = image.gaussianpyramid(lenna, {1/2, 1/4, 1/8, 1/16})
 
for i, img in ipairs(pyramid) do
    image.save("pyramid_level" .. i .. ".png", img)
end

8. Jednodimenzionální Gaussovský filtr

V knihovně image frameworku Torch najdeme i funkci nazvanou gaussian1D, která slouží k vytvoření jádra jednorozměrného Gaussovského filtru. Toto jádro je reprezentováno obyčejným 1D vektorem, přičemž počet prvků vektoru odpovídá zvolené velikosti filtru. Funkci gaussian1D se předává pět parametrů:

image.gaussian1D(size, sigma, amplitude, normalize, mean)

Význam jednotlivých parametrů:

Parametr Stručný popis
size počet prvků výsledného vektoru s jádrem filtru
sigma směrodatná odchylka, řídí strmost křivky
amplitude amplituda, výchozí hodnota je rovna jedné
normalize povolení či zákaz normalizace hodnot tak, aby byl součet roven jedné
mean střední hodnota, umožňuje posun křivky, z níž se odvozují koeficienty filtru

Filtr s „klasickými“ hodnotami vznikne tak, že se amplituda nastaví na jedničku, povolí se normalizace hodnot, střední hodnota se nastaví na 1/2 a směrodatná odchylka na takovou hodnotu, aby to odpovídalo požadované strmosti:

th> image.gaussian1D(10, 0.1, 1.0, true, 0.5)
 0.0000
 0.0009
 0.0175
 0.1295
 0.3521
 0.3521
 0.1295
 0.0175
 0.0009
 0.0000
[torch.DoubleTensor of size 10]

Dtto, ale bez provedení normalizace:

th> image.gaussian1D(10, 0.1, 1.0, false, 0.5)
 0.0000
 0.0022
 0.0439
 0.3247
 0.8825
 0.8825
 0.3247
 0.0439
 0.0022
 0.0000
[torch.DoubleTensor of size 10]

„Vycentrování“ díky tomu, že vygenerujeme vektor s jedenácti prvky. Prostřední prvek již má (bez provedené normalizace) hodnotu 1:

th> image.gaussian1D(11, 0.1, 1.0, false, 0.5)
 3.2620e-05
 1.3447e-03
 2.4258e-02
 1.9150e-01
 6.6151e-01
 1.0000e+00
 6.6151e-01
 1.9150e-01
 2.4258e-02
 1.3447e-03
 3.2620e-05
[torch.DoubleTensor of size 11]

Změna směrodatné odchylky:

th> image.gaussian1D(10, 0.5, 1.0, true, 0.5)
 0.0779
 0.0914
 0.1030
 0.1116
 0.1162
 0.1162
 0.1116
 0.1030
 0.0914
 0.0779
[torch.DoubleTensor of size 10]

Posun koeficientů doprava (hodnoty prvků nyní monotónně rostou):

th> image.gaussian1D(10, 0.3, 1.0, true, 1.0)
 0.0018
 0.0048
 0.0117
 0.0255
 0.0496
 0.0864
 0.1348
 0.1881
 0.2349
 0.2625
[torch.DoubleTensor of size 10]

9. Zobrazení hodnot jádra jednorozměrného Gaussovského filtru

Namísto sledování hodnot prvků vektorů bude názornější, když si prvky Gaussovského filtru necháme zobrazit přímo knihovnou Torch. Všechny znalosti k tomu potřebné již máme, protože umíme vykreslit jednoduchý graf funkce jedné nezávislé proměnné s využitím knihovny Gnuplot. Podívejme se nyní, jak se vykreslí čtyři různé Gaussovské filtry, přesněji řečeno hodnoty prvků těchto filtrů, které se navzájem propojí úsečkami.

Příprava:

require("image")
require("gnuplot")
 
 
SIZE = 40

Dále si připravíme pomocný vektor, díky němuž bude možné zobrazit hodnoty na x-ové ose:

x = torch.linspace(0, 1.0, SIZE)

Vytvoření čtyř vektorů s jádry Gaussovského filtru:

y1 = image.gaussian1D(SIZE, 0.1, 1.0, true, 0.5)
y2 = image.gaussian1D(SIZE, 0.2, 1.0, true, 0.5)
y3 = image.gaussian1D(SIZE, 0.3, 1.0, true, 0.5)
y4 = image.gaussian1D(SIZE, 0.1, 1.0, true, 0.25)

Nastavení parametrů grafu:

gnuplot.pngfigure("gaussian.png")
gnuplot.title("Gaussian")
gnuplot.xlabel("x")
gnuplot.ylabel("Gaussian")
gnuplot.raw('unset xtics')

Vykreslení všech čtyř průběhů i s legendou:

gnuplot.plot({"sigma=0.1, mean=1/2", y1},
             {"sigma=0.2, mean=1/2", y2},
             {"sigma=0.3, mean=1/2", y3},
             {"sigma=0.1, mean=1/4", y4})
gnuplot.plotflush()
gnuplot.close()

Obrázek 17: Výsledek běhu předchozího příkladu zobrazujícího hodnoty čtyř Gaussovských filtrů.

10. Zobrazení dvourozměrného Gaussovského filtru

Pro zajímavost si ještě zobrazme hodnoty koeficientů dvourozměrného Gaussovského filtru. Použijeme přitom funkci gnuplot.imagesc, která jednotlivé hodnoty reprezentuje změnou barvy ve zvolené barvové škále či ve stupních šedi.

Opět začneme importem potřebných modulů:

require("image")
require("gnuplot")

Dále vytvoříme dvě jádra filtrů, každé o velikosti 30×30 koeficientů:

z1 = image.gaussian(30, 0.1, 1.0, false)
z2 = image.gaussian(30, 1.1, 1.0, false)

Obrázek 18: Výsledek běhu předchozího příkladu zobrazujícího hodnotu dvoudimenzionálního Gaussovského filtru.

Zbývá nám vykreslení hodnot 2D mřížky, k čemuž nám poslouží nám již známá funkce imagesc:

gnuplot.pngfigure("2d_gaussian_1.png")
gnuplot.imagesc(z1, 'color')
gnuplot.plotflush()
gnuplot.close()
 
gnuplot.pngfigure("2d_gaussian_2.png")
gnuplot.imagesc(z2, 'color')
gnuplot.plotflush()
gnuplot.close()

Obrázek 19: Výsledek běhu předchozího příkladu zobrazujícího hodnotu dvoudimenzionálního Gaussovského filtru (odlišné koeficienty).

11. Aplikace Gaussova filtru pro odstranění šumu v jednorozměrném signálu

Ukažme si nyní, jak je možné použít jednodimenzionální Gaussův filtr pro odstranění šumu z jednorozměrného signálu. Bude to nepatrně složitější, protože nelze použít funkci pro 1D konvoluci. Nejprve vytvoříme signál bez šumu, což bude čistá sinusovka (SIZE je počet prvků výsledného vektoru):

x = torch.linspace(0, 2*math.pi, SIZE)
fce = torch.sin(x)

Dále sinusovku „zašumíme“ náhodným signálem, tedy zhruba takto:

fcernd = fce + (torch.rand(SIZE)-1/2)/2

Obrázek 20: Modrá křivka – původní signál, zelená křivka – zašuměný signál, červená šipka – po odstranění šumu filtrem o délce tří prvků.

Nyní zkonstruujeme jednorozměrný Gaussovský filtr, tj. vektor se třemi, pěti či sedmi prvky (můžete zkusit i větší filtr popř. změnit hodnotu sigma):

gaussian = image.gaussian1D(gaussian_size, 0.3, 1.0, true, 0.5)

Konvoluci budeme muset ošidit, protože se jak původní signál, tak i Gaussovský filtr převede na 2D matice s jediným řádkem a počtem sloupců odpovídajících délce původního vektoru:

filtered = image.convolve(fcernd:reshape(1,SIZE), gaussian:reshape(1,gaussian_size), "same")

Obrázek 21: Modrá křivka – původní signál, zelená křivka – zašuměný signál, červená šipka – po odstranění šumu filtrem o délce pěti prvků.

Všechny tři signály si necháme zobrazit:

gnuplot.pngfigure(filename)
gnuplot.raw("set pointsize 0.0")
gnuplot.title("Gaussian blur")
gnuplot.xlabel("x")
gnuplot.ylabel("fce")
gnuplot.plot({{"sin(x)", x, fce, "-"},
             {"sin(x)+random", x, fcernd, "-"},
             {"filtered", x, filtered[1], "-"}}
)
gnuplot.plotflush()
gnuplot.close()

Obrázek 22: Modrá křivka – původní signál, zelená křivka – zašuměný signál, červená šipka – po odstranění šumu filtrem o délce sedmi prvků.

12. Demonstrační příklad: aplikace Gaussova filtru se zobrazením výsledků

Úplný zdrojový kód příkladu, v němž se aplikuje Gaussův filtr o zvolené velikosti na „zašuměný“ signál, vypadá následovně. Popis jednotlivých kroků jsme si již řekli v předchozí kapitole:

require("image")
require("gnuplot")
 
 
SIZE = 100
 
x = torch.linspace(0, 2*math.pi, SIZE)
fce = torch.sin(x)
fcernd = fce + (torch.rand(SIZE)-1/2)/2
 
 
function filter(fcerndm, gaussian_size, filename)
    gaussian = image.gaussian1D(gaussian_size, 0.3, 1.0, true, 0.5)
    filtered = image.convolve(fcernd:reshape(1,SIZE), gaussian:reshape(1,gaussian_size), "same")
 
    gnuplot.pngfigure(filename)
    gnuplot.raw("set pointsize 0.0")
    gnuplot.title("Gaussian blur")
    gnuplot.xlabel("x")
    gnuplot.ylabel("fce")
    gnuplot.plot({{"sin(x)", x, fce, "-"},
                 {"sin(x)+random", x, fcernd, "-"},
                 {"filtered", x, filtered[1], "-"}}
    )
    gnuplot.plotflush()
    gnuplot.close()
end
 
filter(fcernd, 3, "gaussian_blur_3.png")
filter(fcernd, 5, "gaussian_blur_5.png")
filter(fcernd, 7, "gaussian_blur_7.png")

13. Další pokročilejší filtry nabízené modulem image

Mezi další užitečné filtry patří především filtr LCN neboli Local contrast normalization. Ve výchozím nastavení tento filtr používá jádro odpovídající Gaussovskému filtru o rozměrech 9×9 prvků. Postup aplikace LCN je následující:

  1. Normalizace kontrastu zdrojového obrázku.
  2. Vytvoření jádra filtru.
  3. Lokální normalizace kontrastu podle vzorce res = (src – lm(src)) / sqrt(lm(src) – lm(src*src)), přičemž funkce lm() počítá lokální kontrast s využitím filtru vytvořeného v předchozím kroku

Podívejme se nyní, co se stane, pokud tento filtru budeme aplikovat na testovací obrázek Lenny. Povšimněte si, že odečtem lokálního kontrastu od pixelů původního obrázku se zvýraznily změny v obrazu:

Obrázek 23: Výsledek aplikace LCN (výsledek je zmenšen o čtyři pixely na každé straně).

Obrázek číslo 23 byl vytvořen následujícím příkladem:

require("image")
require("gnuplot")
 
 
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 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)
 
lenna = image.load(image_name, 1)
image.save("lenna_grayscale.png", lenna)
 
lcn_default = image.lcn(lenna)
image.save("lcn_default.png", lcn_default)

Vzhledem k tomu, že pro LCN je možné použít i filtr s odlišným jádrem, můžeme si trošku zaexperimentovat:

require("image")
require("gnuplot")
 
 
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 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
 
 
function applyLcnAndSaveImage(source_image, size, sigma, filename)
    kernel = image.gaussian(size, sigma, 0.5, true)
    target_image = image.lcn(source_image, kernel)
    print("Applying the following filter to create " .. filename)
    print(kernel)
    image.save(filename, target_image)
end
 
 
setup(original_image_address, image_name)
 
lenna = image.load(image_name, 1)
image.save("lenna_grayscale.png", lenna)
 
sizes = {3, 5, 7, 15}
sigmas = {0.2, 0.5, 1.1}
 
for _, size in ipairs(sizes) do
    for __, sigma in ipairs(sigmas) do
        filename = "custom_lcn_" .. size .. "x" .. size .. "_sigma_" .. sigma .. ".png"
        applyLcnAndSaveImage(lenna, size, sigma, filename)
    end
end

Obrázek 24: LCN používající Gaussovský filtr s jádrem 3×3, sigma je nastavena na hodnotu 1,1.

Obrázek 25: LCN používající Gaussovský filtr s jádrem 5×5, sigma je nastavena na hodnotu 1,1.

Obrázek 26: LCN používající Gaussovský filtr s jádrem 7×7, sigma je nastavena na hodnotu 1,1.

Obrázek 27: LCN používající Gaussovský filtr s jádrem 15×15, sigma je nastavena na hodnotu 1,1.

14. Zpracování binárních obrázků – operace binární morfologie

V knihovně Torch nalezneme i některé operace binární morfologie. Jedná se o specifické filtry prováděné nad bitmapami, ovšem slovo „bitmapa“ zde musíme chápat v původním významu „rastrový obrázek, v němž je hodnota každého pixelu reprezentovaná jediným bitem“. Většina operací binární morfologie se používá při rozpoznávání tvarů či objektů v obrázcích (morphological image processing). Typické je použití těchto operací v OCR (Optical Character Recognition); jednou z těchto operací je vytvoření kostry (skeleton) znaku, který má být následně rozpoznán. Ovšem například dilataci a erozi lze (při vhodné aplikaci) použít pro zvýraznení hran v původním černobílém či plnobarevném obrázku. Čtyři základní operace binární morfologie jsou vypsány v následující tabulce spolu s informací o tom, zda má význam aplikovat danou operaci několikrát (pokud je u operace znak ×, nemá opakované použití vliv na výsledek):

Operace Vícenásobné opakování Podpora v Torchi Podpora v Matlabu
Otevření × × imopen
Uzavření × × imclose
Dilatace imdilate
Eroze imerode

Obrázek 28: Bitmapa, tj. dvoubarevný rastrový obrázek.

Poznámka: dnes je již použití klasických bitmap do značné míry omezeno na zpracování obrazu mj. i výše uvedenými funkcemi (morfologie obrazu), specifikaci výběrových masek v rastrových grafických editorech popř. pro ikony či pro specifický pixel art [1] [2] [3].

Poznámka2: jedním z filtrů, které se aplikují na bitmapu, je možné implementovat i slavnou hříčku Život Johna Conwaye.

15. Operace dilatace (binární morfologie)

První morfologickou operací, která je frameworkem Torch podporována, je dilatace. Tuto operaci lze povést nad bitmapou zavoláním funkce:

target = image.dilate(source)

popř. lze explicitně specifikovat jádro (kernel) neboli strukturní element:

target = image.dilate(source, kernel)

Obrázek 29: Dilatace mřížky ze druhého obrázku, první iterace.

Posledním nepovinným parametrem je možné určit, jakou hodnotu (0 nebo 1) mají imaginární pixely okolo obrázku, protože strukturní element se aplikuje i na okraji obrázku, tudíž zasahuje mimo něj. Implicitní hodnotou je v tomto případě nula:

target = image.dilate(source, kernel, 1)

Obrázek 30: Dilatace mřížky ze druhého obrázku, druhá iterace.

V případě, že se použije standardní strukturní element o velikosti 3×3 pixely, dojde při aplikaci operace dilatace k následujícím změnám ve výsledném obrázku:

  1. Díry o maximální velikosti jednoho pixelu jsou zaplněny (zmizí).
  2. Hrany objektů se o jeden pixel zvětší (zvětšení je měřeno v horizontálním a vertikálním směru). Říká se, že objekty získají novou „slupku“.
  3. Blízké objekty jsou spojeny.

Obrázek 31: Dilatace mřížky ze druhého obrázku, třetí iterace.

Operaci dilatace je možné aplikovat vícekrát za sebou. V takovém případě bude docházet k neustálému narůstání „slupky“ okolo objektů. Pojďme si to ukázat. Nejprve vytvoříme binární obrázek s mřížkou. Nezapomínejme na to, že bílá barva reprezentuje objekt, černá barva pozadí:

require("image")
require("gnuplot")
 
 
SIZE = 256
GRID = 16
BORDER = 100
 
 
function createImageWithGrid(size, grid, border)
    img=torch.Tensor(size, size)
    img:zero()
 
    for row = border, size-border, grid do
        img:narrow(1, row, 1):fill(1)
    end
 
    for column = border, size-border, grid do
        img:narrow(2, column, 1):fill(1)
    end
 
    return img
end
 
 
source = createImageWithGrid(SIZE, GRID, BORDER)

Obrázek 32: Dilatace mřížky ze druhého obrázku, čtvrtá iterace.

Původní mřížku uložíme do souboru, abychom si ji mohli prohlédnout:

image.save("original_grid.png", source)

Obrázek 33: Dilatace mřížky ze druhého obrázku, pátá iterace.

Nyní budeme sedmkrát za sebou na obrázek aplikovat operaci dilatace a mezivýsledek si vždy uložíme do souboru. Po sedmé aplikaci již prakticky dojde ke spojení mřížky do jediného kříže:

for i=1,7 do
    dilated = image.dilate(source)
    image.save("dilated" .. i .. ".png", dilated)
    source = dilated
end

Obrázek 34: Dilatace mřížky ze druhého obrázku, šestá iterace.

Obrázek 35: Dilatace mřížky ze druhého obrázku, sedmá iterace.

16. Operace eroze (binární morfologie)

Zatímco operace dilatace sloužila pro vyplnění děr v objektech a pro jejich rozšíření o další slupky, je operace eroze vlastně přímým opakem, protože dokáže objekty zeštíhlit. Tuto operaci zavoláme pomocí funkce:

target = image.erode(source)

popř. je možné (opět) explicitně specifikovat jádro (kernel) neboli strukturní element:

target = image.erode(source, kernel)

Obrázek 36: Eroze „tlusté“ mřížky z obrázku číslo 34, první iterace.

Poslední alternativa volání slouží ke specifikaci hodnoty imaginárních pixelů okolo obrázku. Implicitní hodnotou je v tomto případě jednička!:

target = image.erode(source, kernel, 0)

Obrázek 37: Eroze „tlusté“ mřížky z obrázku číslo 34, druhá iterace.

V případě, že se použije standardní strukturní element o velikosti 3×3 pixely, dojde při aplikaci operace eroze k následujícím změnám ve výsledném obrázku:

  1. Body o maximální velikosti jednoho pixelu jsou vymazány (zmizí).
  2. Hrany o šířce jeden pixel taktéž zmizí.
  3. Ostatní hrany jsou zúženy o jeden pixel.
  4. Může dojít k rozdělení (rozpadu) složitějších objektů.

Obrázek 38: Eroze „tlusté“ mřížky z obrázku číslo 34, třetí iterace.

Poznámka: erozi je možné použít například pro detekci hran. Postačí od původního obrázku odečíst obrázek erodovaný, k čemuž nám v Torchi dobře poslouží přetížený operátor -.

Příklad pro ukázání operace eroze začíná stejně jako příklad předchozí, protože si nejprve připravíme mřížku:

require("image")
require("gnuplot")
 
 
SIZE = 256
GRID = 16
BORDER = 100
 
 
function createImageWithGrid(size, grid, border)
    img=torch.Tensor(size, size)
    img:zero()
 
    for row = border, size-border, grid do
        img:narrow(1, row, 1):fill(1)
    end
 
    for column = border, size-border, grid do
        img:narrow(2, column, 1):fill(1)
    end
 
    return img
end
 
 
source = createImageWithGrid(SIZE, GRID, BORDER)

Uložíme ji do souboru:

image.save("original_grid.png", source)

Obrázek 39: Eroze „tlusté“ mřížky z obrázku číslo 34, čtvrtá iterace.

Vytvoříme „tlustou“ verzi mřížky:

for i=1,5 do
    dilated = image.dilate(source)
    source = dilated
end

Obrázek 40: Eroze „tlusté“ mřížky z obrázku číslo 34, pátá iterace.

A potom ji opět ztenčíme, tentokrát již kýženou operací eroze:

for i=1,7 do
    eroded = image.erode(source)
    image.save("eroded" .. i .. ".png", eroded)
    source = eroded
end

Po šesté iteraci ovšem celý objekt zmizí, neboť byl ztenčen na nula pixelů.

17. 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ů:

18. 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. Gaussian filter
    https://en.wikipedia.org/wi­ki/Gaussian_filter
  12. Gaussian function
    https://en.wikipedia.org/wi­ki/Gaussian_function
  13. Laplacian/Laplacian of Gaussian
    http://homepages.inf.ed.ac­.uk/rbf/HIPR2/log.htm
  14. Odstranění šumu
    https://cs.wikipedia.org/wi­ki/Odstran%C4%9Bn%C3%AD_%C5%A­1umu
  15. Binary image
    https://en.wikipedia.org/wi­ki/Binary_image
  16. Erosion (morphology)
    https://en.wikipedia.org/wi­ki/Erosion_%28morphology%29
  17. Dilation (morphology)
    https://en.wikipedia.org/wi­ki/Dilation_%28morphology%29
  18. Mathematical morphology
    https://en.wikipedia.org/wi­ki/Mathematical_morphology
  19. Cvičení 10 – Morfologické operace
    http://midas.uamt.feec.vut­br.cz/ZVS/Exercise10/conten­t_cz.php
  20. Differences between a matrix and a tensor
    https://math.stackexchange­.com/questions/412423/dif­ferences-between-a-matrix-and-a-tensor
  21. 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?
  22. BLAS (Basic Linear Algebra Subprograms)
    http://www.netlib.org/blas/
  23. Basic Linear Algebra Subprograms (Wikipedia)
    https://en.wikipedia.org/wi­ki/Basic_Linear_Algebra_Sub­programs
  24. Comparison of deep learning software
    https://en.wikipedia.org/wi­ki/Comparison_of_deep_lear­ning_software
  25. TensorFlow
    https://www.tensorflow.org/
  26. Caffe2 (A New Lightweight, Modular, and Scalable Deep Learning Framework)
    https://caffe2.ai/
  27. PyTorch
    http://pytorch.org/
  28. Seriál o programovacím jazyku Lua
    http://www.root.cz/serialy/pro­gramovaci-jazyk-lua/
  29. LuaJIT – Just in Time překladač pro programovací jazyk Lua
    http://www.root.cz/clanky/luajit-just-in-time-prekladac-pro-programovaci-jazyk-lua/
  30. 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/
  31. 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/
  32. 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/
  33. 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/
  34. 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/
  35. 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/
  36. 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/
  37. 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/
  38. 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/
  39. 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/
  40. 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/
  41. Lua Profiler (GitHub)
    https://github.com/luafor­ge/luaprofiler
  42. Lua Profiler (LuaForge)
    http://luaforge.net/projec­ts/luaprofiler/
  43. ctrace
    http://webserver2.tecgraf.puc-rio.br/~lhf/ftp/lua/
  44. The Lua VM, on the Web
    https://kripken.github.io/lu­a.vm.js/lua.vm.js.html
  45. Lua.vm.js REPL
    https://kripken.github.io/lu­a.vm.js/repl.html
  46. lua2js
    https://www.npmjs.com/package/lua2js
  47. lua2js na GitHubu
    https://github.com/basicer/lua2js-dist
  48. Lua (programming language)
    http://en.wikipedia.org/wi­ki/Lua_(programming_langu­age)
  49. LuaJIT 2.0 SSA IRhttp://wiki.luajit.org/SSA-IR-2.0
  50. The LuaJIT Project
    http://luajit.org/index.html
  51. LuaJIT FAQ
    http://luajit.org/faq.html
  52. LuaJIT Performance Comparison
    http://luajit.org/performance.html
  53. LuaJIT 2.0 intellectual property disclosure and research opportunities
    http://article.gmane.org/gma­ne.comp.lang.lua.general/58908
  54. LuaJIT Wiki
    http://wiki.luajit.org/Home
  55. LuaJIT 2.0 Bytecode Instructions
    http://wiki.luajit.org/Bytecode-2.0
  56. Programming in Lua (first edition)
    http://www.lua.org/pil/contents.html
  57. Lua 5.2 sources
    http://www.lua.org/source/5.2/
  58. REPL
    https://en.wikipedia.org/wi­ki/Read%E2%80%93eval%E2%80%93prin­t_loop
  59. The LLVM Compiler Infrastructure
    http://llvm.org/ProjectsWithLLVM/
  60. clang: a C language family frontend for LLVM
    http://clang.llvm.org/
  61. LLVM Backend („Fastcomp“)
    http://kripken.github.io/emscripten-site/docs/building_from_source/LLVM-Backend.html#llvm-backend
  62. Lambda the Ultimate: Coroutines in Lua,
    http://lambda-the-ultimate.org/node/438
  63. Coroutines Tutorial,
    http://lua-users.org/wiki/CoroutinesTutorial
  64. Lua Coroutines Versus Python Generators,
    http://lua-users.org/wiki/LuaCorouti­nesVersusPythonGenerators
Našli jste v článku chybu?