Tehtävä 3 - Kuva

Huom! Viimeksi päivitetty 22-10-2013 16.37

Tässä ohjelmointitehtävässä lähestytään kuvan manipulaatiota kahdesta näkökulmasta. Aluksi kuvaa operoidaan paikka-avaruudessa, minkä jälkeen kuvalla operointia harjoitellaan myös taajuusavaruudessa.

Taustaa

Kuva on valon intensiteetin ja värin eri aallonpituuksien spatiaalinen (paikallinen) jakauma. Kuvasignaali on jatkuva-arvoinen funktio (analoginen kuva) tai koodattu merkkijono (digitaalinen kuva), joka taltioi kuvan intensiteetin ja värin paikan funktiona. [1] Kuvien käsittely tietokoneella vaatii digitoinnin, missä kuva muutetaan numeeriseen, tietokoneelle sopivaan, muotoon. Jokainen kuva-alkio, pikseli, kuvastaa alkuperäisen kuvan intensiteettiä ja väriä.

Paikka-avaruudessa kuvan operointi on melko yksinkertaista. Scalassa kuvien lukeminen ja luominen onnistuu helposti esimerkiksi Javan java.awt.image.BufferedImage-luokan avulla. Luokka tarjoaa metodit, joiden avulla päästään käsiksi yksittäisten pikselien väriarvoihin. Tämän luokan lisäksi tehtävässä käytetään apuna java.awt.Color-luokkaa, joka tarjoaa näppärän säiliön RGB-värille.

Fourier-muunnos

Fourier-muunnos
Fourier-muunnoksen avulla signaali voidaan esittää sini-funktioiden sarjana. Lähde: http://en.wikipedia.org/wiki/ File:Fourier_transform_time_and_frequency_domains_(small).gif

Taajuusavaruudessa kuvan käsittely aloitetaan muuttamalla kuva taajuusavaruuteen tekemällä Fourier-muunnos. Fourier-muunnoksen avulla kuva voidaan esittää sini- ja/tai cosini-muotoisten taajuuksien avulla. Fourier-muunnos ei varsinaisesti kuulu tämän kurssin sisältöön ja tarkemmin siihen päästään tutustumaan vasta myöhemmillä matematiikan kursseilla.

Fourier-muunnos (tässä 1-dimensioinen) on jatkuva funktio:

F(u) = ∫ -∞ f(x)e-2πiux dx, missä

f(x) on signaali paikka-avaruudessa
F(u) on signaali taajuusavaruudessa

Kuvaa voidaan käsitellä taajuusavaruudessa ja se voidaan muuttaa takaisin paikka-avaruuteen käänteisellä Fourier-muunnoksella:

f(x) = ∫ -∞ F(u)e+2πiuxdu

Koska tässä tehtävässä operoidaan rasterikuvilla (ja kuvan funktio ei siis ole jatkuva), on käytettävä Fourier-muunnoksen diskreettiä muotoa, jolla operoidaan yksittäisiä kuvapisteitä seuraavasti:

Xu = Σ M-1 m=0 xm ∙ e-2πium/M

ja käänteisenä:

xm = 1MΣ M-1 u=0 Xu ∙ e2πium/M

