I forrige post gikk jeg gjennom en kjapp introduksjon til bildeanalyse - og viste hvordan man kan komme i gang med Python og OpenCV. Vi gikk gjennom hvordan man leser inn og viser frem bilder, konverterer til svarthvit og binært, samt hvordan man kan gjøre en ‘morphological close’ for å lukke huller i et bilde. Nå er det på tide å gjøre noe mer fornuftig - prøve å få ut nyttig informasjon fra et bilde.

En av de mest grunnleggende problemene i bildeanalyse er det å finne linjer i et bilde. Det kan for eksempel dreie seg om å finne igjen omrisset av et registreringsskilt på et bilde av en bil, vinkelen på et hustak eller hva som helst annet som dreier seg om ting som utgjør relativt rette streker på et bilde. Jeg har for eksempel brukt det til å kjenne igjen de forskjellige sektorene på en dartskive.

Dagens eksempel

Jeg har et kjøkkenbord som ser veldig fint ut når det er ryddig. Dagens noe teoretiske eksempel går ut på å bygge et system som kan ta bilde av dette kjøkkenbordet og finne ut om bordet er tomt eller fullt opp av forskjellige gjenstander. Jeg har tatt et bilde vi kan bruke som utgangspunkt:

Bilde av kjøkkenbordet

Som vi ser er ikke bordet helt ryddig, men det er ikke så farlig. Vi ser også at bildet er tatt fra en vinkel som gjør at bordet egentlig ikke ser helt firkantet ut - og vi har fått med masse ‘rot’ i form av stoler, vinduskarm og gulv. Utfordringen blir her i første omgang å identifisere selve bordflaten. Jeg er jo bare interessert i det som befinner seg på bordet.

En observasjon jeg gjør meg er at kantene på bordet ser ut til å utgjøre ganske distinkte linjer i bildet. Så planen min er altså å finne disse linjene og gjøre noe smart som avgrenser området vårt basert på dette.

Hough Transform - finne linjer

En vanlig måte å finne linjer i bilder er å bruke Hough Transform. Det er en relativt enkel, men smart, måte å bruke matematikk på bilder på. Man konverterer punktene i bildet til polarkoordinater og lager en slags stemmeordning som finner punkter som ligger innenfor en ønsket klasse - for eksempel linjer. Den kan også brukes til å finne sirkler og andre fasonger. Selve teorien er godt forklart i Wikipedia-artikkelen.

Forarbeid - klargjøre bildet

OpenCV sin implementasjon av Hough Transform krever et binært bilde som input. Så vi starter med å lese inn bildet og konvertere det til svarthvit:

img = cv.imread("bordet.jpg", cv.IMREAD_UNCHANGED)

imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)

Deretter kan vi enten bruke Thresholding som vi gjorde i introduksjonen, eller en annen teknikk for kantgjenkjennelse. Jeg prøver meg på det som heter Canny Edge Detection. Den tar to parametre i tillegg til selve bildet - minVal og maxVal. Alle gråtoner over maxVal regnes som kant - alle under minVal utelukkes. Verdiene i midten inkluderes om de er forbundet med sikre kanter. Jeg prøvde meg litt frem og fant ut at minVal=20 og maxVal=150 gir ok resultater:

edges = cv.Canny(imgGray, 20, 150)

Da sitter vi igjen med et bilde som gir et omriss av de viktigste formene. Ganske kult i grunn. Dette blir da utgangspunktet vårt for å finne linjer:

Canny Edge Detection

Deteksjon av unike linjer

Nå er vi endelig klare til å prøve å finne disse linjene. Det er jo mange linjer i bildet, men vi vet en del om de vi er ute etter så vi får prøve å tilpasse etter det. Det første vi gjør er å bruke funksjonen HoughLines - som gir oss en liste av (rho, theta) verdier tilbake. Dette er altså vinkel i radianer theta og avstand i pixler rho (se teori om Hough Transform):

import numpy as np
...

lines = cv.HoughLines(edges, 1, np.pi / 720, 200)

Jeg har laget en liten funksjon for å tegne linjene vi har funnet i rødt tilbake på det originale bildet. Det er kjekt for å se om vi er på rett spor:

