Göm menyn

5B. Funktionell programmering och högre ordningens funktioner

5.4 Inledning

Uppgifterna i denna omgång går ut på att använda funktionell programmering för att manipulera bilder. Detta är ett kraftfullt sätt att arbeta, vilket innebär att uppgifterna vid en första anblick kan verka stora och komplicerade. Därför inleder vi med att förklara vad uppgiften går ut på i stora drag, för att ni ska få en bild av helheten. Därefter går vi in på detaljerna i enskilda uppgifter. Var därför inte oroliga om ni inte förstår uppgiften på en gång. Fråga din labbhandledare om något känns krångligt, och utnyttja gärna våra programmeringsstugor där du kan få extra hjälp!

Studiematerial

Innan denna deluppgift påbörjas ska följande studiematerial ha lästs:

5.5 Funktioner som utdata

Det är ofta användbart att kunna skapa funktioner som skapar nya funktioner enligt någon viss specifikation. För att förstå hur man arbetar med detta, och för att förstå vad för slags information en funktion kan ha tillgång till, är det värt att ta en titt på begreppet closure.

Vi börjar med ett exempel:

def create_ad(business): def actual_ad(): print("Ät mat från", business) return actual_ad

Här har vi nu en def inuti en def. Funktionen create_ad kommer att returnera en funktion, nämnligen den lokala funktion som vi definierat på raderna ovanför return. Notera att det inte finns några parenteser på sista raden. Vi anropar inte funktionen actual_ad utan använder den som returvärde.

Nu ska vi försöka använda create_ad.

>>> advertise_dibbler = create_ad("CMOT Dibbler's") >>> advertise_dibbler <function actual_ad at 0x00C75A08> >>> business = "Harga's House of Ribs" >>> advertise_dibbler() Ät mat från <???>

Vad kommer att stå på sista raden i exemplet ovan? Vi börjar exemplet ovan med att göra en tilldelning som binder namnet advertise_dibbler till något. Detta något är det som kommer ut från beräkningen till höger om likhetstecknet. Vad som händer, steg för steg, är nu att vi inuti create_ad definierar den lokala funktionen actual_ad. Denna funktion skickar vi sedan som utdata, så när anropet är klart är advertise_dibbler bundet till en funktion. Så långt är allt väl. Nu är frågan vad advertise_dibbler kommer att skriva ut när den anropas. Där funktionen skapades var business = "CMOT Dibbler's", men den anropas från topploopen i Python, där business = "Harga's House of Ribs". Så, vilket värde väljs?

I Python, och många andra språk, är svaret "CMOT Dibbler's". Vi ser alltså att vi har möjligheten att packa ihop en funktion och en uppsättning variabelvärden, så att funktionen "minns" dessa oavsett hur det ser ut utanför. Det här paketet av funktion och omgivning kallas ett closure.

Det här gör också att vi kan skapa olika reklamfunktioner - som inte kommunicerar med varandra - med hjälp av create_ad.

>>> advertise_harga = create_ad("Harga's House of Ribs") >>> advertise_dibbler = create_ad("CMOT Dibbler's") >>> advertise_harga() Ät mat från Harga's House of Ribs >>> advertise_dibbler() Ät mat från CMOT Dibbler's

Övning 506 Skriv en funktion create_lock som tar en kod och ett hemligt meddelande, och ger en kodlåsfunktion. Den funktion som returneras ska inte returnera det hemliga meddelandet förrän den hemliga koden har givits.

>>> my_lock = create_lock(42, 'Lita inte på Lupine!') >>> my_lock <function lock at 0x00C75E40> >>> my_lock(123) Fel kod! >>> my_lock(42) Lita inte på Lupine!

5.6 Anonyma funktioner, lambda

Hittills har vi knutit alla funktioner vi har skapat till namn med hjälp av def. Det behöver man inte nödvändigtvis göra. För enklare beräkningar kan det ibland räcka det med ett så kallat lambda-uttryck. När du skriver sådana räcker det med två delar: inparametrar och utdata.

>>> lambda x,y: x + y <function <lambda> at 0x00C759C0> >>> (lambda x,y: x + y)(1, 2) 3

