Bildegjenkjenning 2.0


Bildegjenkjenning for Firebot Vi bestemte oss tidlig for at bildegjenkjenning skulle være fremgangsmåten vi bruker for å finne branner (tente lys) i arbeidsområdet. Vi vurderte to alternative plasseringer av kamera: Enten fastmontert med utsyn over hele arbeidsområdet, eller montert på kjøretøyet. Begge alternativer ble utprøvd, og valget falt på kjøretøymontering. Vi kan dermed montere kameraet sammen med slukke/tenne-utstyret. Neste spørsmål ble da: Hvilken kameraløsning skal vi velge? Alternativene var mange, inkludert:

  • GoPro kamera: Lite og hendig, men vanskelig å hente video-strøm fra.
  • Web-kamera: Njaa …
  • Pixy CMUCam: Kameramodul med innebygget bildegjenkjenning. Hadde sikkert vært midt i blinken, men vi har ikke denne modulen tilgjengelig. Dessuten er det ikke mye læring i å bruke ferdigtygd vare …
  • Raspberry Pi kamera-modul: Denne løsningen viste seg å være et naturlig valg, siden vi uansett hadde beregnet å bruke Raspberry Pi (RasPi) til bildegjenkjenningsbiten. I tillegg viste det seg at skolen hadde Raspi-cam tilgjengelig.

