เจาะลึกการกรองสีในรูป ด้วย OpenCV: ง่ายๆ แต่มือใหม่ก็พลาดได้
บางทีเราก็อยากให้คอมมัน 'เห็น' อะไรที่เฉพาะเจาะจงหน่อยใช่ป่ะ? อย่างพวก 'สี' เนี่ยแหละ ที่โคตรสำคัญในการแยกแยะวัตถุ หรือจะนับของอะไรบางอย่างในรูป เช่น มีกล่องสีเขียวกี่ใบในโกดัง? หรือจะหาผลไม้สุกๆ จากสีของมัน? ง่ายสุดก็เริ่มจาก OpenCV เลย
หัวใจของการกรองสีก็คือ เราต้องบอก OpenCV ว่า 'สี' ที่เราอยากได้เนี่ย มันอยู่ในช่วงไหนของสเปกตรัมสีบ้าง ซึ่งปกติเรามักจะคุ้นกับ RGB (Red, Green, Blue) แต่สำหรับงานพวกกรองสีแบบนี้เนี่ย HSV (Hue, Saturation, Value) มันเวิร์คกว่าเยอะ!
ทำไม HSV ถึงดีกว่า?
- Hue (H): อันนี้แหละคือตัว 'สี' เพียวๆ เลย ไม่ว่าจะแดง เขียว น้ำเงิน มันคือมุมบนวงล้อสี
- Saturation (S): คือความ 'สด' ของสี สีจางๆ หรือสีสดๆ ก็อยู่นี่
- Value (V): คือความ 'สว่าง' สีเข้มๆ หรือสีอ่อนๆ ก็ปรับตรงนี้ได้
ข้อดีคือ ถ้าเราอยากได้สีแดง เราก็แค่กำหนด Hue Range ของสีแดงไปเลย ไม่ต้องห่วงเรื่องความสดหรือความสว่างของแดงนั้นๆ มากนัก ต่างกับ RGB ที่ค่าจะเปลี่ยนไปเยอะมากถ้าความสว่างเปลี่ยนนิดเดียว
เริ่มต้นโค้ด: โหลดรูปและแปลงเป็น HSV
มาดูโค้ดกันเลยดีกว่า ง่ายๆ แค่นี้
import cv2
import numpy as np
# โหลดรูปภาพที่ต้องการจะประมวลผล
# แนะนำให้ใช้รูปที่มีสีที่เราต้องการจะหา จะได้เห็นผลชัดๆ
image_path = 'sample_image.jpg' # อย่าลืมเปลี่ยนชื่อไฟล์รูปด้วยนะ!
img = cv2.imread(image_path)
# เช็คว่าโหลดรูปได้ไหม ถ้าไม่ได้นี่คือ error พื้นฐานเลย
if img is None:
print(f"Error: ไม่เจอรูปที่ {image_path} หรืออ่านไม่ได้")
exit()
# ปรับขนาดรูปหน่อยก็ดีนะ ถ้าไฟล์มันใหญ่เกินไป
# จะได้ไม่กิน RAM เยอะเวลาทำงาน
img = cv2.resize(img, (800, 600)) # ตัวอย่าง ปรับเป็น 800x600 px
# สำคัญมาก! ต้องแปลง BGR เป็น HSV
# OpenCV มันอ่านรูปมาเป็น BGR นะ ไม่ใช่ RGB แบบที่เราคุ้นเคย
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# แสดงรูปต้นฉบับ
cv2.imshow('Original Image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
ข้อควรระวัง (และเคยพลาดมาแล้ว!):
ตอนแรกๆ ผมก็งงเป็นไก่ตาแตกเลยนะ คือโค้ดมันรันได้ไม่มี Error อะไรนะ แต่นั่งดูผลลัพธ์แล้วแบบ 'อ้าวเฮ้ย! สีที่ detect ได้มันเพี้ยนๆ ไปหมดเลยว่ะ' สีฟ้ากลายเป็นแดงบ้างล่ะ แดงกลายเป็นม่วงบ้างล่ะ คือโคตรงง
สาเหตุ? ลืมไปว่า OpenCV มันอ่านรูปมาเป็น BGR (Blue, Green, Red) เว้ย! ไม่ใช่ RGB แบบที่เราคุ้นเคยกันในโปรแกรมแต่งรูปหรือในเว็บทั่วไปเนี่ยแหละ แล้วพอไป cvtColor แต่ลืมระบุ cv2.COLOR_BGR2HSV หรือบางทีเผลอไปใช้ cv2.COLOR_RGB2HSV นี่คือจบเลย สีจะเพี้ยนไปหมด โคตรคลาสสิกอ่ะ!
กำหนด Range สีและสร้าง Mask
พอได้ hsv แล้ว ทีนี้ก็มาถึงการกำหนดช่วงสีที่เราต้องการ จริงๆ แล้วค่า Hue, Saturation, Value เนี่ยมันก็มี Range ของมันนะ
- Hue: 0-179 (ใช่! ไม่ใช่ 0-360 เหมือนที่เราเห็นในโปรแกรมอื่นนะ อันนี้ต้องจำไว้เลย)
- Saturation: 0-255
- Value: 0-255
เราจะใช้ cv2.inRange() เพื่อบอกว่าพิกเซลไหนที่มีค่าสีอยู่ในช่วงที่เรากำหนด
# ตัวอย่าง: หาช่วงสีเขียว (ค่าพวกนี้ต้องลองปรับดูตามสภาพแสงและสีจริงของวัตถุนะ)
# ค่า H, S, V เป็นค่า Low และ High ของสีเขียว
# ค่า H (Hue) ของสีเขียวมันจะอยู่ประมาณ 60 แต่ถ้าอยากได้เขียวแบบกว้างๆ ก็ขยายช่วงหน่อย
# S (Saturation) คือความสด, V (Value) คือความสว่าง
lower_green = np.array([40, 50, 50]) # ค่า HSV ต่ำสุดของสีเขียว
upper_green = np.array([80, 255, 255]) # ค่า HSV สูงสุดของสีเขียว
# สร้าง Mask โดยการบอกว่า Pixel ไหนอยู่ในช่วงสีที่เรากำหนดบ้าง
# ผลลัพธ์ที่ได้จะเป็นภาพขาวดำ (Binary Mask) โดยที่สีขาวคือ Pixel ที่อยู่ใน Range
mask = cv2.inRange(hsv, lower_green, upper_green)
# แสดง Mask ที่ได้
cv2.imshow('Green Mask', mask)
cv2.waitKey(0)
cv2.destroyAllWindows()
เรื่องค่า lower_green กับ upper_green:
ไอ้ตรงนี้แหละที่บางทีก็ปวดหัวนิดนึง เพราะมันไม่มีค่า 'ตายตัว' เป๊ะๆ หรอกนะ มันขึ้นอยู่กับแสงในรูป ความสดของสีวัตถุที่เราถ่ายมา การตั้งค่ากล้อง ฯลฯ
- คำแนะนำคือ: ถ้าอยากหาสีอะไร ลองใช้พวกโปรแกรม Image Editor ที่มันบอกค่า HSV ได้ (เช่น Photoshop, GIMP) แล้วจิ้มๆ สีที่เราต้องการดู แล้วก็เอาค่ามาเป็นจุดเริ่มต้น แล้วค่อยๆ ปรับ
lower_กับupper_ให้มันครอบคลุมสีที่เราต้องการมากที่สุด หรือไม่ก็ใช้โปรแกรมเล็กๆ ที่ช่วยเลือก HSV range ใน OpenCV นั่นแหละ หาใน Google ดูมีเยอะเลย
ปรับปรุง Mask ให้เนียนขึ้น: Morphological Operations
Mask ที่ได้จาก inRange เนี่ย บางทีมันก็มี Noise เล็กๆ น้อยๆ ปนมาบ้าง หรือบางทีวัตถุมันมีช่องโหว่เล็กๆ เราสามารถใช้เทคนิคที่เรียกว่า Morphological Operations มาช่วยได้ หลักๆ ก็มี erode (กัดกร่อน) กับ dilate (ขยาย)
erode: จะช่วยลดขนาดของวัตถุที่เป็นสีขาวใน Mask ลง ทำให้พวกจุด Noise เล็กๆ หายไปdilate: จะช่วยขยายขนาดของวัตถุที่เป็นสีขาวใน Mask ทำให้ช่องโหว่เล็กๆ ในวัตถุเรามันเต็มขึ้น
ปกติเราจะใช้ erode ก่อนเพื่อกำจัด Noise แล้วค่อย dilate เพื่อคืนรูปทรงเดิม หรือถ้าเรียกเป็นศัพท์เฉพาะทางหน่อยก็คือ Opening (Erode แล้ว Dilate) หรือ Closing (Dilate แล้ว Erode) นั่นแหละ
# กำหนด Kernel สำหรับ Morphological Operations
# Kernel คือเมทริกซ์ที่ใช้ในการประมวลผลแต่ละ Pixel
# ตัวเลขใน np.ones() คือขนาดของ Kernel เช่น (5,5) คือ 5x5 Pixel
kernel = np.ones((5,5), np.uint8)
# ใช้ Erode เพื่อกำจัด Noise เล็กๆ น้อยๆ
# iterations คือจำนวนครั้งที่จะทำซ้ำ ยิ่งมากยิ่งกร่อนเยอะ
mask_eroded = cv2.erode(mask, kernel, iterations=1)
# ใช้ Dilate เพื่อเชื่อมต่อส่วนที่ขาดหายไป หรือขยายส่วนที่กร่อนไปแล้วให้กลับมา
mask_dilated = cv2.dilate(mask_eroded, kernel, iterations=1)
# แสดง Mask ที่ปรับปรุงแล้ว
cv2.imshow('Processed Green Mask', mask_dilated)
cv2.waitKey(0)
cv2.destroyAllWindows()
เอา Mask ไปใช้จริง: ไฮไลต์วัตถุ
สุดท้าย เราก็เอา Mask ที่ได้เนี่ยไปประยุกต์ใช้ได้หลายอย่าง เช่น เอาไปไฮไลต์เฉพาะวัตถุที่เรากรองสีมาได้ หรือเอาไปตัดฉากหลังออกไปเลยก็ยังได้
# ใช้ Mask เพื่อเอาเฉพาะส่วนของรูปต้นฉบับที่เป็นสีเขียว
# cv2.bitwise_and คือการ AND ภาพต้นฉบับกับ Mask
# ผลลัพธ์คือ รูปต้นฉบับที่มีเฉพาะส่วนที่ตรงกับสีขาวใน Mask
res = cv2.bitwise_and(img, img, mask=mask_dilated)
# แสดงผลลัพธ์สุดท้าย
cv2.imshow('Detected Green Objects', res)
cv2.waitKey(0)
cv2.destroyAllWindows()
# ปิดหน้าต่างทั้งหมดเมื่อโปรแกรมจบ
cv2.destroyAllWindows()
รวมโค้ดเป็นฟังก์ชัน: ใช้งานง่ายๆ
เพื่อให้มันหยิบไปใช้สะดวก ก็จับยัดใส่ฟังก์ชันไปเลย
import cv2
import numpy as np
def detect_color(image_path, lower_hsv, upper_hsv, resize_to=(800, 600), erode_iter=1, dilate_iter=1):
img = cv2.imread(image_path)
if img is None:
print(f"Error: ไม่เจอรูปที่ {image_path} หรืออ่านไม่ได้")
return None, None
if resize_to:
img = cv2.resize(img, resize_to)
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
# สร้าง Mask
mask = cv2.inRange(hsv, lower_hsv, upper_hsv)
# Morphological Operations เพื่อลด Noise และเติมเต็มช่องว่าง
kernel = np.ones((5,5), np.uint8)
mask = cv2.erode(mask, kernel, iterations=erode_iter)
mask = cv2.dilate(mask, kernel, iterations=dilate_iter)
# เอา Mask ไปประยุกต์ใช้กับรูปต้นฉบับ
result = cv2.bitwise_and(img, img, mask=mask)
return img, mask, result
# --- การใช้งานฟังก์ชัน ---
# ตัวอย่าง: หาช่วงสีน้ำเงิน
# ค่า HSV ของสีน้ำเงินจะอยู่ประมาณ 100-120
lower_blue = np.array([100, 50, 50])
upper_blue = np.array([140, 255, 255])
original_img, blue_mask, blue_result = detect_color(
'sample_image.jpg', # เปลี่ยนชื่อรูปด้วยนะ
lower_blue,
upper_blue,
erode_iter=1,
dilate_iter=2 # ลองเพิ่ม Dilate Iteration ดูเพื่อให้ Mask มันเชื่อมกันดีขึ้น
)
if original_img is not None:
cv2.imshow('Original', original_img)
cv2.imshow('Blue Mask', blue_mask)
cv2.imshow('Blue Objects Detected', blue_result)
cv2.waitKey(0)
cv2.destroyAllWindows()
สรุปส่งท้าย
คือมันไม่ได้ยากอะไรมากเลยนะ แค่ต้องเข้าใจหลักการนิดหน่อย แล้วก็จำไว้ว่า HSV มันดีกว่า BGR สำหรับงานพวกนี้อ่ะแหละ ส่วนไอ้เรื่องค่า Low/High ของ HSV นี่แหละที่บางทีต้องลองผิดลองถูกนิดหน่อย บางทีใช้ Color Picker ช่วยได้เยอะเลย ส่วน Morphological Ops ก็เหมือนเป็นไม้ตายสุดท้ายไว้แก้ปัญหา Mask ไม่เนียนอ่ะนะ
ลองเอาไปปรับใช้ดูนะ มันช่วยให้งานง่ายขึ้นเยอะ ถ้าต้องดีลกับอะไรที่เกี่ยวกับสีในรูปภาพบ่อยๆ!