TIES411 - Tilastolliset piirteet

* Huom. Tämä on kesken*

Aihe

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.

Kynnystys

Lähdetään liikkeelle kuvan kynnystämisestä:

Test

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ä.

Morfologia

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:

Test

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

Yhtenäiset alueet

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ä.

Kuvien momentit

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.

Test
m002240.000
m01107830.000
m026397696.000
m03427320202.000
m10105814.000
m116020746.000
m12393823640.000
m205875750.000
m21371182176.000
m30359394856.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
Test
mu002240.000
mu010.000
mu021206933.098
mu033145759.303
mu100.000
mu11927030.098
mu122355284.855
mu20877266.698
mu21750190.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
Test
eta001.000
eta010.000
eta020.241
eta030.013
eta100.000
eta110.185
eta120.010
eta200.175
eta210.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
Test
hu10.415
hu20.141
hu30.001
hu40.000
hu50.000
hu60.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

Jakaumat

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.

Test

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

Muotopiirteet

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.

Fourier-reunapiirteet

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.

Tehtävä

Etsi oheisesta kuvasta erilliset kappaleet, ja merkitse ruuvit ja mutterit eri väreillä.

Kokeillaan käyttää Hu-momentteja tunnistukseen. Lasketaan ensin momentit mallikappaleille:

Test
hu10.407
hu20.133
hu30.001
hu40.000
hu50.000
hu60.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
Test
hu10.193
hu20.001
hu30.000
hu40.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]'