Tuesday, February 26, 2013

การหาขอบ (Edge detection)

การหาขอบของภาพ ถูกนำมาใช้เพื่อหลายวัตถุประสงค์ เช่น เพื่อ segment ภาพนั้นออกเป็นส่วนๆ หรือ เพื่อเป็นกระบวนการเบื้องต้นสำหรับการหาเส้นตรง หรือวงกลมในภาพ ฯลฯ ในที่นี้เราจะลองใช้คำสั่งสำเร็จรูปของ OpenCV เพื่อหาขอบภาพ ด้วยสามวิธี ได้แก่



Canny

import cv2
import numpy as np

img = cv2.imread("lena.jpg",cv2.CV_LOAD_IMAGE_GRAYSCALE)
#Appy Gaussian blur to remove some noises
inoise = cv2.GaussianBlur(img,(3,3),sigmaX=0)

#Canny edge detection
lowThresh = 50
upThresh = 100
out = cv2.Canny(inoise,lowThresh,upThresh)

cv2.imshow("Origin",img)
cv2.imshow("Edge Detection",out)
cv2.waitKey()
cv2.destroyAllWindows()


Sobel
#Gradient X
gradX = cv2.Sobel(inoise,cv2.CV_16S,dx=1,dy=0)
#Gradient Y
gradY = cv2.Sobel(inoise,cv2.CV_16S,dx=0,dy=1)
#Convert Gradients to 8 bits
gradX = cv2.convertScaleAbs(gradX)
gradY = cv2.convertScaleAbs(gradY)
#Total Gradient (approximate)
#grad = abs(gradX) + abs(gradY)
out = cv2.addWeighted(gradX,1,gradY,1,0)


Laplacian
out = cv2.Laplacian(inoise,ddepth=cv2.CV_16S,ksize=3)
#Need to set depth to 16-bit signed integer because Laplacian can give negative intensity.
# Convert it to 8-bit image with absolute value.
out = cv2.convertScaleAbs(out)

Monday, February 25, 2013

Filtering / Convolution

การทำ Filtering / Convolution ก็คือการเอา kernel / template / filter (หน้าต่างขนาดเล็กว่ารูป มักมีขนาดเป็นเลขคี่ เช่น 3x3, 5x5, 7x7 ฯลฯ) มาคูณกับพื้นที่ของรูปที่ kernel นั้นซ้อนทับอยู่ แล้วหาผลรวม จากนั้นก็นำไปแทนที่พิกเซลที่อยู่ตรงกับตำแหน่งกลางของ kernel นั้น

เราลองมาดูการทำ filtering ด้วย average filter เพื่อเบลอรูปกันครับ


import cv2
import numpy as np

img = cv2.imread("lena.jpg")

ksize = 5   #kernel size
#create average kernel
kernel = np.ones((ksize,ksize))/(ksize*ksize)
ddepth = -1     #depth of output image, -1 is same as input image
out = cv2.filter2D(img,-1,kernel)
#same as: out = cv2.blur(img,(ksize,ksize))

cv2.imshow("Origin",img)
cv2.imshow("Result",out)
cv2.waitKey()
cv2.destroyAllWindows()

ถ้าต้องการเขียนโค้ดเอง สมมติว่าเป็นรูปสีเทา และคำนวณเฉพาะพิกเซลที่ตำแหน่งของ kernel ยังอยู่ในรูป
import cv2
import numpy as np

img = cv2.imread("lena.jpg", cv2.CV_LOAD_IMAGE_GRAYSCALE)

ksize = 5   #kernel size, odd value
#create average kernel
kernel = np.ones((ksize,ksize))/(ksize*ksize)

hksize = ksize%2    #half of kernel size
row,column = img.shape[:2]
out = img.copy()
for r in range(row-ksize+1):
    for c in range(column-ksize+1):
        temp = kernel*img[r:r+ksize,c:c+ksize]
        pix = temp.sum()
        out[r+hksize,c+hksize] = np.uint8(pix)

cv2.imshow("Origin",img)
cv2.imshow("Result",out)
cv2.waitKey()
cv2.destroyAllWindows()

ต้องระวังบรรทัด out[r+hksize,c+hksize] = np.uint8(pix) ให้ดีครับ เพราะถ้าไม่ใช่ average filter ในบรรทัดนี้อาจจะต้องทำการ clip/normalize ค่าพิกเซลให้อยู่ในช่วง 0-255 ด้วย