Bildegjenkjenning er en komplisert prosess, og tilnærmet umulig å gjennomføre på den tiden vi har til rådighet dersom man ikke bruker et eksisterende bibliotek. Etter en del leting fant vi, som tidligere nevnt OpenCV. OpenCV (Open Source Computer Vision Library: http://opencv.org) er et anerkjent bildebehandlingsbibliotek med mye dokumentasjon og mange artikler og kodeeksempler tilgjengelig på Internett. En annen forutsetning for valg av bibliotek var at det måtte kunne kjøre på RasPi. Vi fant fort ut at dette skulle være greit. Det viste seg også at SD-kortet som fulgte med Pi’en ikke var stort nok for alt vi måtte installere. Et nytt SD-kort ble kjøpt inn, og Raspbian ble installert. Deretter lastet vi ned OpenCV, og satte i gang prosessen med å bygge dette for RasPi. Biblioteket er på totalt 3.8 GB, og Make-prosessen skulle vise seg å ta 13-14 timer, så dette var greit å gjøre hjemme … Vi bruker raspicam for å kontrollere kameraet fra C++. Dette biblioteket måtte også lastes ned og bygges, men det gikk kjappere. Dette biblioteket er basert på C, så ting må gjøres på en litt annen måte enn en die-hard OO-stakkar er vant til. Dette var imidlertid ikke så veldig store utfordringen, siden interaksjonen mot dette biblioteket er relativt liten. For å kommunisere med det Arduino-baserte styresystemet til roboten, bestemte vi oss for å bruke en I2C-buss. Denne bussen bruker et minimum av ledninger mellom nodene, samtidig som den har kapasitet til å håndtere mange noder samtidig. I vårt tilfelle trenger vi ikke mer enn én server og én klient, men I2C er likevel godt egnet. Vi bruker biblioteket WiringPi til å hjelpe oss med denne kommunikasjonen. En spesiell utfordring ved kommunikasjon mellom Arduino og RasPi er at RasPi ikke tåler mer enn 3.3V på inngangene, mens Arduino gjerne trykker ut 5V. Av denne grunn valgte vi å sette opp RasPien som master, og Arduinoen som slave. Dermed er det alltid RasPien som tilfører spenning til bussen, så den er trygg. Eneste betingelse er at Arduinoen aldri setter sine pinner «høye». Hensikten med bildegjenkjenningsfunksjonen er å finne branner i arbeidsområdet til roboten, og deretter sende koordinatene tilbake til Arduinoen. Dette gjøres ved å lete etter områder i kameraets synsfelt som er lysere enn et gitt nivå. Vi prøvde først å lete kun etter områder med flammens farge og intensitet, men dette viste seg å bli for snevert. Selv om denne fremgangsmåten ga lite støy i bildet, ble det rett og slett for få piksler i kameraets synsfelt til at vi kunne se lyset på litt avstand. Løsningen vi endte opp med var å lete etter lys i alle spekterets farger, med lav metning og høy intensitet. Dette matcher flammen og det nærmeste området av lyset bra, men medfører også en del støy i form av gjenskinn fra vinduer eller kunstig lys. RaspiCam kan levere videostrøm eller stillbilder etter behov. Vi har valgt å behandle stillbilder så fort som plattformen tillater. Dette vil si at et nytt bilde blir tatt så snart det forrige er prosessert. Denne fremgangsmåten gir oss en oppdateringsfrekvens på anslagsvis 4-6 bilder i sekundet, når programmet ikke bruker ressurser på å vise bilder til skjerm. Bildeprosesseringen foregår slik:

  • Et frame tas ved hjelp av en kommando som sendes til raspicam. Dette bildet lagres i et objekt av Mat-klassen fra OpenCV. I dette objektet lagres hver fargekanal i sin egen matrise.
  • Funksjonen cvtColor() brukes for å konvertere bildets farger fra RBG (rød-blå-grønn, kartesisk format) til HSV (hue-saturation-value, sylindrisk koordinatsystem). I HSV-området beskrives farger ved hjelp av fargehjulet med en ekstra akse, slik at det utvides til en sylinder. Hue (nyanse) angir farge som en verdi mellom 0 og 179, og angir «vinkelen» i forhold til startpunktet i hjulet. Saturation (metning) angir «hvor ren» fargen er, angitt ved hvor langt fra sentrum vi er. Lavere saturation (nærmere sentrum) betyr større grad av blanding av farger. Value angir intensiteten, og angis ved høyden på sylinderen.
  • En «threshold»-funksjon benyttes for å selektere piksler med intensitet over ønsket nivå. Resultatet av denne funksjonen lagres i en ny matrise i form av binære verdier, dvs. svarte eller hvite piksler.
  • Deretter går vi i gang med en filtreringsprosess for å fjerne mest mulig støy før vi prøver å finne konturer. Denne prosessen består av «eroding» og «dilation», hvor dilation forsøker å binde sammen nærstående enkeltpiksler til større samlinger (redusere ujevnheter i kantene av interessante områder), mens eroding har til hensikt å fjerne enkeltpiksler som står for seg selv (støy).
  • OpenCV forsøker deretter å finne konturer i bildet ved å se etter områder med sammenhengende piksler, og lagrer hvert av disse i en vector-liste. For hvert område som finnes beregnes arealet (antall piksler i området), samt tyngdepunktet (senteret av området).
  • Vi prøver å eliminere en del støy ved å bare vurdere områder med minimum 10 piksler. Dersom det finnes flere slike områder i bildet, velger vi det området med sentrum nærmest det punktet som ble valgt i forrige frame. Dermed unngår vi en del tilfeller av at forskjellige konturer velges fra frame til frame.
  • Nå sender vi dette punktet til Arduinoen via I2C-bussen som et punkt med verdier mellom (0, 0) og (255,255) hvor (0,0) er øvre venstre hjørne av synsfeltet og (255,255) er nedre høyre hjørne. Vi har valgt å lage en protokoll der vi først sender 3 kjente bytes for å indikere starten av en melding, før x og y-koordinatene sendes til slutt. Dette hindrer at vi får byttet om på rekkefølgen dersom en melding ikke kommer frem som den skal.

ImgProcess   Illustration 1: Debug-bilde til venstre viser treff på flammen. Høyre side viser området som matcher nivået vi ønsker.

Det har vært nok av utfordringer i prosessen med å få til bildeprosesseringen. Det har ikke vært bare enkelt å få alle biblioteker og annen software opp og gå på Raspberry Pi, men det gikk til slutt. Da vi endelig hadde fått det meste opp og gå, sa SD-kortet takk for seg. Masse «bad disk-block» ved boot… Dette er spesielt morsomt når det tar 13-14 timer å bygge bare det ene biblioteket. Jeg plugget kortet inn i en Windows-maskin for å se om det var mulig å kopiere det vha. et backup-program som leser blokker direkte fra disken. Dette gikk ikke; «disken er korrupt». Kortet ble imidlertid stående i maskinen en stund mens jeg bannet og svor. Etter ca. en halv time fant jeg ut at jeg skulle prøve igjen, og da fikk jeg kopiert det uten feilmeldinger. Kanskje det var oppvarmingen som hjalp? Kortet ble byttet på garanti, men før jeg tok det nye kortet i bruk, leste jeg en del på nett om andre som hadde hatt lignende opplevelser. RasPi er ganske nøye på hvilke SD-kort den liker eller ikke. http://elinux.org/RPi_SD_cards lister opp en mengde spesifikke SD-kort med informasjon om RasPi liker dem eller ikke. Det første kortet var imidlertid en av de godkjente typene, så jeg fortsatte å lete. En annen ting som mange nevnte var at det ofte oppstod problemer med SD-kort på RasPi dersom strømforsyningen ikke leverte nok spenning. Det er tydeligvis vitalt at spenningen (målt under drift) mellom TP1 og TP2 på RasPi ikke er under 4.75 eller over 5.25 volt. http://elinux.org/RPi_Hardware (under avsnittet «Power Supply Problems») forklarer hvordan spenningen skal måles under drift. File-RPI_Test_Points File-Voltmeter Jeg målte kun 4.6 volt når jeg brukte USB-porten på pc-en som strømkilde, så jeg regner med at det var en medvirkende årsak til at kortet ble ødelagt. Etter dette brukte vi kun en ekstern strømforsyning for å være sikker på at det var nok spenning. Etter å ha lagt tilbake sikkerhetskopien fra det første kortet, virket alt som det skulle. Jeg har imidlertid blitt mer nøye på å ta sikkerhetskopier med ujevne mellomrom … Når RasPi-en blir montert på kjøretøyet, vil vi ikke ha skjerm og tastatur tilgjengelig for å kunne starte bildeprosesseringsprosessen manuelt. Det må derfor startes automatisk når RasPi-en booter opp. Dette er tilsynelatende enkelt nok, og kan gjøres på mange forskjellige måter og ved forskjellige tidspunkter i boot-prosessen. Det som er viktig å tenke på er at prosessen ikke startes for tidlig, dvs. før alle nødvendige ressurser er tilgjengelig. Vi må også huske på å legge inn en mulighet for å avslutte programmet, f.eks. via en tastekombinasjon på tastaturet. Om man glemmer dette, vil man ikke kunne få tilgang til operativsystem-shellet i det hele tatt. Dooh… Vi endte opp med å legge inn en linje i .bashrc i hjemmekatalogen til brukeren «pi» for å starte opp programmet. På denne måten ble det enkelt å debugge oppstarten, siden dette skriptet kjøres hver gang man åpner et nytt terminalvindu (inkludert det første som operativsystemet åpner ved fullført boot). Vi måtte som tidligere nevnt forsøke en del forskjellige fremgangsmåter for å finne et tent lys på litt avstand, men mener nå at dette er i orden. Kommunikasjonen mellom RasPi og Arduino fungerer fint, og vi er nå klare til å implementere dette på kjøretøyet. 2014-10-24 10.08.152014-10-24 10.06.402014-10-24 10.08.28


Leave a Reply