Lambda kan enbart innehålla uttryck (d v s man kan inte ha satser som t ex if i ett lambda), men i gengäld behöver man inte skriva return för att skicka tillbaka något. Det sker automatiskt. Det andra exemplet ovan visar att vi kan skapa och köra ett lambda i ett enda steg.

Övning 507 Skriv ett lambdauttryck som tar ett heltal som inargument och dubblerar det.

Övning 508 Skriv ett lambdauttryck som tar två inargument och ger summan av kvadraterna på argumenten.

Övning 509 Man kan, även om det blir lite komplicerat, skriva lambdan inuti lambdan. Skriv ett lambdauttryck som tar ett tal x som argument och ger dig en anonym funktion som resultat. Denna anonyma funktion ska i sin tur ta ett argument y, och ge summan av de båda talen x och y. Om du skriver detta vid Python-prompten ska det se ut ungefär så här:

>>> (lambda <din kod här>)(5) # ger en funktion som lägger till 5 till talet y <function <lambda> at 0x00C756A8> >>> (lambda <din kod här>)(5)(3) # applicera funktion ovan med inargumentet 3 8

5.7 Vad går uppgifterna ut på?

Det slutliga målet med uppgifterna i den här omgången är att skriva en funktion som kan kombinera två bilder till en, baserat på en tredje bild (en "mask"). För att få så mycket användning av funktionen som möjligt vill vi att ni ska skriva en så generell funktion som möjligt. Målet är att ha en funktion som kan utföra olika många typer av kombinering. Detta kanske låter svårt och komplicerat, men med hjälp av funktionell programmering (som ni nyss övat på) ska vi guida er steg för steg. Nedan följer några exempel på vad vi vill åstadkomma.

5.7.1 Byta ut blå himmel mot slumpgenererade stjärnor

Vi utgår från följande originalbild:

En bild med blå himmel

En bild med blå himmel

Denna vill vi behandla med hjälp av följande funktion som genererar en stjärnhimmel:

def stars(_): val = random.random()*255 if random.random() > 0.99, else 0 return (val,val,val)

På något sätt vill vi filtrera fram himlen och applicera funktionen ovan för att få följande bild:

En bild med natthimel

En bild med natthimmel

5.7.2 Kombinera två bilder till en med en mjuk övergång mellan

Vi utgår från två originalbilder, en med flygplan och en med blommor. Som tredje bild har vi en bild av samma storlek som de andra två, men som innehåller en gradient där vi växlar från svart till vitt.

Bild 1 Bild 2 En bild som beskriver hur bilderna ska kombineras

Genom att använda den tredje bilden som mask vill vi kombinera ihop de två första så de liksom smälter in i varandra. Målet är att resultatet ska se ut så här:

En jämn övergång mellan bilderna

En jämn övergång mellan bilderna

5.8 In- och utdata

När man står inför en större uppgift kan det vara svårt att se hur man ska gå från idé eller uppgiftsbeskrivning till en fungerande 'produkt'. Ett bra första steg kan vara att fundera på vad funktionen behöver för in- och utdata.

Utdata i det här fallet är relativt enkelt. Funktionen ska skapa en ny bild från sin indata.

Indata är lite mer komplext. Från det andra exemplet ser vi att funktionen behöver kunna ta in tre separata bilder, två källbilder och en bild (mask) som beskriver hur de ska kombineras. Från det första exemplet ser vi att minst ett indata ska kunna vara en funktion som ger en färg för varje pixel istället för en vanlig bild.

Slutligen behövs något sätt att säga hur datan i masken ska tolkas. En generell lösning på det är att ta in en funktion som säger vad ett visst pixelvärde i bilden betyder.

Funktionens definition skulle alltså kunna se ut så här:

def combine_images(mask, mask_function, image_generator1, image_generator2): """ Combines 2 images Input: mask: Image to be interpreted by the mask function mask_function: Returns a value between 0 and 1 for any pixel value. A value of 0 means means use only image 1 and a value of 1 means only use image 2. A value between 0 and 1 means use (value)*image1 + (1-value)*image2 image_generator1: Returns a pixel value for each pixel in an image image_generator2: -||- Output: A new image which is a combination of image1 and 2 """ pass

