* Huom. Tämä on kesken*
Kohteiden tunnistaminen kuvasta edellyttää kohteiden kuvailua jonkinlaisen numeroina ilmaistavan piirteen avulla. Kohteiden tunnistamista käsitellään tarkemmin toisessa tutoriaalissa; tällä viikolla käytämme vain yksinkertaisia etäisyysmittoja piirteiden vertailuun. Tämäkin menetelmä on kuitenkin täysin riittävä moniin perinteisiin teollisuuden konenäkötehtäviin.
Lähdetään liikkeelle kuvan kynnystämisestä:
import WebTools
import CV.Thresholding
main = web "kynnystys" "images/bolt_1.png" kynnystys
where
kynnystys :: Image GrayScale D8 -> Image GrayScale D32
kynnystys kuva = montage (3,1) 5 [
unsafeImageTo32F $ thresholdOtsu ZeroAndMax kuva,
unsafeImageTo32F $ adaptiveThreshold ByMean ZeroAndMax 5 5 kuva,
unsafeImageTo32F $ adaptiveThreshold ByMean MaxAndZero 5 0 kuva ]
OpenCV:ssä on monia edistyneitä kynnystysmenetelmiä, joista pari esimerkkiä tässä.
Morfologiset operaatiot ovat eräänlaisia filttereitä, jotka muuttavat binäärikuvan muotoa. Käymme ne läpi lyhyesti, sillä niitä tarvitaan usein kynnystyksen jälkeen löydettyjen kappaleiden siistimiseen, esimerkiksi pienien reikien ja rakojen paikkaamiseen tai tukkeutuneiden reikien avaamiseen.
Morfologiset operaatiot perustuvat muokkauselementtiin (sructuring element). Se on eräänlainen binäärinen maski, tyypillisesti paljon tutkittavaa kappaletta pienempi, jota liikutetaan kappaleen yli. Kappaleen rakennetta muokataan sen perusteella, kuinka elementti sopii kappaleen sisälle. Tyypillisiä elementtejä ovat eri muotoiset ja kokoiset laatikot ja ellipsit.
Yleinen morfologinen operaatio (Szeliski 3.3.2 mukaan) on muotoa
\(\theta(c,t) = \begin{cases} 1, &\text{ jos } c \geq t\\ 0, &\text{ muuten.} \end{cases}\)
Tässä funktiolla \(c\) tarkoitetaan kuvan konvolvointia muokkauselementillä \(s\) (\(c = f * s\)) ja lopputuloksen kynnystämistä. Eri kynnysarvoilla saadaan eri tavoin käyttäytyviä operaatioita. Kun \(S\) on muokkauselementin koko, Szeliski määrittelee perusoperaatiot näin:
Laajentaminen siis ottaa pikselin mukaan, jos yksikin elementin pikseleistä osuu kappaleen sisälle, mikä tietysti kasvattaa kappaleen kokoa. Kuluttaminen ottaa pikselin mukaan vain, jos elementti sopii kuvan sisälle kokonaan, mikä kutistaa kuvaa. Jos ensin laajennetaan ja sitten kulutetaan, kappaleessa mahdollisesti olevat pienet reiät sulkeutuvat, ja jos ensin kulutetaan ja sitten laajennetaan, kappaleessa mahdollisesti olevat tukkeutuneet kohdat aukenevat.
Yleisiä matemaattisia merkintöjä operaatioille ovat
Lisätietoa morfologiasta wikipediassa.
Kokeillaan sitten aiemmin kynnystämämme kuvan siistimistä morfologisten operaatioiden avulla:
import WebTools
import CV.Thresholding
import CV.Morphology
main = web "muokkaus" "images/bolt_1.png" muokkaus
where
muokkaus :: Image GrayScale D8 -> Image GrayScale D32
muokkaus kuva = montage (3,1) 5
[ unsafeImageTo32F $ kynnystetty
, unsafeImageTo32F $ laajennettu
, unsafeImageTo32F $ kulutettu
]
where
kynnystetty = thresholdOtsu MaxAndZero kuva
laajennettu = dilate elem 1 kynnystetty
kulutettu = erode elem 1 laajennettu
elem = structuringElement (5,5) (1,1) EllipseShape
Jos kuvassa saattaa esiintyä useita tutkittavia kohteita yhtä aikaa, kynnystetystä ja siistitystä binäärikuvasta etsitään seuraavaksi erilliset, yhtenäiset alueet connected components -menetelmän avulla (Szeliski 3.3.4). Lyhyesti kuvattuna tämä menetelmä etsii ja merkitsee alueet, jotka koostuvat vierekkäisistä pikseleistä.
Perinteinen ja yleissivistykseen kuuluva tapa kuvailla binäärikuvasta löytyviä kappaleita ovat niin sanotut kuvamomentit. Momentti on tietynlainen painotettu keskiarvo kuvan tai kappaleen pikseliarvoista. Momentit kuvailevat kappaleen muotoa tilastollisten tunnuslukujen tapaan.
Määritellään momentit \(M_{ij}\) kuvalle \(I(x,y)\) seuraavasti:
\(M_{ij} = \sum_x \sum_y x^i y^j I(x,y)\).
Pohtimalla hetken yllä olevaa kaavaa huomaamme, että
\(M_{00} = \sum_x \sum_y I(x,y)\)
mikä binäärikuvan tapauksessa tarkoittaa pinta-alaa. Samoin huomaamme, että \(M_{10}\) on kappaleen x-koordinaattien summa ja \(M_{01}\) on y-koordinaattien summa. Siten
\((x_c,y_c) = (\frac{M_{10}}{M_{00}},\frac{M_{01}}{M_{00}})\)
on kappaleen sentroidi eli massakeskipiste. Kaikki momentit voidaan laskea myös harmaasävykuville, jolloin kuva on ymmärrettävä eräänlaisena tiheysfunktiona. Sentroidin avulla voidaan määritellä keskeismomentit (central moments) jotka ovat siirtoinvariantteja eli eivät riipu kappaleen sijainnista kuvassa.
Määritellään keskeismomentit \(\mu_{ij}\) kuvalle \(I(x,y)\) ja sentroidille \((x_c,y_c)\) seuraavasti:
\(\mu_{ij} = \sum_x \sum_y (x - x_c)^i (y - y_c)^j I(x,y)\).
Keskeismomenttien avulla saadaan laskettua esimerkiksi kappaleen kovarianssimatriisi, jonka ominaisvektorit laskemalla voidaan analysoida kappaleen pääakseleita ja niiden suuntaa.
Keskeismomentit, joille \(i + j \ge 2\), saadaan vielä normalisoitua siten, että ne ovat skaala-invariantteja eli riippumattomia kappaleen koosta kuvassa:
\(\eta_{ij} = \frac{\mu_{ij}}{\mu_{00}^{1 + \frac{i + j}{2}}}\).
Näistä normalisoiduista keskeismomenteista voidaan muodostaa kääntöinvariantteja momentteja, jotka siis eivät riipu kappaleen asennosta kuvassa. Tunnetuimpia ovat Hu-momentit, jotka löytyvät myös OpenCV:stä. Näistä esimerkkinä mainittakoon ensimmäinen Hu-momentti
\(H_1 = \eta_{20} + \eta_{02}\)
jolla on vastaavuus fysiikasta tutun inertiamomentin kanssa; jos tulkitaan pikselien arvot fysikaaliseksi tiheydeksi, tämä momentti kuvaisi sentroidin eli massakeskipisteen ympäri pyörivän kappaleen inertiamomenttia.
Tarkempaa tietoa kuvamomenteista wikipediasta. Huomattakoon täältä esimerkiksi Hu-momentteja kohtaan esitetty kritiikki, jonka mukaan toinen ja kolmas momentti eivät sopisi hyvin hahmontunnistukseen.
m00 | 2240.000 |
m01 | 107830.000 |
m02 | 6397696.000 |
m03 | 427320202.000 |
m10 | 105814.000 |
m11 | 6020746.000 |
m12 | 393823640.000 |
m20 | 5875750.000 |
m21 | 371182176.000 |
m30 | 359394856.000 |
import WebTools
import Text.Printf
import Data.List
import CV.Thresholding
import CV.Morphology
import CV.ConnectedComponents
main = webTable "perusmomentit" "images/bolt_1.png" perusmomentit
where
perusmomentit :: Image GrayScale D8 -> (Image GrayScale D8, [[String]])
perusmomentit kuva = (
unsafeImageTo8Bit suljettu,
transpose
[ [ "m00","m01","m02","m03","m10","m11","m12","m20","m21","m30" ]
, (map (printf "%.3f") mom) ]
)
where
kynnystetty = thresholdOtsu ZeroAndMax kuva
suljettu = close elem (unsafeImageTo32F kynnystetty)
elem = structuringElement (7,7) (1,1) EllipseShape
mom = spatialMoments suljettu True
mu00 | 2240.000 |
mu01 | 0.000 |
mu02 | 1206933.098 |
mu03 | 3145759.303 |
mu10 | 0.000 |
mu11 | 927030.098 |
mu12 | 2355284.855 |
mu20 | 877266.698 |
mu21 | 750190.230 |
mu30 | -1047468.692 |
import WebTools
import Text.Printf
import Data.List
import CV.Thresholding
import CV.Morphology
import CV.ConnectedComponents
main = webTable "keskimomentit" "images/bolt_1.png" keskimomentit
where
keskimomentit :: Image GrayScale D8 -> (Image GrayScale D8, [[String]])
keskimomentit kuva = (
unsafeImageTo8Bit suljettu,
transpose
[ [ "mu00","mu01","mu02","mu03","mu10","mu11","mu12","mu20","mu21","mu30" ]
, (map (printf "%.3f") mom) ]
)
where
kynnystetty = thresholdOtsu ZeroAndMax kuva
suljettu = close elem (unsafeImageTo32F kynnystetty)
elem = structuringElement (7,7) (1,1) EllipseShape
mom = centralMoments suljettu True
eta00 | 1.000 |
eta01 | 0.000 |
eta02 | 0.241 |
eta03 | 0.013 |
eta10 | 0.000 |
eta11 | 0.185 |
eta12 | 0.010 |
eta20 | 0.175 |
eta21 | 0.003 |
eta30 | -0.004 |
import WebTools
import Text.Printf
import Data.List
import CV.Thresholding
import CV.Morphology
import CV.ConnectedComponents
main = webTable "normimomentit" "images/bolt_1.png" normimomentit
where
normimomentit :: Image GrayScale D8 -> (Image GrayScale D8, [[String]])
normimomentit kuva = (
unsafeImageTo8Bit suljettu,
transpose
[ [ "eta00","eta01","eta02","eta03","eta10","eta11","eta12","eta20","eta21","eta30" ]
, (map (printf "%.3f") mom) ]
)
where
kynnystetty = thresholdOtsu ZeroAndMax kuva
suljettu = close elem (unsafeImageTo32F kynnystetty)
elem = structuringElement (7,7) (1,1) EllipseShape
mom = normalizedCentralMoments suljettu True
hu1 | 0.415 |
hu2 | 0.141 |
hu3 | 0.001 |
hu4 | 0.000 |
hu5 | 0.000 |
hu6 | 0.000 |
hu7 | -0.000 |
import WebTools
import Text.Printf
import Data.List
import CV.Thresholding
import CV.Morphology
import CV.ConnectedComponents
main = webTable "hu-momentit-1" "images/bolt_1.png" hu_momentit
where
hu_momentit :: Image GrayScale D8 -> (Image GrayScale D8, [[String]])
hu_momentit kuva = (
unsafeImageTo8Bit suljettu,
transpose
[ [ "hu1","hu2","hu3","hu4","hu5","hu6","hu7" ]
, (map (printf "%.3f") mom) ]
)
where
kynnystetty = thresholdOtsu ZeroAndMax kuva
suljettu = close elem (unsafeImageTo32F kynnystetty)
elem = structuringElement (7,7) (1,1) EllipseShape
mom = huMoments suljettu True
Momenttien lisäksi kynnystettyjä kappaleita voidaan kuvailla erilaisten histogrammien eli diskreettien jakaumien perusteella. Kynnystettyä binäärikuvaa, josta on irrotettu haluttu kappale, voidaan käyttää myös maskina jonka avulla muodostetaan harmaasävyhistogrammi kyseisen kappaleen alueelta. Samoin kappaleesta voidaan laskea intensiteetin keskiarvo ja keskihajonta, joiden perusteella voidaan toisinaan tunnistaa kappaleita, joiden kirkkaus tai väri poikkeaa huomattavasti toisistaan.
Muistellaan hieman keskiarvon ja keskihajonnan määritelmiä ja yritetään löytää intuitiivisia ajattelutapoja niiden käyttämiseen.
Muistamme, että keskiarvo on kuvan \(I\) (jossa pikselien lukumäärä on \(N\)) satunnaisen pikselin intensiteetin odotusarvo:
\(\mu = E\left[I(x,y)\right] = \frac{1}{N}\sum_{x,y} I(x,y)\).
Vastaavasti varianssi on odotusarvo satunnaisen pikselin neliölliselle etäisyydelle keskiarvosta:
\(\sigma^2 = E\left[(I(x,y) - \mu)^2\right] = \frac{1}{N}\sum_{x,y}(I(x,y) - \mu)^2\).
Kuvien varianssin ja keskihajonnan laskemisen kannalta on hyvä tietää seuraava yhtäsuuruus:
\(\sigma^2 = E\left[I(x,y)^2\right] - E\left[I(x,y)\right]^2\).
Kuvan intensiteetin varianssi on siis helppo laskea pikselien intensiteetin ja neliöllisen intensiteetin summien avulla. Integraalikuvaa käyttäen taas saadaan laskettua minkä tahansa suorakulmaisen alueen summa kolmella laskutoimituksella.
Keskihajonta \(\sigma\) on siis varianssin \(\sigma^2\) neliöjuuri, ja se on hyödyllinen mitta päätettäessä esimerkiksi, onko kahden alueen intensiteetti tai väri riittävän kaukana toisistaan. Jos kahden alueen intensiteetin erotus on suurempi kuin niiden keskihajontojen summa, ne todennäköisesti ovat keskimäärin eri värisiä.
Kappaleista voidaan muodostaa myös tekstuurijakaumia käyttäen esimerkiksi local binary pattern -piirteitä. Tämä on suomalaisten Ojalaisen ja Pietikäisen kehittämä tekstuuripiirre, jossa tutkitaan kunkin pikselin ympäristöä, ja katsotaan mitkä naapuruston pikselit ovat suurempia kuin tarkasteltava pikseli. Jos tutkitaan 8-naapurustoa, tällä tavoin saadaan 8-bittinen binääriluku. Pyöräyttämällä luvun sillä tavoin, että suurin tai pienin nollien tai ykkösten jono tulee aina ensiksi, saadaan piirteestä kääntöinvariantti. Näistä piirteistä muodostetaan yleensä histogrammi tutkittavalta kuva-alueelta. Lisätietoa erinomaisessa Matti Pietikäisen artikkelissa scholarpediassa.
import WebTools
import Text.Printf
import Data.List
import CV.Thresholding
import CV.Morphology
import CV.Textures
tableToHist :: [Double] -> Image GrayScale D32
tableToHist t = imageFromFunction (w,100) (f t m)
where
w = length t
m = maximum t
f t m (x,y) | (floor $ (t !! x) / m * 100) > y = 1
| otherwise = 0
main = web "nut-hist" "images/nut.png" lbp_hist
where
lbp_hist :: Image GrayScale D8 -> Image GrayScale D8
lbp_hist kuva = unsafeImageTo8Bit hist
where
kynnystetty = thresholdOtsu ZeroAndMax kuva
suljettu = close elem (unsafeImageTo32F kynnystetty)
elem = structuringElement (10,10) (1,1) EllipseShape
taul = weightedLBP 0 0 suljettu (unsafeImageTo32F kuva)
hist = tableToHist taul
Tilastollisten momenttien lisäksi kappaleen muotoa voidaan kuvailla sen reunakäyrän avulla. OpenCV:n connected components -menetelmät palauttavat kappaleet suoraan reunakäyrän muodossa.
Konvekseille kappaleille voidaan muodostaa reunapiirre sämpläämällä etäisyys keskipisteestä reunalle tietyin kulma-askelin. Tästä esityksestä saadaan kääntöinvariantti valitsemalla aloituskohdaksi suurin tai pienin etäisyys.
Epäkonvekseille kappaleille saadaan myös reunapiirre etsimällä ensin kappaleen konveksi peite ja kuvailemalla erikseen peitteen reuna ja peitteen ja kappaleen leikkauksena syntyvät ylimääräiset palaset.
Edellisessä tutoriaalissa laskimme fourier-muunnosta kuville. Nyt kokeilemme, miten kappaleiden reunakäyrästä laskettuja fourier-piirteitä voi käyttää kappaleen muodon kuvailuun ja kappaleiden tunnistamiseen.
Koska fourier-kertoimet kuvailevat reunakäyrän taajuusvastetta, ottamalla huomioon pelkät taajuuskomponentit ja jättämällä vaiheen huomiotta saadaan kääntöinvariantti esitys muodosta.
Voimme tehdä fourier-muunnoksen keskipisteestä sämplättyjen etäisyyksien taulukolle, mutta kätevämpi tapa muodostaa reunakäyrän kuvaus on seurata reunaa tasaisella nopeudella ja sämplätä käyrän tangentti tasaisin väliajoin. Tällaisesta piirteestä olisi vaikeampi saada kääntöinvariantti sellaisenaan, mutta fourier-muunnoksen periodisuuden ansiosta tämä käy helposti tekemällä yksinkertaisesti sämplätylle kulmataulukolle fourier-muunnoksen.
Etsi oheisesta kuvasta erilliset kappaleet, ja merkitse ruuvit ja mutterit eri väreillä.
Kokeillaan käyttää Hu-momentteja tunnistukseen. Lasketaan ensin momentit mallikappaleille:
hu1 | 0.407 |
hu2 | 0.133 |
hu3 | 0.001 |
hu4 | 0.000 |
hu5 | 0.000 |
hu6 | 0.000 |
hu7 | -0.000 |
import WebTools
import Text.Printf
import Data.List
import CV.Thresholding
import CV.Morphology
import CV.ConnectedComponents
main = webTable "hu-bolt" "images/bolt.png" hu_momentit
where
hu_momentit :: Image GrayScale D8 -> (Image GrayScale D8, [[String]])
hu_momentit kuva = (
unsafeImageTo8Bit suljettu,
transpose
[ [ "hu1","hu2","hu3","hu4","hu5","hu6","hu7" ]
, (map (printf "%.3f") mom) ]
)
where
kynnystetty = thresholdOtsu ZeroAndMax kuva
suljettu = close elem (unsafeImageTo32F kynnystetty)
elem = structuringElement (10,10) (1,1) EllipseShape
mom = huMoments suljettu True
hu1 | 0.193 |
hu2 | 0.001 |
hu3 | 0.000 |
hu4 | 0.000 |
hu5 | -0.000 |
hu6 | -0.000 |
hu7 | -0.000 |
import WebTools
import Text.Printf
import Data.List
import CV.Thresholding
import CV.Morphology
import CV.ConnectedComponents
main = webTable "hu-nut" "images/nut.png" hu_momentit
where
hu_momentit :: Image GrayScale D8 -> (Image GrayScale D8, [[String]])
hu_momentit kuva = (
unsafeImageTo8Bit suljettu,
transpose
[ [ "hu1","hu2","hu3","hu4","hu5","hu6","hu7" ]
, (map (printf "%.3f") mom) ]
)
where
kynnystetty = thresholdOtsu ZeroAndMax kuva
suljettu = close elem (unsafeImageTo32F kynnystetty)
elem = structuringElement (10,10) (1,1) EllipseShape
mom = huMoments suljettu True
Lasketaan sitten momentit testikappaleille:
import WebTools
import Text.Printf
import Data.List
import CV.Thresholding
import CV.Morphology
import CV.ConnectedComponents
import CV.ImageMath as IM
sizeAndMask c i = (size,mask)
where
mask = maskConnectedComponent c i
size = (IM.sum $ unsafeImageTo32F mask) / 255
huBin = (flip huMoments) True
main = webTable "hu-nuts-and-bolts" "images/nuts_and_bolts.png" lajittele
where
lajittele :: Image GrayScale D8 -> (Image GrayScale D8, [[Double]])
lajittele kuva =
( montage (1,2) 4 $
[ kuva
, suljettu8
]
, transpose moms
-- [ [ "num","hu1","hu2","hu3","hu4","hu5","hu6","hu7" ]
-- , (show num) : (map (printf "%.3f") mom)
-- ]
)
where
kynnystetty = thresholdOtsu ZeroAndMax kuva
suljettu = close elem (unsafeImageTo32F kynnystetty)
suljettu8 = unsafeImageTo8Bit suljettu
elem = structuringElement (10,10) (1,1) EllipseShape
(komp,num) = fillConnectedComponents suljettu8
masks = map snd $ filter ((>5).fst) $ [sizeAndMask komp i | i <- [1..num]]
moms = map (huBin . unsafeImageTo32F) masks
Error:
Code.hs:22:11:
Couldn't match expected type `D32' with actual type `D8'
Expected type: Image GrayScale D32
Actual type: Image GrayScale D8
In the expression: kuva
In the second argument of `($)', namely `[kuva, suljettu8]'