def drawLines(img, lines):
  for (rho, theta) in lines:
    a = np.cos(theta)
    b = np.sin(theta)
    x0 = a * rho
    y0 = b * rho
    x1 = int(x0 + 2000 * -b)
    y1 = int(y0 + 2000 * a)
    x2 = int(x0 - 2000 * -b)
    y2 = int(y0 - 2000 * a)
    cv.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)

...
drawLines(img, lines)

Vi har funnet masse forskjellige linjer! De ser ganske riktige ut - vi har funnet alle de 4 vi er ute etter, og en hel del vi strengt tatt ikke trenger:

56 linjer

Man kan fintune parametrene til HoughLines for å redusere antallet, men jeg mistenker at vi fortsatt vil sitte igjen med duplikater. Derfor synes jeg det funker bra å lage seg en funksjon som gjør noe smart for å luke vekk duplikater. Vi vet jo at vi bare er interessert i en linje på hvert sted - så jeg runder av både rho og theta og bruker en Python dictionary for å passe på at vi kun tar vare på en:

lines = cv.HoughLines(edges, 1, np.pi / 720, 200)
print(f"Found {len(lines)} lines!")
uniqueLines = {}

for line in lines:
  rho, theta = line[0]

  roundedTheta = round(theta * 2) / 2
  roundedRho = round(rho / 500)
  roundedLine = (roundedRho, roundedTheta)

  if(uniqueLines.get(roundedLine, None) is None):
    uniqueLines[roundedLine] = (rho, theta)

print(f"Rounded to {len(uniqueLines)} lines")
return uniqueLines.values()

Avhengig av hvor mye man vet om linjene man ønsker å finne går det an å finne de som ligger nærmest opp til et sett kjente verdier også. Men her ser det ut som den enkle øvelsen med avrunding har gjort susen:

4 linjer

Jeg slang på litt printing også - for å være sikker på at vi virkelig bare har 4 linjer igjen. Ser ut som vi hadde litt flaks med den avrundingen. Ofte vil man nok oppleve at man må jobbe litt mer for å plukke ut kun de linjene man faktisk vil ha.

Klar for å finne objekter

Nå som vi har funnet linjene som utgjør kanten på bordet har vi et perfekt utgangspunkt når vi skal sjekke om bordet er tomt eller ikke. Og det er et fint tema for neste post :)

Her er det komplette testprogrammet jeg har benyttet meg av i dag. Jeg har delt det inn i litt forskjellige funksjoner. resize er en hjelpefunksjon som endrer størrelsen på bildet for å passe på skjermen, findLines finner de unike linjene, drawLines tegner et sett linjer på et bilde og run knytter hele greia sammen:

import cv2 as cv
import numpy as np

def resize(img, frmt=0.5):
  if frmt != 1:
    return cv.resize(img, None, fx=frmt, fy=frmt, interpolation=cv.INTER_CUBIC)
  else:
    return img

def findLines(img):
  imgGray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
  edges = cv.Canny(imgGray, 20, 150)
  lines = cv.HoughLines(edges, 1, np.pi / 720, 200)
  print(f"Found {len(lines)} lines!")
  uniqueLines = {}

  for line in lines:
    rho, theta = line[0]

    roundedTheta = round(theta * 2) / 2
    roundedRho = round(rho / 500)
    roundedLine = (roundedRho, roundedTheta)

    if(uniqueLines.get(roundedLine, None) is None):
      uniqueLines[roundedLine] = (rho, theta)

  print(f"Rounded to {len(uniqueLines)} lines")
  return uniqueLines.values()

def drawLines(img, lines):
  for (rho, theta) in lines:
    a = np.cos(theta)
    b = np.sin(theta)
    x0 = a * rho
    y0 = b * rho
    x1 = int(x0 + 2000 * -b)
    y1 = int(y0 + 2000 * a)
    x2 = int(x0 - 2000 * -b)
    y2 = int(y0 - 2000 * a)
    cv.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)

def run():
  img = cv.imread("bordet.jpg", cv.IMREAD_UNCHANGED)
  lines = findLines(img)
  drawLines(img, lines)
  return img

img = run()

cv.imshow("bord", resize(img))
cv.moveWindow("bord", 0, 0)
cv.waitKey(0)
cv.destroyAllWindows()