I noen typer utvikling spiller det fortsatt en rolle hvor effektiv hver kodelinje faktisk er. Et slikt område er bildeanalyse - hvor samme operasjon kanskje skal gjentaes på 1 million punkter - eller hvor man skal behandle mange bilder pr. sekund fra en videostrøm. Jeg har tidligere skrevet litt om hvordan man kan gjøre slik bildeanalyse i Python og OpenCV ( Del 1 - Intro, Del 2 - Linjer, Del 3 - Objekter) - og i dag tenkte jeg å vise hvordan jeg går frem for å gjøre koden mest mulig effektiv.

Eksempel

For å illustrere bruker jeg kode fra mine tidligere skriverier om linjer og objekter. Her har vi litt kode som finner linjer i et bilde, tar et utsnitt, rensker bort støy og finner + tegner objekter:

def run():
  img = cv.imread("bordet.jpg", cv.IMREAD_UNCHANGED)
  height, width, _ = img.shape

  lines = findLines(img)
  isects = findIntersections(img, lines)

  mask = np.zeros([height, width], dtype = np.uint8)
  cv.fillPoly(mask, np.array([isects], np.int32), 255)
  maskedImg = cv.bitwise_and(img, img, mask=mask)
  imgGray = cv.cvtColor(maskedImg, cv.COLOR_BGR2GRAY)
  edges = cv.Canny(imgGray, 20, 150)
  th = cv.adaptiveThreshold(imgGray, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)

  strEl = cv.getStructuringElement(cv.MORPH_ELLIPSE, (30, 30))
  closed = cv.morphologyEx(edges, cv.MORPH_CLOSE, strEl)
  strEl2 = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
  opened = cv.morphologyEx(closed, cv.MORPH_OPEN, strEl2)

  cnts, hierarchy = cv.findContours(opened, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
  largeCnts = [cnt for cnt in cnts if cv.contourArea(cnt) > 1000]

  cv.polylines(maskedImg, np.array([isects], np.int32), True, (0, 0, 255), 2)
  cv.drawContours(img, largeCnts, -1, (255, 0, 0), 2, cv.LINE_AA)

  print("Found {} contours".format(len(largeCnts)))

  return [opened, closed, edges, img]

La oss si at denne greia skulle kjøres konstant på en strøm av bilder - og den tok litt lang tid. Så nå ønsker jeg å finne ut hvor jeg bør starte om jeg vil ha litt kjappere behandling. Jeg har opplevd at det sjelden er helt intuitivt hvilken del av koden som bruker mest tid - men har funnet et fint verktøy for å hjelpe meg.

Python line_profiler

For noen år siden kom jeg over line_profiler. Det er et glimrende lite verktøy som kan måle hvor lang tid hver kodelinje i en funksjon bruker. Etter å ha installert line_profiler (pip install line_profiler) får man tilgang til et kommandolinje-verktøy som heter kernprof som brukes i stedet for python når man skal kjøre koden. Da vil den lage en profil av alle funksjoner som har en @profile annotasjon. Så for å se på eksempelkoden over:

  1. Sette inn annotasjon:
@profile
def run():
  img = ...
  1. Kjøre med line_profiler (fila heter bord.py):
kernprof -v -l bord.py

Programmet spytter ut hele koden til run-funksjonen med målte tider:

Total time: 0.139813 s
File: bord.py
Function: run at line 85

Line #      Hits         Time  Per Hit   % Time  Line Contents
==============================================================
    85                                           @profile
    86                                           def run():
    87         1      15181.0  15181.0     10.9    img = cv.imread("bordet.jpg", cv.IMREAD_UNCHANGED)
    88         1          5.0      5.0      0.0    height, width, _ = img.shape
    89
    90         1      81337.0  81337.0     58.2    lines = findLines(img)
    91         1         66.0     66.0      0.0    isects = findIntersections(img, lines)
    92
    93         1         58.0     58.0      0.0    mask = np.zeros([height, width], dtype = np.uint8)
    94         1         97.0     97.0      0.1    cv.fillPoly(mask, np.array([isects], np.int32), 255)
    95         1       2260.0   2260.0      1.6    maskedImg = cv.bitwise_and(img, img, mask=mask)
    96         1        307.0    307.0      0.2    imgGray = cv.cvtColor(maskedImg, cv.COLOR_BGR2GRAY)
    97         1       2665.0   2665.0      1.9    edges = cv.Canny(imgGray, 20, 150)
    98         1       5543.0   5543.0      4.0    th = cv.adaptiveThreshold(imgGray, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY, 11, 2)
    99
   100         1         23.0     23.0      0.0    strEl = cv.getStructuringElement(cv.MORPH_ELLIPSE, (30, 30))
   101         1      26748.0  26748.0     19.1    closed = cv.morphologyEx(edges, cv.MORPH_CLOSE, strEl)
   102         1         33.0     33.0      0.0    strEl2 = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
   103         1       1197.0   1197.0      0.9    opened = cv.morphologyEx(closed, cv.MORPH_OPEN, strEl2)
   104
   105         1        783.0    783.0      0.6    cnts, hierarchy = cv.findContours(opened, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
   106         1         31.0     31.0      0.0    largeCnts = [cnt for cnt in cnts if cv.contourArea(cnt) > 1000]
   107
   108         1        251.0    251.0      0.2    cv.polylines(maskedImg, np.array([isects], np.int32), True, (0, 0, 255), 2)
   109         1       3199.0   3199.0      2.3    cv.drawContours(img, largeCnts, -1, (255, 0, 0), 2, cv.LINE_AA)
   110
   111         1         28.0     28.0      0.0    print("Found {} contours".format(len(largeCnts)))
   112
   113         1          1.0      1.0      0.0    return [opened, closed, edges, img]

Det jeg stort sett har fokusert mest på er % time som altså viser hvor stor andel av den totale kjøretiden som skjedde på akkurat denne kodelinjen. Vi ser på output over at 58.2% av tiden gikk til å finne linjer - og 19.1% på å gjøre EN morphological close operasjon. Så dette burde være gode kandidater om jeg trenger å trimme ned tiden litt.

Det som er litt snedig er at morphological open og close er omtrent samme type operasjon - allikevel tar close her 19.1% og open bare 0.9%. Forskjellen ligger kun i størrelsen på strukturelementet som benyttes. Close bruker et 30x30 pixel ellipse, mens open bare bruker 5x5. La oss se hva som skjer hvis man bruker 5x5 på close også:

   100         1         20.0     20.0      0.0    strEl = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
   101         1       1261.0   1261.0      1.1    closed = cv.morphologyEx(edges, cv.MORPH_CLOSE, strEl)
   102         1          6.0      6.0      0.0    strEl2 = cv.getStructuringElement(cv.MORPH_ELLIPSE, (5, 5))
   103         1       1262.0   1262.0      1.1    opened = cv.morphologyEx(closed, cv.MORPH_OPEN, strEl2)

Dette er ganske logisk om man kan litt om bildeanalyse (~900 operasjoner tar lengre tid enn ~25 når de skal kjøres på 1.25 millioner pixler), men det er ikke alltid like lett å peke ut «synderen» når et stykke kode bruker for lang tid.

Final words…

Python er et språk som er svært lett å profilere. Jeg har brukt line_profiler, men det finnes masse andre verktøy som gjør litt lignende greier. Tidligere har jeg forsøkt å gjøre lignende analyser på Java-kode - og det er noe mer vrient. Det er vel neppe noen som gidder å bruke Java til bildeanalyse, men her har du i hvertfall et godt argument for å bruke Python til ting hvor du trenger god kontroll på ytelsen til hver kodelinje. Om noe skulle gå litt treigt er det veldig fort gjort å finne ut hvorfor!