Och givetvis är det er uppgift att istället för pass fylla på med riktig Python-kod. Detta ska vid dock inte göra riktigt än. Först ska ni göra några förberedande övningar och uppgifter.

5.9 Filtrera ut pixlar

För att få fram resultatet i det första exemplet så måste vi ha en mask-funktion som returnerar 1 för alla blåa pixlar (himlen) och 0 för resten. I HSV-formatet kan detta göras på följande sätt:

def is_sky(pixel): (h,s,v) = pixel if h > 100 and h < 150 and \ s > 50 and s < 200 and \ v > 100 and v < 255: return 1 else: return 0

Funktionen tittar på HSV-värdet för en enskild pixel. För att vi ska tolka det som himmel och returnera 1 måste tre villkor vara uppfyllda:

  • Hue måste ligga mellan 100 och 150, vilket är ungefär där nyansen blå finns.
  • Saturation måste ligga mellan 50 och 200, d v s vi är ganska generösa här. Vi tar inte med riktigt bleka eller riktigt mättade färger.
  • Value måste ligga mellan 100 och 255, d v s vi undviker de riktigt mörka blå färgerna.

Även om den här funktionen fungerar bra, så är den inte särskilt generell. Den är lite bökig att skriva och det riskerar att bli rörigt om vi vill ha andra funktioner för att filtrera fram andra färger. Ett bättre förslag är att skriva en generell funktion som returnerar en jämförelsefunktion så som den ovan om den får min- och max värden för alla tre färgkanaler.

Uppgift 5B1: Välja pixlar

Målet med uppgiften är att du ska kunna definiera högre ordningens funktioner som skapar funktioner.

Definiera funktionen pixel_constraint(hlow, hhigh, slow, shigh, vlow, vhigh) som skapar en ny funktion som givet en pixel returnerar om den ligger mellan low och high för de tre färgkanalerna.

>>> is_black = pixel_constraint(0, 255, 0, 255, 0, 10) >>> is_black((231, 82, 4)) 1 >>> is_black((231, 72, 199)) 0

I exemplet ovan bestämmer vi att svart innebär vilka värden som helst för hue och saturation, men enbart låga värden på value.

Om funktionen som man får från pixel_constraint körs på en lista av pixlar i en HSV-bild kan man få en ny lista där pixlar som uppfyller predikatet är 1 och pixlar som inte uppfyller predikatet är 0. (För en bild img i BGR-format fås samma bild på HSV-format med cv2.cvtColor(img, cv2.COLOR_BGR2HSV)) Bland hjälpfunktionerna i cvlib.py finns funktionen greyscale_list_to_cvimg. Den kan konvertera en sådan lista med ettor och nollor till en svartvit bild.

hsv_plane = cv2.cvtColor(cv2.imread("plane.jpg"), cv2.COLOR_BGR2HSV) plane_list = cvimg_to_list(hsv_plane) is_sky = pixel_constraint(100, 150, 50, 200, 100, 255) sky_pixels = list(map(lambda x: x * 255, map(is_sky, plane_list))) cv2.imshow('sky', greyscale_list_to_cvimg(sky_pixels, hsv_plane.shape[0], hsv_plane.shape[1])) cv2.waitKey(0)

De första två raderna läser in en bild från filen plane.jpg och omvandlar den från BGR- till HSV-format. Därefter omvandlas bilden till en lista med pixlarnas färgvärden med den funktion som ni gjorde i förra labbomgången.

Därefter skapar vi ett filter som identifierar himlen, enligt samma resonemang som ovan. Filtret körs på listan med pixlar och vi får en ny lista med ettor och nollor.

Till sist omvandlar vi listan med ettor och nollor till en svart/vit bild och visar upp den. Om allt gjorts rätt bör vi få en bild som ser ut så här (som kan jämföras med bilden ovan):

5.10 Bilder som funktioner

Hittills har vi representerat bilder som endimensionella listor av pixelvärden vilket fungerar bra för bilder där datan redan finns, till exempel i en fil vi läser in. En mer generell och abstrakt representation av bilder är funktioner som för varje pixel returnerar ett pixelvärde.

Det kan kännas lite extremt och överdrivet "matematiskt" att försöka beskriva bilder med hjälp av funktioner, men det har en stor fördel. Det gör det väldigt enkelt att skapa bilder där datan inte finns i en lista utan att samtidigt behöva skapa en ny lista med datan.