ถ้าเป็นรูปสี BGR ก็ต้องเพิ่มอีก 1 loop สำหรับ channel

hksize = ksize%2    #half of kernel size
row,column,channel = img.shape
out = img.copy()
for k in range(channel):
    for r in range(row-ksize+1):
        for c in range(column-ksize+1):
            temp = kernel*img[r:r+ksize,c:c+ksize,k]
            pix = temp.sum()
            out[r+hksize,c+hksize,k] = np.uint8(pix)

วิธีนี้จะใช้เวลาค่อนข้างมากครับ แต่ทำให้เราสามารถประยุกต์ใช้ kernel ตามที่เราต้องการได้

Thresholding การเปลี่ยนรูปเทาให้เป็นขาวดำ

ทำได้หลายวิธีครับ ในที่นี้เราจะใช้วิธีง่ายๆโดยการกำหนดค่า threshold ซึ่งถ้าค่าพิกเซลมากกว่าหรือเท่ากับค่านี้ก็จะเปลี่ยนให้เป็นสีขาว (255) มิฉะนั้นก็เปลี่ยนเป็นสีดำ (0)

import cv2

img = cv2.imread("lena.jpg",cv2.CV_LOAD_IMAGE_GRAYSCALE)
thres = 128
ret,bw = cv2.threshold(img,thres,255,cv2.THRESH_BINARY)

cv2.imshow("Original",img)
cv2.imshow("Binary",bw)

cv2.waitKey()
cv2.destroyAllWindows()

หรือจะใช้เทคนิคของ NumPy โดยเปลี่ยนโค้ดข้างต้นเป็น
import cv2

img = cv2.imread("lena.jpg",cv2.CV_LOAD_IMAGE_GRAYSCALE)
thres = 128
bw = 255*np.ones_like(img)
bw[img < thres] = 0

cv2.imshow("Original",img)
cv2.imshow("Binary",bw)

cv2.waitKey()
cv2.destroyAllWindows()

จะได้ผลลัพธ์เหมือนกันครับ

การแยกแต่ละ channel ของรูป

ภาพที่อ่านจาก OpenCV ด้วยคำสั่่ง cv2.imread() จะมีลักษณะตามปกติเป็น
  • NumPy Array
  • มี 1,2,3,4 channels
  • มีค่าพิกเซลเป็น uint8
  • รูปสีจะเป็น BGR
ในการเข้าถึงแต่ละพิกเซล ถ้าเป็นรูปขาวดำ หรือสีเทา (channel=1) เราทำได้โดย
img[row,column]

ถ้าเป็นรูปสี BGR (channel = 3) เราจะใช้
img[row,column,channel]

ซึ่งแตกต่างกับ array 3 มิติเล็กน้อยที่จะใช้ img[channel,row,column]

ถ้าเราต้องการเฉพาะ channel ใดๆ ก็สามารถแยกออกมาได้ เช่น
blue = img[...,0]            #same as img[:,:,0]
ตัวแปร blue ก็จะอ้างถึง channel สีน้ำเงินในรูป
หากเราเปลี่ยนค่าตัวแปร blue รูปต้นฉบับจะเปลี่ยนตาม

หากไม่ต้องการให้รูปต้นฉบับเปลี่ยน ต้องทำสำเนาไป เช่น
blue = img[...,0].copy()

ลองดูตัวอย่างครับ

import cv2
import numpy as np

img = cv2.imread('lena.jpg')

blue = img[...,0].copy()
green = img[...,1].copy()
red = img[...,2].copy()

cv2.imshow("Blue",blue)
cv2.imshow("Red",red)
cv2.imshow("Green",green)
cv2.waitKey()
cv2.destroyAllWindows()

หากต้องการให้เห็นแต่ละสีในสีนั้นๆ

ก็ปรับโค้ดเล็กน้อยครับ
import cv2
import numpy as np

img = cv2.imread('lena.jpg')

blue = np.zeros_like(img)
blue[...,0] = img[...,0].copy()
green = np.zeros_like(img)
green[...,1] = img[...,1].copy()
red = np.zeros_like(img)
red[...,2] = img[...,2].copy()

cv2.imshow("Blue",blue)
cv2.imshow("Red",red)
cv2.imshow("Green",green)
cv2.waitKey()
cv2.destroyAllWindows()