Käänteisessä versiossa eksponentin etumerkin kääntymisen lisäksi kerrotaan lopputulos vielä 1/M:llä (kuvan tapauksessa leveyden käänteisluvulla. Kerroin voidaan myös yhtä hyvin sijoittaa käänteisen DFT:n sijaan normaaliversioon ilman, että lopputulos muuttuu.

Koska me operoimme kuvilla, jotka ovat kaksiulotteisia myös kaksiulotteiseen DFT:hen tutustuminen on aiheellista. Alla vastaavat versiot DFT:stä kaksiulotteisessa tapauksessa.

X(u, v) = Σ  Σ M-1 N-1 m=0n=0 x(m, n) ∙ e-2πi(um/M + vn/N)

ja käänteisenä:

x(m, n) = 1  1——M NΣ  Σ M-1 N-1 u=0v=0 X(u, v) ∙ e2πi(um/M + vn/N)

Koska käänteislukukerrointa voi siirtää käänteisen ja normaalin version välillä kunhan kokonaisuutena kertoimen arvo on sama, tämän tehtävän ratkaisussa jaamme kertoimen tasan kummallekin versiolle. Käänteisenä ratkaisua kerrotaan korkeuden käänteisluvulla ja normaaliversiossa kuvan leveyden käänteisluvulla.

Fourier-muunnoksen lisäksi tässä vaiheessa on aiheellista mainita myös Eulerin lause, joka huomattavasti helpottaa yllä olevien laskujen ratkaisua.

eix = cos x  +  i ∙ sin x

Muuttamalla DFT Eulerin yhtälön avulla helpompaan muotoon, ei tarvitse välittää Neperin luvusta tai sen eksponenttien laskemisesta.

Suurilla kuvilla tavallinen DFT (Discrete Fourier Transform) on kuitenkin hidas verrattuna rekursiiviseen FFT:hen (Fast Fourier Transform). FFT:ssä diskreetti Fourier-muunnos jaetaan jokaisella rekursion kierroksella kahteen (parilliseen ja parittomaan) DFT:hen. Jolloin esimerkiksi n:n arvolla 1024, DFT tarvitsee 1 048 476 laskuoperaatiota tuloksen laskemiseen, kun FFT ainoastaan 10 240.

Tässä harjoitustehtävässä laadittava ratkaisu perustuu Cooley-Tukey algoritmiin, josta sinun toteutettavaksi jää joitakin osia. Algoritmi toimii esimerkiksi ainoastaan kuvilla, joiden korkeus ja leveys ovat kahden potensseja — vastuullasi on varmistaa, että muunlaisia kuvia ei yritetä käyttää. Yksinkertaistettuna algoritmi toimii seuraavasti:

    jos kuvan korkeus ja leveys ovat kahden potensseja
      täytetään tulosmatriisi lähtömatriisin arvoilla bittikäännetyssä järjestyksessä (kts. Bit-reversal)
      suoritetaan sarakkeiden FFT
      suoritetaan rivien FFT
      jos FFT on käänteinen
        kerrotaan tulos leveyden käänteisluvulla
      muussa tapauksessa
        kerrotaan tulos korkeuden käänteisluvulla 
	

Digitaalisen kuvan suotimet

Suotimella (engl. filter) tarkoitetaan laitetta tai prosessia, jonka avulla signaalista voidaan poistaa ei-toivottu komponentti tai ominaisuus. [2] Tässä tehtävässä paikka-avaruudessa operoitaessa suotimet toteutetaan matriiseina, joita operoidaan kuvaan pikseli kerrallaan. Taajuusavaruudessa operoitaessa suotimet voidaan rakentaa esimerkiksi niin, että taajuuksista suodatetaan tietyn korkuiset signaalit pois.

Paikka-avaruuden suotimet

Suotimet, jotka muodostetaan paikka-avaruutta varten voisivat näyttää esimerkiksi tältä:

	0.0 0.2 0.0
	0.2 0.2 0.2
	0.0 0.2 0.0

Matriisilla, joka voidaan Scalassa toteuttaa kaksiulotteisen taulukon avulla, kerrotaan jokaista pikseliä siten, että suotimen keskikohta tasataan pikselin kanssa. Tällöin kertomisen jälkeen pikselin uudeksi arvoksi tulee alkuperäisen arvon ja suotimen keskimmäisen alkion tulo summattuna ympäröivien pikselien tuloon vastaavien suotimen alkioiden kanssa. Jotta kuvan valoisuus ei muuttuisi, tulisi suotimen alkioiden yhteenlaskettu summa olla 1.

Alkuperäinen kuva
1) Alkuperäinen kuva.
0 0 1 0 0
0 1 1 1 0
1 1 1 1 1
0 1 1 1 0
0 0 1 0 0
2) Suodin. Kertoimena käytetty 1/13.
Tuloskuva
3) Tuloskuva suotimen jälkeen. Hiukan sumennettu (blurred) lopputulos.

