Kierros Kuva, Deadline Ti 28.11. klo 23:59 Tässä tehtävässä tutustutaan kuvien muokkaamiseen. Tulet toteuttamaan kuvankäsittelyalgoritmeja ohjelmaan jolla voit muokata kuvia. Ohjelmointi 1-kurssilta apua on tarjolla erityisesti luvuista: Luku 5.5: Silmukoita, vektoreita ja vaalit Luku 5.6: Taulukoita ja sisäkkäisyyttä Luku 7.3: Funktiot parametreina VALMISTELU Lataa valmis projektipaketti MyCourses-sivustolta ja tutustu sen dokumentaatioon. Toteutettavat luokat ja metodit löytyvät pääosin paketista s1.image.filters. COLOR-LUOKKA JA SEN KUMPPANIOLIO Color-apuluokan tarkoitus on helpottaa värien luomista ja muokkausta kuvaa käsiteltäessä. Käytännössä kuvat, joita käsittelemme ovat vain suuria taulukoita, joissa jokainen kuvapikseli on yksi Color-olio. Luokassa on konstruktori joka ottaa värin kolmena värikomponenttina sekä alpha-kanavana, joka kuvaa läpinäkyvyyttä. Kukin komponentti on arvoltaan 0 ja 255 välillä, missä 255 on kirkkain mahdollinen arvo. Emme vielä tällä kierroksella tarvitse läpinäkyvyyttä, mutta palaamme siihen videokierroksella. Tällä kierroksella voit asettaa tämän parametrin arvoksi aina 255, jolloin kuva ei näy lainkaan läpi. def this(red: Int, green: Int, blue: Int, alpha: Int) TEHTÄVÄ 1 COLOR-LUOKAN CLAMP-METODI Colorin kumppaniolion metodi clamp rajoittaa sille parametrina välitetyn arvon välille [0, 255] ja palauttaa tuloksen. Jos metodille välitetään esimerkiksi arvo 78, palauttaa metodi 78. Mikäli metodille välitetty arvo on alle 0, palautetaan 0 ja mikäli metodille välitetty arvo on enemmän kuin 255, palautetaan 255. Tätä metodia ei käytetä Color-olioita luotaessa automaattisesti. Color-luokasta löytyy kuitenkin samalla tavoin nimetty metodi clamp, joka kutsuttaessa palauttaa uuden Color-olion, jonka värit on rajattu välille 0-255. Toteuta tämä metodi itse. Koska Color:in clamp-metodi ei rajoita arvoa kuin pyydettäessä, voit käyttää Color-luokan *, - ja + metodeja värien yhdistelyyn omissa suotimissasi ja vasta lopuksi rajoittaa värikomponentit tietylle alueelle. Käytännössä tämä mahdollistaa joissain algoritmeissa sen, että voit käsitellä kaikkia värikomponentteja kerralla. VÄRIEN KÄSITTELYSTÄ (KIINNOSTUNEILLE) Jos tutkit Color luokkaa tarkemmin, voit löytää arvon argb sekä sen vastinparin, kumppaniolion applymetodista. Nämä metodit käsittelevät kokonaislukuja bittitasolla. Voit tutustua erilaisiin bittioperaatioihin esimerkiksi täältä http://www.tutorialspoint.com/scala/scala_bitwise_operators.htm. Käytännössä niissä muutetaan väri muotoon, jossa yksittäinen väriarvo voidaan tallentaa yhtenä 32-bittisenä arvona. Argb-muuttujassa luodaan värikomponenteista 32-bittinen esitys käyttäen muuttujien a, r, g ja b kahdeksaa (8) ensimmäistä bittiä. Saat scalassa (ja monessa muussakin kielessä) siirrettyä bittejä sopivaan sijaintiin << - operaattorilla ja yhdistettyjä näitä lukuja -operaattorilla.
FILTER-LUOKAN YKSINKERTAISET SUOTIMET Tässä vaiheessa tulet: Toteuttamaan suotimen, jolla voidaan muokata kuva harmaasävyiseksi. Toteuttamaan suotimen, jolla kuvan värit voidaan kääntää vastaväreiksi. Toteuttamaan suotimen, jolla voidaan muokata kuvan valoisuutta. Toteuttamaan kolme suodinta, joilla kullakin voidaan säätää yhtä kuvan värikomponenttia. 3.1. HARMAASÄVY JA VASTAVÄRIT Tutustu aluksi s1.image.image -luokkaan ja sen dokumentaatioon. Image-luokan pikselit on tätä tehtävää varten laitettu tarjolle kaksiulotteiseen Color-alkioita sisältävään taulukkoon, johon on viittaus Image-luokan ilmentymämuuttujassa data. Tietokoneen muistissa kuvapisteet ovat tyypillisesti yhdessä yhtenäisessä muistialueessa tavallaan yksiulotteisessa taulukossa. Tässä tehtävässä on tarkoitus kasata omia suotimia, jotka ottavat parametrinaan kuvan ja tuottavat tuloksena uuden, muunnellun version. Voit itse luoda uusia Image-olioita luomalla kaksiulotteisen Color-taulukon ja antamalla taulukon Image:lle parametriksi. Luokkatiedostossa SimpleFilters on annettu esimerkkinä yksi suodin, Uglify, joka vaihtaa värikomponenttien paikkaa saaden aikaan psykedeelisen kuvan. Tutustu tämän yksittäisolion koodiin ennen muiden suotimien aloittamista. Voit myös kokeilla sitä käyttöliittymässä. object Uglify extends BasicFilter { val name = "Uglify" def filter(image: Image) = { val original = image.data val filtered = Array.ofDim[Color](image.height, image.width) for { y <- 0 until image.height x <- 0 until image.width } { val pixel = original(y)(x) val changed = Color(pixel.g, pixel.b, pixel.r) filtered(y)(x) = changed } } } new Image(filtered) Muuttujassa name on suotimen nimi, joka myös tulee näkyviin ohjelman käyttöliittymässä olevaan nappiin. Metodi filter taas ottaa parametrinaan kuvan ja tuottaa sen perustella uuden kuvan. Filter-metodissa noudetaan ensin alkuperäisen kuvan pikselitaulukko sekä luodaan uusi taulukko tuotettavan kuvan pikseleille. Tämän jälkeen käymme for-silmukalla läpi kaikki kuvan pikselit ja luodaan vasta luomaamme taulukkoon uudet värioliot jokaiseen taulukon kohtaan. Lopuksi luodaan uusi kuva. Esimerkissä on käytetty for-silmukan muotoa, jossa voidaan kirjoittaa useampi sisäkkäinen silmukka samojen aaltosulkujen sisään. Se toimii taysin samalla tavalla kuin kaksi sisäkkäistä for-silmukkaa mutta on helpompi lukea (etenkin jos sisäkkäisiä silmukoita on paljon). Huomaa, että uuden taulukon olisi voinut tuottaa myös map-funktioilla.
TEHTÄVÄ 2 - INVERT Ensimmäinen oma suotimesi on samasta tiedostosta löytyvä Invert. Kuten Uglify:ssä, tässäkin riittää totuettaa metodi filter. def filter(image: Image) : Image joka kääntää kuvan värin negatiiviseksi. Jokaiselle kuvapisteelle lasketaan uusi arvo, jossa värikomponenttien (RGB) uusi arvo määräytyy erotuslaskulla 255 - "värikomponentin arvo" ja läpinäkyvyyskomponentti Alpha pysyy ennallaan. TEHTÄVÄ 3 - GRAYSCALE Seuraava suodin on yksittäisoliossa GrayScale. Sen filter-metodi muuttaa kuvan harmaasävyiseksi. Jokaiselle kuvapisteelle lasketaan uusi arvo, jossa värikomponenttien (RGB) uudeksi arvoksi tulee kaikkien värikomponenttien (RGB) keskiarvo ja läpinäkyvyyskomponentti Alpha pysyy ennallaan. Bonus: Ihmissilmä havainnoi eri värejä eri tavoin, minkä vuoksi voit päästä parempaan lopputulokseen painottamalla komponentteja eri arvoilla. Voit halutessasi tutustua artikkeliin Wikipediassa ja valita osaväreille paremmat kertoimet: https://en.wikipedia.org/wiki/grayscale Vinkki: Voit haluttaessa lisätä tämän ominaisuuden Color-luokkaan TEHTÄVÄ 4: LIGHTNESS Tässä tehtävässä säädetään kuvan kirkkautta. Nyt suodin löytyy tiedostosta parametricfilters.java. Tässä tiedostossa olevat suotimet saavat yhden säädettävän parametrin joka löytyy suotimen ilmentymämuuttujasta value. Kukin suodin voi itse valita välin, jolla muuttujan value arvo voi vaihdella, antamalla yliluokan AdjustableFilter parametreille minimum ja maximum haluamansa arvot. Arvo base asettaa suotimen alkuperäisen arvon. Lightness-suotimessa value säätää kuvan kirkkautta. Kun kuvapisteitä kerrotaan ykköstä (1) pienemmillä arvoilla, kuva tummenee ja vastaavasti yhtä (1) suuremmat arvot vaalentavat kuvaa. Tulkitse valuen arvoa niin, että sen sisältämä arvo on prosentuaalinen muutos kuvan kirkkauteen. Arvo 100 kaksinkertaistaa kuvan kirkkauden, kun taas -100 laskee kirkkauden nollaan. TEHTÄVÄ 5: VÄRIKOMPONENTTIEN MUOKKAUS Seuraavaksi toteutetaan kolme suodinta, joiden toiminta on periaatteiltaan keskenään hyvin samankaltainen. Luomme metodit, jotka muokkaavat kuvan väriä komponentti kerrallaan. Toteutettavat suotimet ovat Redness, Greenness ja Blueness. Kukin suodin muuttaa nimensä mukaisesti vain yhtä värikomponenttia muuttujan value mukaan kun muut komponentit pysyvät samoina. Value:n arvo vaihtelee välillä [- 100, 100]. Tämä arvo voidaan suoraan lisätä komponentin arvoon.
FILTER-LUOKAN MONIMUTKAISEMMAT SUOTIMET Seuraavassa vaiheessa tulet: Toteuttamaan metodin, jonka avulla voidaan sumentaa kuvaa. Toteuttamaan metodin, jonka avulla voidaan tarkentaa kuvaa. Toteuttamaan näiden metodien hyödyntämät apumetodit (ja suotimet). Seuraavaksi toteutettavat suotimet eroavat edellisen osatehtävän suotimista siinä, että pikselien uusi arvo ei muodostu pelkästään alkuperäisen arvon ja jonkin operaation pohjalta, vaan uuteen arvoon vaikuttaa pikselin naapurusto. Sekä blur että sharpen suotimet tuottavat kunkin pikselin arvon perustuen alkuperäisiin pikseleihin, mutta kertoimena käytetään yksittäisen arvon sijaan taulukkoa. Suodintaulukot ovat kaksiulotteisia taulukoita, joilla pituus ja leveys ovat yhtä suuret ja parittomat (esim. 3x3 tai 7x7). Taulukon alkiot ovat kokonaislukuja. Blur-suotimen sumentava toiminnallisuus saadaan keskiarvoistamalla pikselien arvoja. Jokaisen pikselin arvo muodostuu siis sen ympäristön keskiarvosta. Sharpen-suotimen tarkentava toiminnallisuus taas korostaa kunkin pikselin alkuperäistä väriä suhteessa naapurustoonsa. Kummallekin metodille parametrina välitetään kokonaisluku value, joka kertoo, kuinka suuri alue pikselin ympäriltä tulee ottaa huomioon. Parittomilla luvuilla suodintaulukossa jokainen alkio on nollasta poikkeava (neliskulmainen suodin) ja parillisille luvuille suodintaulukossa taulukon kulmat jätetään nollaksi, jolloin saadaan kulmallaan seisovan neliön muotoisia suotimia. Alla esimerkit. Arvolla 1 suodin ei tee mitään. Amount = 2 0 1 0 1 1 1 0 1 0 amount = 4 0 0 1 0 0 0 1 1 1 0 0 1 1 1 0 0 0 1 0 0 Amount = 3 1 1 1 1 1 1 1 1 1 amount = 5 APUMETODI CREATEFILTER Äsken esitettyjen suotimien aikaansaamiseksi toteuta yksittäisolioon s1.image.filters.tools metodi: def createfilter(size: Int, value: int) :Array[Array[Int]] Ensimmäinen parametri size määrittää suotimen muodon ja koon, ja toinen parametri value on nollasta eroavien alkioiden sisältämä arvo (käytännössä tämä on lähes aina 1). Metodi palauttaa ylläolevan kaltaisen kaksiulotteisen taulukon, jota käytetään suotimena.
APUMETODI MULTIPLYWITHFILTER Toteuta samaan olioon metodi: def multiplywithfilter( x: Int, y: Int, image: Image, filter: Array[Array[Int]], divisor: Int): Color joka kertoo kuvan image yksittäisen pikselin (x, y) parametrina välitetyllä suotimella (filter) ja lopuksi jakaa tuloksen parametrin divisor arvolla, jotta kuva ei muutu kirkkaammaksi. Käytännössä tämä jakaja on suotimessa olevien lukujen summa. Metodi palauttaa koordinaateissa (x, y) olevan pikselin uuden arvon. Parametrina annettu pikseli kerrotaan suotimen keskikohdalla. Käy läpi parametrina annetun pikselin (x, y) ympäristö kertoen arvot suotimessa olevilla kertoimilla ja laske laske uusi arvo näiden summasta. Kun suodin on kooltaan 3*3, osuu pikseli (x, y) suotimen kohtaan (1, 1), tällöin pikselin (x, y) uusi arvo muodostuu kun kerrotaan pikseli kohdassa (x-1, y-1) suotimen kohdalla (0,0), pikseli kohdassa (x-1, y) suotimen kohdalla (0, 1), pikseli kohdassa (x-1, y+1) suotimen kohdalla (0, 2) jne. ja lasketaan näiden arvojen summa. Voit käsitellä kunkin värikomponentin erikseen jättäen läpinäkyvyyskomponentin Alpha koskemattomaksi. Huomaa että jotkin luokan Color-metodit voivat tehdä työstä paljon helpompaa koska kaikki komponentit voi myös hoitaa kerralla. Entäs reunat? Kuvan reunoilla suodin ei enää mahdu kokonaisuudessaan kuvan alueelle (esimerkiksi pikseli (0,0) tulisi kertoa vain yhdellä kulmalla). Onneksi tähänkin on ratkaisu APUMETODI PAD No, mitä sitten teemme näille reunapikseleille? Käytännössä mahdollisia tapoja käsitellä ne on useita: Voidaan asettaa ne nolliksi Voidaan peilata reunapikselit Voidaan monistaa reunapikseliä Viereisessä kuvassa on esimerkki viimeisestä yllämainituista vaihtoehdoista. Esimerkissämme keskimmäinen 3x3 neliö on alkuperäinen kuva ja sen ympärille on monistettu jokaista pikseliä reunasta ulospäin niin että jokaisella pisteellä on arvo. Pikselin A-arvo leviää täyttämään lopulta koko kulman. A A A B C C C A A A B C C C A A A B C C C D D D E F F f G G G H J J j G G G H J J J G G G H J J J Kun tämä operaatio suoritetaan suodatettavalle kuvalle ennen suodatusta, ei kuvan koko muutu ja reunapikselit voidaan käsitellä samalla tavoin kuin muutkin. If-lauseita ei tarvita tässä vaiheessa enää tarvita. Toteuta siis seuraava apumetodi: def pad(data: Array[Array[Color]], border: Int): Array[Array[Color]] Ensimmäinen parametri on taulukko jonka ympärille lisätään väripikseleitä, toinen parametri kertoo kuinka monta pikseliä lisätään. Riittää että metodi tukee arvoja 1-3, joskin se ei muutu yhtään monimutkaisemmaksi tämänkään jälkeen.
SUOTIMET BLUR JA SHARPEN Äsken toteutetut apumetodit yksinkertaistavat merkittävästi blur- ja sharpen suodinten toteutusta. Kummassakin luodaan suodin getfilter metodin avulla, laajennetaan pad-metodilla kuvaa tarvittava määrä ja konvoloidaan laajennetun kuvan pikselit multiplywithfilter metodin avulla. Lopuksi vain luodaan taulukosta uusi kuva. TEHTÄVÄ 6: BLUR Sumentaminen on käytännössä keskiarvottamista, eli jokaisella naapuruston pikselillä on samanlainen painoarvo. Jotta kuvan valoisuus ei muuttuisi, on pidettävä huolta siitä, että suodintaulukon alkioiden summa ei ylitä yhtä. TEHTÄVÄ 7: SHARPEN Samoin kuin sumentavissa suotimissa myös tarkentavien suodintaulukoiden alkioiden summan tulisi olla yksi. Koska tarkentava suodin korostaa pikselin alkuperäistä arvoa, voidaan laatia getfilter metodilla tarvittava suodin, käyttäen parametrin seed arvona lukua -1. Tämän jälkeen tulee suotimen keskimmäinen arvo korvata sellaisella positiivisella kokonaisluvulla, että suotimen alkioiden summaksi tulee 1. Alla esimerkki muutamasta suotimesta. amount = 3-1 -1-1 -1 9-1 -1-1 -1 amount = 4 0 0-1 0 0 0-1 -1-1 0-1 -1 13-1 -1 0-1 -1-1 0 0 0-1 0 0
TEHTÄVÄ 8 SEAM CARVING Tässä viimeisessä tehtävässä toteutetaan luennolla nähty seam carving algoritmi. Pohja suotimelle löytyy luokasta RemoveVerticalSeam. Algoritmissa on kolme vaihetta: 1. Pikselien energioiden laskeminen 2. Saumojen energioiden laskeminen 3. Pienimmän energian sauman valinta ja poistaminen Jos et ollut luennolla, voit tutustua algoritmiin katsomalla tämän videon: https://www.youtube.com/watch?v=6ncijxtlugc ja lukemalla tämän Wikipedia-sivun: https://en.wikipedia.org/wiki/seam_carving Kysymys on algoritmista, jolla kuvasta voidaan poistaa mahdollisimman huomaamattomasti pisteitä, jolloin kuvaa voidaan kaventaa. Algoritmia voidaan käyttää myös kuvien venyttämiseen tai valittujen kohteiden poistamiseen, mutta tässä tehtävässä toteutamme vain perusversion kaventamisesta. ENERGIAT Ensimmäisessä vaiheessa lasketaan pikselien energiat. Voit käyttää apuna monia aiemmin tehtyjä metodeja. Pikselin energian voi periaatteessa laskea monella eri tavalla, mutta yksi parhaista on käyttää kuvan gradienttia pisteessä. Yksinkertaisimmillaan ensimmäinen vaihe muistuttaa sharpen suotimen käyttöä. Suodattamalla kuva alla esitetyillä kahdella eri suotimella saadaan laskettua kuvan gradientti x ja y suunnissa. Meitä kiinnostaa vain gradientin suuruus, joten allaolevien suodinten tuottamat kuvat voi ensin muuntaa harmaasävykuviksi ja liittää sitten niiden arvot yhteen pythagoraan lauseen mukaisesti. grad = funktiona math.sqrt ) x " + y " (scalassa neliöjuurifunktio löytyy 0 0 0 1 0-1 0 0 0 0 1 0 0 0 0 0-1 0 Kuva: Yksinkertaiset vaaka ja pystygradientin laskevat suotimet. Hienomman lopputuloksen saa Sobel-operaattoreilla. https://en.wikipedia.org/wiki/sobel_operator PISTEIDEN ENERGIOISTA SAUMOJEN ENERGIOIHIN Lue algoritmia kuvaava artikkeli wikipediasta. Se selittää asian hyvin esimerkkien kautta. Käytännössä seuraavassa vaiheessa lasketaan dynaamisella ohjelmoinnilla pienimmän energian omaava sauma yhtenäinen pisteiden jono joka kulkee kuvan läpi ylhäältä alas (tai mahdollisesti vasemmalta oikealle). Tässä riittää jos teet ylhäältä alas laskevan version algoritmista. Algoritmi alkaa laskemalla kuvan ylimmän rivin yhden pikselin mittaisten saumojen energiat. Nämä arvot ovat pisteiden energiat, joten itse asiassa meidän ei tarvitse tehdä mitään. Seuraavaksi katsotaan riviä 2. Tällä rivillä oleviin pisteisiin päättyvät saumat joilla on pienin energia ovat kulkevat edellisellä rivillä laskemiemme saumojen kautta. Valitaan siis joka pikselin kohdalla kolmesta sen
yläpuolella olevasta summasta pienin ja summataan siihen pisteen oma energia. Tästä tulee tähän pisteeseen loppuvan sauman energia. Toistamalla prosessia rivi kerrallaan saadaan alimmalle riville pienimmät k.o. pisteisiin päättyvät saumojen energiat. PIENIMMÄN SAUMAN VALINTA JA POISTAMINEN KUVASTA Nyt tarvitsee vain valita pienin luku alimmalta riviltä. Siihen päättyvällä saumalla on pienin energia. Poistetaan vastaava pikseli kuvasta. Valitaan seuraavaksi tämän kohdan yläpuolella olevista pikseleistä (saumoista) se, jolla on pienin energia ja poistetaan se kuvasta. Jatketaan tätä kunnes koko kuva on käyty läpi. VIIMEISET VINKIT Jos haluat tehdä myös vaakasaumoja poistavan version, kierrä tai transponoi kuvan x-ja y-koordinaatit keskenään, poista haluamasi saumat ja palauta lopputulos alkuperäiseen asentoon. Tekemällä pad-metodia muistuttavan apumetodin joka lisää reunoille energioiltaan ylisuuria pisteitä (esim 10 * erotuksen maksimiarvo), ei saumoja laskiessa tai niitä poistaessa tarvitse käsitellä reunoja poikkeavalla tavalla PALAUTUS Testaa vielä lopuksi kaikkien tekemiesi luokkien toiminta ja varmista, että kaikki toimii toivomallasi tavalla. Viimeistään tässä vaiheessa kommentoi koodisi ne pätkät, jotka saattavat olla vaikeaselkoisia. Pakkaa projektisi totuttuun tapaan.zip-paketiksi ja anna sille nimeksi opiskelijanumerosi_image.zip. Kuvatiedostoja tai bin-kansiota ei tule palauttaa joten ruksi ne pois kun luot paketin. Lisää mukaan halutessasi readme.txt tiedosto jossa voit antaa palautetta tehtävästä ja pohtia mikä oli helppoa tai erityisen haastava. Palauta projekti mycoursesissa olevien ohjeiden mukaan. VIELÄ KÄYTTÖLIITTYMÄSTÄ Ohjelman käyttöliittymässä näkyy vasemmalla alkuperäinen kuva, oikealla muokattu. Klikkaamalla suodinten nappeja tai raahaamalla liukusäätimiä voit nähdä niiden vaikutuksen kuvaan. Jos haluat kokeilla useampaa suodinta samaan kuvaan, paina nappia Apply-to-Original, jolloin suodatettu kuva kopioidaan muokattavaksi.