หรือ
import cv2
import numpy as np

img = cv2.imread('lena.jpg')

blue = img.copy()
blue[...,1:3] = 0
green = img.copy()
green[...,0:3:2] = 0
red = img.copy()
red[...,0:2] = 0

cv2.imshow("Blue",blue)
cv2.imshow("Red",red)
cv2.imshow("Green",green)
cv2.waitKey()
cv2.destroyAllWindows()

OpenCV ก็มีคำสั่งสำเร็จรูปสำหรับการแยก channel คือ คำสั่ง split() ลองดูตัวอย่างการใช้งานครับ
import cv2
import numpy as np

img = cv2.imread("lena.jpg")
out = cv2.split(img)

cv2.imshow("Blue",out[0])
cv2.imshow("Green",out[1])
cv2.imshow("Red",out[2])
cv2.waitKey()
cv2.destroyAllWindows()

ก็จะได้ผลลัพธ์เหมือนตัวอย่างแรกครับ

Brightness และ Contrast

เราลองมาปรับค่า brightness โดยการบวกเพิ่มค่าพิกเซล และค่า contrast โดยการคูณเพิ่มค่าพิกเซล ของรูปสีเทา ให้ได้ผลดังนี้ครับ


เราจะใช้คำสั่ง cv2.add() และ cv2.multiply() ซึ่งคำสั่งทั้งสองจะ clamp หรือ saturate พิกเซล (ถ้าค่าต่ำกว่า 0 ให้เป็น 0 และ ถ้าค่าสูงกว่า 255 ให้เป็น 255) โดยอัตโนมัติครับ

โค้ดประมาณนี้ครับ
import cv2

#read image as gray
img = cv2.imread("lena.jpg",cv2.CV_LOAD_IMAGE_GRAYSCALE)

#increase brightness
bri = cv2.add(img,100)
#increase contrast
con = cv2.multiply(img,1.5)

cv2.imshow("Original",img)
cv2.imshow("Brightness",bri)
cv2.imshow("Contrast",con)

cv2.waitKey()
cv2.destroyAllWindows()

ในกรณีที่เป็นรูปสี ที่มีหลาย channel ผมลองโค้ดเดิมแล้วผลลัพธ์ไม่ถูกต้องครับ มันบวกหรือคูณแค่ channel เดียว เลยพยามเปลี่ยนโค้ดให้เป็นเมตริกซ์บวกหรือคูณกัน (ซึ่งอาจจะมีวิธีที่ดีกว่า) ดังนี้ครับ
import cv2
import numpy as np

img = cv2.imread("lena.jpg")

#brightness
a = 100*np.ones_like(img)
bri = cv2.add(img,a)

#contrast

b = np.zeros_like(img)
con = cv2.scaleAdd(img,1.5,b)   #1.5*img + 0


cv2.imshow("Original",img)
cv2.imshow("Brightness",bri)
cv2.imshow("Contrast",con)

cv2.waitKey()
cv2.destroyAllWindows()

ผลลัพธ์

อย่างไรก็ตาม เราสามารถใช้เทคนิคของ NumPy Array มาทำงานนี้ได้ ดังตัวอย่างต่อไปนี้ครับ ซึ่งใช้ได้กับทั้งรูปสีเทาและสีปกติ
import cv2
import numpy as np

img = cv2.imread("lena.jpg")

#increase brightness
bri = img+100.0
#clip in range 0-255
bri = np.clip(bri,0,255)
#convert to uint8
bri = np.uint8(bri)

#increase contrast
con = img*1.5
con = np.clip(con,0,255)
con = np.uint8(con)

cv2.imshow("Original",img)
cv2.imshow("Brightness",bri)
cv2.imshow("Contrast",con)

cv2.waitKey()
cv2.destroyAllWindows()

สังเกตว่า bri = img+100.0 ตัวเลขที่เอาไปบวกมีทศนิยมด้วย เพื่อเปลี่ยนให้ค่า Array จาก uint8 เป็น float เนื่องจาก ถ้าใช้แค่ bri = img+100 ค่าพิกเซลที่เกิน 255 จะถูกทอนค่าลง เช่น 256 ก็จะกลายเป็น 1 (256%255) ทำให้ได้ค่าที่ไม่ถูกต้องครับ