Yllä olevassa esimerkissä ensimmäisessä kuvassa alkuperäinen kuva, johon käytetään keskellä olevaa suodinta ja lopputulokseksi saadaan oikealla oleva kuva. Huomaa, että tässä suotimessa alkioiden yhteenlaskettu summa on 13, jolloin lopputulos pitäisi jakaa vielä kolmellatoista tai vastaavasti muuttaa suodinta niin, että jokainen 1 muutettaisiin 1/13:ksi. Jos tuloskuvaa skaalaa riittävän suureksi, voi huomata, että reunoilla oleva muutaman pikselin levyinen kaistale on hiukan eri värinen viereisiin pikseleihin nähden. Tämä johtuu siitä, että tehtävässä suotimella operointia on helpotettu sen verran, että kuvan pikselien käsittely aloitetaan suotimen säteen päästä reunasta.

Taajuusavaruuden suotimet

Alla ensimmäisessä kuvassa on muokkaamaton kuva, jota käytämme tässä esimerkkinä. Kun ensimmäiselle kuvalle tehdään Fourier-muunnos, saadaan aikaiseksi toisen kuvan mukainen tuotos. (Todellinen tuloskuva näyttää oikeasti erilaiselta, tässä kulmat on siirretty keskelle luettavuuden parantamiseksi.) Toisessa kuvassa reunoilta keskelle siirryttäessä taajuudet ovat korkeasta matalampaan. Jolloin kun Fourier-muunnoksen jälkeen käytetään kolmannen kuvan mukaista suodinta, kaikki korkeat taajuudet, jotka ovat suotimessa mustalla alueella poistetaan kuvasta (kerrotaan nollalla) kun taas valkoisen alueen taajuudet jäävät koskematta (tai kerrotaan yhdellä). Lopputuloksena käänteisen FFT:n jälkeinen tuloskuva näkyy neljäntenä.

Alkuperäinen kuva
1) Alkuperäinen kuva.
FFT:llä operoitu kuva
2) Kuva FFT:n jälkeen.
Suodin
3) Suodin, jolla kuvaa operoidaan.
Kuva suotimen jälkeen
4) Tuloskuva suotimen jälkeen.

Uppgiftsbeskrivning

Alla klasser som behövs i uppgiften kan laddas i ett zip-paket härifrån: luokat.zip.

Deluppgift 1 - Den abstrakta klassen Filter

Först gör vi en abstrakt klass Filter, som kommer att innehålla hjälpmetoder vilka de konkreta filterklasserna som senare implementeras kommer att dra nytta av. För dessa kommande PixelFilter- och FourierFilter-klasser gör vi en metod som heter applyFilter. Den tar som parametrar en bild av typ BufferedImage samt namnet på ett filter och returnerar en ändrad BufferedImage. Här låter vi metodens implementation vara tom och låter subklasserna implementera dem skilt.

Vi borjar med att implementera två enkla metoder: getComponent och clampTo. getComponent tar emot ett Color-objekt och ett index som motsvarar den färgkomponent som användaren vill att metoden skall returnera. clampTo tar emot ett intervall och ett värde som skall klämmas in i det givna intervallet. Metoderna ser ut så här:

   /**
   * Returns the specified RGB color component specified by component in color.
   * If component is not between [0, 2] zero is returned. 
   */
   protected def getComponent(color: Color, component: Int) : Float = {
     // ... your code here ...
   }
  
   /**
    * Assures that value is at the lowest lower and at the highest upper
    */
   protected def clampTo(lower: Float, upper: Float, value: Float) : Float = {
     // ... your code here ...
   }

I getComponent-metoden definieras färgkomponenten som ett heltal mellan 0 och 2, där varje tal motsvarar ett av RGB-färgrymdens färgkomponenter. Se till att den givna komponenten hör till de tillåtna värden. I clampTo-metodens implementation lönar det sig att se på vad för slags metoder scala.Int-klassen har att bjuda på.

Deluppgift 2 - Överlagring av metoder

Överlagring av metoder betyder att man har flera metoder med samma namn, men med olika signaturer, dvs. kombinationen av typ av returvärde och typerna och mängden av parametrarna är unika.