I det inledande exemplet i denna labb använde vi funktionen stars för att generera en enkel bild på en stjärnhimmel. Fördelen här är att bilddatan är generell och inte bunden till en viss storlek på en lista, den kan ge data till en 10x10 eller 10000x10000 pixlar stor bild utan att behöva skapa en lista för varje storlek.

Uppgift 5B2: Funktioner från bilder

Målet med uppgiften är att du ska kunna definiera högre ordningens funktioner som skapar funktioner.

Lite senare ska vi skriva en generell funktion för att kombinera bilder. Den kommer kräva funktioner som genererar bilddata som argument. Om man redan har en bild färdig i en fil behöver man alltså på något sätt skapa en funktion som kan generera den bilden. Det låter krångligare än vad det är. Kolla på exemplet längre ner, så klarnar det säkert.

Skriv en funktion generator_from_image som tar en bild som indata och returnerar en funktion som givet ett index för en pixel returnerar bildens färg i den pixeln.

Funktionerna som returneras ska alltså fungera ungefär som representationen vi har för bilder som endimensionella listor. Returvärdet för index 0 är pixeln i övre vänstra hörnet, returvärdet för bildens bredd är övre högra hörnet, o s v.

orig_img = cv2.imread("plane.jpg") orig_list = cvimg_to_list(orig_img) generator = generator_from_image(orig_list) new_list = [generator(i) for i in range(len(orig_list))] cv2.imshow('original', orig_img) cv2.imshow('new', rgblist_to_cvimg(new_list, orig_img.shape[0], orig_img.shape[1])) cv2.waitKey(0)

Vad händer här? Först läser vi in en bild från en fil och omvandlar den till en lista av pixlar. Därefter skapar vi en generator för den inlästa bilden, med hjälp av den nyskrivna funktionen generator_from_image. För att bevisa att det funkar skapar vi med hjälp av en listbyggare en ny lista som "tvingar fram" bilden ur generatorfunktionen. Till sist visar vi upp originalbilden och den processade bilden. Dessa ska vara lika.

5.11 Funktioner som indata

Vi har sett att det går att skapa funktioner som skapar och ger funktioner som utdata. Nu går vi åt andra hållet, och skapar funktioner som tar funktioner som indata, så kallade högre ordningens funktioner.

Övning 510 Skriv en rekursiv funktion keep_if som har som indata ett predikat (en funktion som kontrollerar indata enligt något kriterium och ger svaret True eller False) och en textsträng. De bokstäver som ska behållas är de som uppfyller kriteriet.

>>> keep_if(lambda bokstav: bokstav == 'e' or bokstav == 'm', 'Vimes') 'me' >>> keep_if(lambda bokstav: False, 'Vimes') ''

Övning 511 Skapa en funktion foreach som tar en funktion och en lista, och ger en lista där funktionen applicerats på varje element i listan. Använd gärna listbyggare.

>>> foreach(lambda x: x**3, [0, 1, 2, 3]) [0, 1, 8, 27]

Övning 512 Om f och g är två matematiska funktioner kan vi kombinera dem till en ny funktion h som vi kan definiera som h(x) = f(g(x)). Här antar vi att både f och g vardera tar ett argument. Skriv en funktion compose som tar två Python-funktioner och ger dig den sammansatta funktionen.

def f(x): return x + 10 def g(y): return 2 * y + 7 >>> h = compose(f, g) >>> h(2) 21

Om du vill ha en lite större utmaning kan du försöka skriva om funktionen compose så att den kan sätta samman funktioner där den andra funktionen g tar godtyckligt antal argument (så exempelvis compose(lambda x: x**2, lambda y,z: y+z)).

Övning 513 Ibland kan man vilja kombinera en funktion med sig själv flera gånger. Detta kan vi t.ex. åstadkomma med den här funktionen:

def apply_repeatedly(fn, n, x): res = x for i in range(n): res = fn(res) return res

Funktionen apply_repeatedly tar en funktion fn, ett tal n som är antalet gånger vi ska upprepa och ett startvärde x. Vi kan t.ex. testa att upprepa kvadrering av tal så här:

>>> apply_repeatedly(lambda x: x**2, 1, 3) 9 >>> apply_repeatedly(lambda x: x**2, 2, 3) 81 >>> apply_repeatedly(lambda x: x**2, 3, 3) 6561

Att köra kvadrering en gång på talet 3 ger givetvis 9 som resultat. Att köra kvadrering två gånger innebär att vi tar resultatet av den första omgången och kvadrerar det igen, vilket ger resultatet 81.

Funktionen apply_repeatedly är i sig inte så tokig, men nu vill vi ha en funktion som kan generera nya funktioner som gör detta lite mer generellt. Definiera därför en rekursiv funktion repeat som tar en funktion och det antal gånger den ska appliceras. Resultatet ska vara en ny funktion som applicerar funktionen rätt antal gånger på indata. Följande exempel förklarar lite tydligare vad funktionen förväntas göra:

>>> square_thrice = repeat(lambda x: x**2, 3) >>> square_thrice(3) 6561

I exemplet ovan skapar vi först funktionen square_thrice och sedan applicerar vi den på talet 3. Detta bör ge resultatet ((3^2)^2)^2 = (9^2)^2 = 81^2 = 6561.

Ledning: Hur ser de olika fallen ut?

Om vi tar en funktion f och repeterar den 1 gång, modell square_once = repeat(lambda x: x**2, 1), bör resultatet bli själva funktionen. Vad bör skilja mellan detta och den som repeterats 2 gånger? Mer allmänt, mellan den som repeterats p gånger och den som repeterats p+1 gånger?

Tips: I övning 512 skrev du förmodligen funktionen compose. Den kan vara användbar här!

Uppgift 5B3: Huvudfunktionen

Målet med uppgiften är att du ska kunna använda högre ordningens funktioner för att lösa komplexa programmeringsproblem.

Implementera nu (äntligen) huvudfunktionen combine_images så att den i följande körexempel ger en bild där himlen är utbytt mot stjärnor.

# Importera nödvändiga bibliotek, inklusive lösningar från förra omgången import cv2 import random from la5a import * # Läs in en bild plane_img = cv2.imread("plane.jpg") # Skapa ett filter som identifierar himlen condition = pixel_constraint(100, 150, 50, 200, 100, 255) # Omvandla originalbilden till en lista med HSV-färger hsv_list = cvimg_to_list(cv2.cvtColor(plane_img, cv2.COLOR_BGR2HSV)) plane_img_list = cvimg_to_list(plane_img) # Skapa en generator som gör en stjärnhimmel def generator1(index): val = random.random() * 255 if random.random() > 0.99 else 0 return (val, val, val) # Skapa en generator för den inlästa bilden generator2 = generator_from_image(plane_img_list) # Kombinera de två bilderna till en, alltså använd himmelsfiltret som mask result = combine_images(hsv_list, condition, generator1, generator2) # Omvandla resultatet till en riktig bild och visa upp den new_img = rgblist_to_cvimg(result, plane_img.shape[0], plane_img.shape[1]) cv2.imshow('Final image', new_img) cv2.waitKey(0)

Uppgift 5B4: Mjuka övergångar

Målet med uppgiften är att du ska kunna sätta dig in i en större mängd redan färdig programkod och utöka den med nya möjligheter.

För att uppnå resultatet i det andra exemplet måste det gå att kombinera bilder med en mjuk övergång mellan. Detta ska göras med formeln. conditon-funktionen ska kunna returnera ett värde 0 < condition < 1 och det värdet ska kombinera bilderna på följande sätt:

generator1*condition + generator2*(1 - condition)

Skriv också en condition-funktion som givet en svartvit bild returnerar ett värde mellan 0 och 1 för olika nyanser av grå. En helt svart pixel ska ge 0 och en helt vit ett värde 1.

>>> gradient_condition((0, 0, 0)) 0 >>> gradient_condition((255, 255, 255)) 1 >>> gradient_condition((128, 128, 128)) 0.5

Använd funktionerna gradient_condition och combine_images med bildfilerna flower.jpg, plane.jpg och gradient.jpg för att skapa bilden som skapades i exempel 2.


Sidansvarig: Peter Dalenius
Senast uppdaterad: 2023-10-09