Uppgiften är att implementera metoderna imageToPixels och pixelsToImage i Filter-klassen med två olika signaturer var. Ena typen av signaturerna kommer att användas i PixelFilter-klassen, som hanterar pixlar med hjälp av Color-klassen, och andra typen i FourierFilter-klassen, som representerar pixlar som flyttalstabeller. Om vi i ett senare skede skulle bestämma oss att byta till att använda flyttalstabeller i PixelFilter-klassen, så innebär denna struktur att man endast behöver modifiera PixelFilter-klassen.

Metoderna används för att konvertera mellan två bildformat: Flyttalstabeller, som är lätta att hantera algoritmiskt och BufferedImage-objekt, som är lätta att spara och rita på rutan.

Implementera alltså följande metoder:


   /**
    * Fills the target array with color information in image.
    */
   protected def imageToPixels(image: BufferedImage, target: Array[Array[Color]]) : Unit = { 
     // ... your code here ...
   }
   
   /**
    * Fills the target array with color information in image.
    */
   protected def imageToPixels(image: BufferedImage, target: Array[Array[Array[Float]]]) : Unit = { 
     // ... your code here ...
   }
   
   /**
    * Writes the contents of the pixels array to the BufferedImage in image returning the result.
    */
   protected def pixelsToImage(image: BufferedImage, pixels: Array[Array[Color]]) : BufferedImage = {
     // ... your code here ...
   }
   
   /**
    * Writes the contents of the pixels array to the BufferedImage in image returning the result.
    */
    protected def pixelsToImage(image: BufferedImage, pixels: Array[Array[Array[Float]]]) : BufferedImage = {
     // ... your code here ...
   }

I båda metoderna måste du göra en slingstruktur som itererar igenom varje rad och kolumn. Det lönar sig att använda getComponent-metoden då du implementerar imageToPixels-metoden som FourierFilter-klassen kommer att använda, och likaså clampTo-metoden vid implementationen av pixelsToImage-metoden. Varför finns ordet protected framför metodernas definitioner? Läs mera om omfattningsbestämningar hos http://www.tutorialspoint.com/scala/scala_access_modifiers.htm

Deluppgift 3 - Fungerar det korrekt?

Vi rekommenderar att klasserna testas ordentligt varje gång någon större helhet blir klar, så att man med möjligast stor säkerhet kan säga att lösningen fungerar korrekt. Det finns många olika sätt att testa — allt från testdriven utveckling (se http://en.wikipedia.org/wiki/Test-driven_development), där testerna skrivs före själva implementationen, till att lägga fokus på preventiva åtgärder för att minska fel (se http://en.wikipedia.org/wiki/Cleanroom_software_engineering). Vi uppmuntrar er att hitta ett sätt som passar er bäst (inte nödvändigtvis den sistnämnda metoden) och att testa era lösningar ordentligt före ni returnerar dem.

I det här skedet ser vi till att den nyss skapade Filter-klassen fungerar någorlunda rätt. Gör en FilterTest-klass, som ärver Filter-klassen, i vilken du skriver dina test. Ladda också följande TestView-klass. Det räcker, att du endast implementerar superklassens applyFilter-metod.

Det lönar sig att testa lösningarna med både Color-objekt och Array[Float]. Du kommer att behöva både två-, och i det senare fallet, tredimensionella tabeller i dina tester. Exempel på användningen av dessa tabeller kan hittas här: http://www.tutorialspoint.com/scala/scala_arrays.htm. Du kan skapa en två- eller flerdimensionell tabell med ofDim-metoden, som vill veta både typen av element som den skall spara och tabellens storlek. T.ex. en 3x3x3-tabell som innehåller heltal kan skapas så här:

   var cube = ofDim[Int](3, 3, 3)
Du kan få reda på tabellens storlek genom att gräva fram bildens dimensioner via BufferedImage-klassens metoder.

Nu kan du testa att Filter-klassens metoder fungerar korrekt. Se till att du itererar igenom varenda pixel i bilden, och att returneringen av en komponent och klämmandet fungerar rätt.

Deluppgift 4 - PixelFilter-klassen

Nästa uppgift är att implementera den första egentliga filtrerande klassen, som ärver den tidigare skapade Filter-klassen. PixelFilter-klassen, som vi skall implementera, är menad för att modifiera bilder i rymdplanet. Klassens viktigaste och enda utåt synliga metod är implementationen av applyFilter, som redan nämndes i sammanhang med Filter-klassen.

PixelFilter-klassen är gjord så att man kan från något av klassens objekt anropa applyFilter-metoden med en bild, som man vill modifiera, och namnet på ett filter (en sträng), som man vill använda på bilden. operateFilter-metoden tar hand om filtrets konstruktion och applyFilter använder filtret på bilden.

Metoderna applyFilter och operateFilter

Implementera applyFilter-metoden i din PixelFilter-klass. Först måste en struktur skapas för att spara information om bilden på samma sätt som i testklassen. Här använder vi en tvådimensionell tabell som innehåller Color-objekt. Efter det behöver man endast konvertera bilden till ett lättare format genom att fylla tabellen, anropa operateFilter-metoden och sedan konvertera tabellen tillbaka till det givna formatet.

operateFilter-metoden tar som parametrar åtminstone tabellen, som innehåller bildens data och namnet på ett filter. Metoden behöver inte returnera någonting.

operateFilter-metoden associerar en filtermatris med det givna filternamnet. Det här fungerar lättast med Scalas match-konstruktion. Efter att det rätta filtret have blivit konstruerat, itererar vi igenom bildens pixlar och operarar filtret på varje pixel.

match-strukturen är bara en liten del av Scalas mönsterigenkänningsegenskaper (eng. pattern matching). För oss räcker det med att känna igen strängar och att ändra på programmets exekvering på basen av det. I grunden är match-strukturerna som används i det här exemplet av formen:

filterName match {
  case "filter1" => // do something
  case "filter2" => // do something else
}

I vardera fall kan åtgärderna som skall göras vara flera än ett och de behöver inte placeras på samma rad som case-ordet. Du kan bekanta dig närmare med olika pattern matching egenskaper här: http://www.tutorialspoint.com/scala/scala_pattern_matching.htm

Namnen på de olika filtren finns definierade i en textfil som heter pixelfilter.txt. En klass som heter View tar hand om att läsa denna fil. Den kan du ladda ner härifrån: View.scala. Du kan skapa din pixelfilter.txt-fil själv och placera den på ett passligt ställe. I filen placeras varje filters namn på sin egen rad — det här namnet kommer att synas på en knapp som modifierar bilden och skickas till PixelFilter-klassen för jämförelse. Kom ihåg att vid behov ändra på sökvägarna i View-klassen. Du kan i det här skedet också skapa fourierfilter.txt-filen så att du inte behöver modifiera View-klassen på grund av det. En aning simplifierat ser programmets struktur ut som i bilden nedan:

Luokkien toiminta.

Som redan tidigare nämndes, kommer operateFilter-metodens implementation att göras med hjälp av match-konstruktioner. Den ända absolut nödvändiga case-satsen är att jämföra med strängen original. Det lönar sig med att börja med det här fallet och fortsätta med andra filtrar först efter att den här fungerar korrekt. Tillsätt original till pixelfilter.txt-filen så att en knapp associeras med det här filtret. Filtret som bör skapas är en 1x1 tabell som har värdet 1 (ett) i sin enda cell. Det är lämpligast att använda en tvådimensionell tabell som innehåller flyttal.

Efter att filtret har blivit konstruerat, går operateFilter-metoden igenom varje element i bildtabellen och multiplicerar varje färgkomponent med filtret. För att kunna konvertera tabellen till ett Color-objekt måste du klämma komponenternas värden in i rätt intervall.

R, G ja B.

Skapandet av filter

Först kan vi skapa filter som ändrar på bildens ljushet, "darken" och "lighten". De består, liksom "original", endast av en 1x1 tabell.

På bilden till höger ser du hur de gröna, röda och blåa färgerna ändras med olika värden på intensiteten. För att bilden skall bli ljusare, måste värdet i tabellen vara större än 1, och på samma sätt måste värdet vara mindre än 1 för att bilden skall bli mörkare. Du kan själv söka efter en passlig balans med att pröva på olika värden.

Nu flyttar vi oss till lite besvärligare och komplicerade filter. Att kunna ändra bildens ljushet är användbart, men att kunna ändra bildens skarphet kan i vissa fall vara ytterst värdefullt.

Vi börjar med det enklare suddande (eng. blur) filtret. Att sudda en bild blir av med små detaljer och jämnar ut bilden. I sin enklaste form blir en pixels nya värde medeltalet av dess närliggande pixlars värden. Bildens gradienter jämnas ut och skarpheten minskar. Ett suddande filter tar bort brus men samtidigt minskar också skarpheten mellan olika färgområden.

Gör ditt suddande filter så att du själv tycker att det blir och ser bra ut. Kom ihåg principerna som introducerades i början: Filtren har en udda bredd (3, 5, 7, osv.), de är symmetriska (höjd == bredd) och summan av elementen är 1 (ett). Fundera hurudan matrisen måste vara för att multiplikationen skall producera medeltalet av omgivningen.

Man kan tänka sig att ett motsatt filter till suddande är ett skärpande filter, där bildens små detaljer är betonade. Operationen som motsvarar att ta medeltalet av något är integrering, från vilket vi kan gissa att skärpning kan åstadkommas med hjälp av derivering. I den här uppgiften grundar sig det skärpande filtret på andra gradens derivata, som definieras med Laplaces ekvation. Representerad i matrisform räknar filtret omgivande pixlars medelvärde, som i det suddande filtret, men pixelns ursprungliga värde höjs. Filtret som används är dessutom inverterat och i sin grundform ser den ut så här:

   -1 -1 -1
   -1  8 -1
   -1 -1 -1
Märk, att summan av elementen är nu 0 (noll). Du kan söka ett passligt skärpande filter genom att ändra på mittersta elementets värde samtidigt som du ser till att summan av alla element är 1..

Alla ovan nämnda filter har fungerat i RGB-färgrymden, men denna blir ett problem om vi försöker konstruera ett filter som ändrar en färgbild till en svartvit bild. HSB-rymden (Hue, Saturation, Lightness) som introducerades i ett PBL-fall konstruerar färger på ett annat sätt, vilket underlättar implementeringen av ett svartvitt filter. Gör nu ett eget filter som ändrar en färgbild till en svartvit bild.

Följande krävs inte för fulla poäng, men om du vill du kan ännu pröva på att implementera filter som suddar åt ett visst håll (motion blur), skärper kanter (edges), eller tar bort en viss färgkomponent.

Du borde nu ha åtminstone 5 filter som manipulerar bilden och en som returnerar bilden till sitt ursprungliga tillstånd. I det här skedet lönar det sig att fundera på lösningen och om den är fiffig i sin konstruktion. Om du har skrivit all funktionalitet i en operateFilter-metod, så kan det vara klokt att försöka separera sådan funktionalitet, som bara gör en sak och som ofta används, i sina egna metoder.

Deluppgift 5 - FourierFilter-klassen

FourierFilter är, liksom PixelFilter, en subklass av Filter. Klassens struktur motsvarar också sin rymdplansbundna broders struktur — den enda offentliga metoden den har är applyFilter, vars operation också är liknande. På grund av att FourierFilter opererar på bilder i frekvensplanet måste man använda två tabeller, istället för en, för att spara bildens data. Fouriertransformen orsakar det att man måste spara reella värden i en tabell och imaginära värden i en annan. Vi behöver dessutom ett par till tabeller för resultaten.

applyFilter-metoden fungerar på det sättet, att (1) först skapas tabellerna och (2) den ursprungliga bildens data kopieras till den reella tabellen. Sedan (3) flyttas bilden till frekvensplanet genom att räkna ut en tvådimensionell FFT. (4) Tabellernas indexering ändras sedan så att hörnen flyttas till mitten. Till sist (5) använder man ett filter på tabellerna. Efter att filtret har blivit använt måste man ännu (6) flytta hörnen tillbaka, (7) räkna ut en invers FFT för att komma tillbaka till rymdplanet och (8) konvertera tabellerna till ett bildformat som man sedan returnerar. Då man implementerar metoden måste man vara noggrann med vilken tabell man använder och när.

För att implementeringen av en Fouriertransform inte hör till kursens mål, räcker det med att du endast implementerar en del av dess hjälpmetoder. Du kan ladda ner FourierFilter-klassen härifrån: FourierFilter.scala.

Din uppgift är att implementera följande metoder (helst också i samma ordning):

Före du implementerar metoden operateFilter lönar det sig att testa resten av implementationen med att göra att filter "none", som visar bilden efter en enskild Fouriertransform, dvs. som i bild 2 i början av uppgiftsbeskrivningen. Det lättaste sättet att göra det är att hoppa över faserna 5-7 i metoden applyFilter med en simpel villkorssats. Pröva med olika stora bilder. Det lönar sig att fortsätta först sen när allt som har dittills blivit implementerat fungerar korrekt.

Deluppgift 6 - operateFilter-metoden i frekvensplanet

Största fördelen med att operera i frekvensplanet är att sådana operationer som är besvärliga i rymdplanet ofta blir mycket lättare. Du kommer fort att märka, att de filter som du implementerar kommer att vara mycket simplare än de du tidigare har implementerat.

Såsom i den andra filterklassens operateFilter-metod, så grundar sig operationen runt match-strukturen. I det här fallet lönar det sig dock inte att spara filter i tabeller — du märker snart varför.

Efter Fourier-transformen kommer den lägsta frekvensen att finnas i mitten (i origo). Vårt första filter, skårfiltret "notch", ändrar enbart den lägsta frekvensen till noll. Kom ihåg att göra ändringen i både den reella tabellen och den imaginära tabellen. Du kommer att märka, att även en ändring i en enda punkt ändrar hela bildens ljushet.

Fourier-muutettu testikuva.
Fourier-muunnettu testikuva.
Keinotekoinen testikuva.
Keinotekoinen testikuva.

Sedan skall vi undersöka ett specialfall av skårfiltret. Du kan pröva dessa filter på bilden till höger, eller på vilken som helst annan bild som innehåller regelbundna horisontella eller vertikala mönster. Märk, att när man räknar Fourier-transformen för sådana bilder och ser på bilden i frekvensplanet, kan man se liknande regelbundna mönster.

Pröva nu göra speciella skårfilter med namnen "horizontal" och "vertical", som nollar alla frekvenser på en horisontell eller vertical linje som går igenom origo, men ändra inte värdet i origo. Vad händer åt de konstruerade testbilderna i vardera fall? Förvånar det dig att resultaten ser ut så som de gör?

Jätte kiva filter, men har man någon nytta av dem? Inbilla dig en situation, där en scanner har orsakat ett regelbundet randmönster längs hela bildens längd. Sådana fel kan man lätt fixa med den här typen av filter. Försök t.ex. att fixa bilden med en solros som har tjocka diagonala ränder. Du kommer antagligen inte att kunna fixa ränderna helt och hållet, för de är antagligen för tjocka, men du kommer att få bilden att se bättre ut och i fortsättningen kommer du att kunna fixa problemet.

Keinotekoisesti vaurioitettu testikuva.
Keinotekoisesti vaurioitettu testikuva.

Följande filter som vi implementerar är lågpass- (low pass) och högpassfilter (high pass), varav den första tar bort störningar i bilden genom att klippa bort höga frekvenser, medan den andra klipper bort låga frekvenser. Du såg redan ett exempel av ett lågpassfilter i början av uppgiftbeskrivningen. Det är senast nu som det lönar sig att erinra sig om hur cirkelns ekvation ser ut, för du kommer att använda en cirkel för att begränsa ett område i filtren. Pröva bägge filter med olika värden.

Du kommer att märka, att då radien av lågpass-filtret ökar, blir bilden också skarpare, samtidigt som filtrets effekt också blir mindre. Det kommer också att bildas ringar på de områden av bilden som är jämna. Du kan försöka bli av med den effekten genom att göra filtrets kanter mjukare. Du kan åstadkomma detta genom att t.ex. variera filtrets multiplikativa faktor mellan 0 och 1 som en funktion av avståndet till origo.

Då högpassfiltrets radie ökar, kommer kanterna att bli tunnare och tunnare. Också högpassfiltret kommer att producera ringar då ett visst gränsvärde överskrids. Detta kan fixas på samma sätt som med lågpassfiltret.

Till slut kan du ännu implementera ett filter, som kombinerar två tidigare filter — ett ringfilter, som istället för att släppa igenom ett cirkelformat område, släpper igenom endast ett ringformat område. Alla frekvenser från bildens kanter till den yttre cirkelns kant sätts till noll, och på samma vis sätts alla frekvenser från origo till den inre cirkelns kant till noll. Pröva på ringar med olika tjocklek och radie för att nå det bästa resultatet.

Du borde nu ha åtminstone de följande filtren klara: "notch", "vertical", "horizontal", "low", "high" och "ring".

Deluppgift 7 - Till slut

Testa ännu till slut alla klasser som du har skapat och se till att de fungerar rätt. Du skall också senast i det här skedet kommentera sådana delar av din kod som kan vara svårförståeliga. Om något filter fungerar speciellt bra med någon specifik bild, lönar det sig att nämna om saken i kommentarerna. Packa din lösning så att assistenten som bedömer den inte behöver ladda något extra för att kunna köra programmet. Inkludera alltså åtminstone följande filer i returpaketet: Filter.scala, FourierFilter.scala, PixelFilter.scala, View.scala, fourierfilter.txt, pixelfilter.txt och bildfilerna som du har testat ditt program med. Namnge paketet enligt följande modell: studentnr_kierros3.zip.

Efter att du lämnat in din lösning skall du ännu gå och ge feedback här: http://www.cs.hut.fi/cgi-bin/teekysely.pl?action=showform&id=studio1-scala3-2013&lang=FIN

Palautus

Tehtävän palautus tehdään aikarajaan lauantaihin 26.10. klo 18.00 mennessä Rubyric-järjestelmään. Myöhästyneestä palautuksesta seuraa 2 pisteen vähennys alkavaa vuorokautta kohden – eli esimerkiksi sunnuntaina klo 18.01 palautetun työn pisteistä vähennetään 4 pistettä.

Arvostelu

Ohjelmointitehtävät arvostellaan erillisten kriteerien mukaisesti. Tehtävän maksimipistemäärä on 60 pistettä ja hyväksytty suoritus on vähintään 30 pistettä.

Jos tehtäväpalautus on selvästi hyvin keskeneräinen ja se saa alle vaaditun hyväksytyn pistemäärän, se voidaan palauttaa tekijälle korjattavaksi (ns. bumerangi). Tehtävä on hyväksytty vasta, kun se on palautettu riittävän laajuisena ja syvällisenä. Bumerangina palautetusta tehtävästä voidaan antaa korjattunakin enintään minimipistemäärä 30.

Kannattaa kiinnittää huomiota koodin ja sisennyksen selkeyteen; jos assistentti näkee koodista selkeästi ajatuksenkulkusi, helpottaa se koodin toimivuuden ja vikatilanteiden vakavuuden arviointia. Kannattaa myös varmistaa, että palautettava koodi kääntyy ja että kyseessä on viimeisin versio harjoituksestasi.

Lisäksi ohjelmointitehtävissä on mahdollista saada lisäpisteitä tehtävänannon ylittävästä suorituksesta

Lähteet

  1. [1]  Saarelma, Hannu. Kuvatekniikan perusteet. Otatieto. Oy Yliopistokustannus University Press Finland Ltd., Helsinki, 2003.
  2. [2]  Wikipedia. Filter (signal processing), 6.7.2013. Saatavilla: http://en.wikipedia.org/wiki/Filter_(signal_processing), viitattu 24.7.2013