Categories
Computer Data

วัดระยะห่างระหว่างตาดำจากภาพโดยภาษา Python

อันนี้เป็นส่วนหนึ่งของงานวิจัย ทำไปแล้วบางส่วน

ปกติการวัดตาดำ เราจะพบได้ในคนที่เลือกขนาดเครื่อง Virtual Reality Headset หรือวัดขนาดแว่นตา หรืออื่น ๆ ปกติเราจะใช้ไม้บรรทัดวัดเพื่อให้รู้ว่าระยะห่างระหว่างตาดำ (Interpupillary Distance) มีระยะห่างเท่าไร อย่างไรก็ดีเราจะใช้ไม้บรรทัดวัดไปตลอดเหรอก็ไม่สะดวกเท่าไร แถมสมัยนี้เราก็ใช้คอมพิวเตอร์กันอยู่แล้วด้วย เลยเอามาเขียนโค้ดส่วนนี้เพื่อจับระยะการอ้าปากครับ

หลักการวัดจากภาพดิจิทัล โดยปกติเวลาที่เราวัดจะได้หน่วยการวัดเป็น pixel แต่สิ่งที่เราต้องการก็คือ ต้องการการวัดที่มีหน่วยเป็นเซนติเมตร หรือมิลลิเมตรที่ตัวโปรแกรมวัดด้วยตัวเองไม่ได้ เราจำเป็นต้องหาวัตถุอ้างอิงเพื่อเป็น Reference สำหรับการแปลงหน่วยจาก pixel เป็นหน่วยที่เราวัดครับ

ในตัวอย่างนี้ เราจะใช้บัตรประชาชนซึ่งเป็นสิ่งที่คนทุกคนมีกันอยู่แล้ว (ยกเว้นเด็กเล็ก) เป็นวัตถุ Reference ใช้สำหรับการวัดในครั้งนี้ ขนาดของบัตรประชาชน (ไทย) มีขนาดที่เป็นมาตรฐาน โดนมีขนาดด้านยาว 86 mm ด้านกว้าง 54 mm เราจะใช้ด้านยาวเป็น Reference

แต่ก่อนที่จะไปวัด เราจะต้องแยกส่วน (Segment) บัตรประชาชนออกจากวัตถุอื่นในภาพก่อน แล้วจะทำอย่างไรดี หลักการนี้เรียกว่า Image Segmentation

Image Segmentation

ภาพตัวอย่างการ Segmentation จากเปเปอร์​ Mask R-CNN

Image Segmentation เป็นหลักการจำแนก pixel ของวัตถุที่เราต้องการออกมาจากวัตถุอื่นในภาพดิจิทัล โดยยกตัวอย่างเช่นระบบการขับรถอัตโนมัติ (self-driving car) ที่จับคนในภาพเพื่อป้องกันไม่ให้เกิดอุบัติเหตุครับ หลักการนี้แบ่งออกมาได้เป็นสองวิธีได้แก่ Semantic Segmentation และ Instance Segmentation

  • Semantic Segmentation เป็นการแยกวัตถุออกจากภาพวัตถุอื่นโดยการแบ่งประเภทของวัตถุ (class) จากภาพ ได้แก่ สีแดงเป็นคน สีน้ำเงินเป็นรถ เป็นต้น
  • Instance Segmentation เป็นการแบ่งวัตถุแต่ละชิ้นในภาพ ที่แตกต่างกับ Semantic Segmentation ที่แบ่งเป็นวัตถุที่ 1,2,3,4 เป็นต้น

ตัวอย่างของเทคนิคที่ใช้ Image Segmentation คือ Mask R-CNN, U-NET ครับ

U-NET

ภาพโครงสร้าง U-NET

U-NET ที่เป็นคนละอันกันกับ O-NET ที่สอบมัธยมศึกษาตอนปลายเข้ามหาวิทยาลัยที่จัดโดยสทศ ครับ

U-NET เป็นโครงข่ายประสาทเทียม (Neural Network Architecture) แบบ Convolutional Neural Network ที่ให้ผลลัพธ์เป็น Matrix ที่มีขนาดกว้าง x ยาวเท่ากันกับภาพเดิม โดยในแต่ละตำแหน่งจะระบุได้ว่าเป็น 0 (ไม่มีภาพ Object) หรือ 1 (มี Object) ในภาพ

โครงสร้างของเครือข่ายประสาท U-NET เราจะเห็นว่าเป็นรูปตัว U (U-shape) ที่แบ่งเป็นสองช่วง ได้แก่ Contracting Path (ด้านซ้าย) และ Upsampling Path (ด้านขวา) เราจะอธิบายในแต่ละส่วนตามด้านล่างนี้ครับ

Contract Path

ภาพโครงสร้าง U-NET ในขั้นตอน Contract Path

ประกอบไปด้วย

  1. 3×3 Convolutions (ที่ไม่มี Padding) 2 รอบ
  2. ตามมาด้วย Activation Function ReLU
  3. ใช้ 2×2 Max Pooling ที่มี Stride 2 เพื่อ Down Sampling ระหว่างที่ทำ Down Sampling เราจะเพิ่มจำนวน Feature Channel เป็น 2 เท่าในแต่ละครั้ง

Upsampling Path

ภาพโครงสร้าง U-NET ในขั้นตอน Upsampling Path

ประกอบไปด้วย

  1. Up Sampling แล้วตามด้วย 2×2 Convolutions (หรือเรียกอีกอย่างว่า “up-convolution”) ที่แบ่งครึ่งจำนวน Channels
  2. นำภาพที่ได้จากขั้นตอนการทำ 3×3 Convolutions + ReLU ใน Contract Path ที่ผ่านการ Cropped แล้วมา Concatenate
  3. ทำ 3×3 Convolutions สองรอบ แล้วตามด้วย ReLU
  4. ทำไปจนกระทั่งถึง Layer สุดท้าย เราทำ 1×1 Convolution เพื่อนำ 64 Component Feature Vector ให้เป็นจำนวน Classes ที่เราต้องการ

จำนวน Convolutional Layer ทั้งหมดที่ใช้ใน U-NET มีทั้งหมด 23 Layers ครับ

สำหรับข้อมูลเพิ่มเติมของ U-NET ผู้อ่านศึกษาได้ในเปเปอร์ U-Net: Convolutional Networks for Biomedical Image Segmentation จากเว็บ arXiv ครับ

MIDV-500

ตัวอย่างรูปภาพในฐานข้อมูล MIDV-500

MIDV-500 หรือเรียกอีกอย่างว่า Mobile Identity Document Video dataset ที่ประกอบไปด้วยวิดีโอ 500 ชิ้นที่มีเอกสารยืนยันตัวตนทั้งหมด 50 ชนิดที่สร้างขึ้นโดยใช้กล้องโทรศัพท์มือถืออย่าง iPhone 5, Samsung Galaxy S3 ที่บันทึกในสภาพแวดล้อม 5 รูปแบบ ได้แก่ ภาพวางบนโต๊ะ บนคีย์บอร์ด และบนมือ ภาพที่ถูกบังบางส่วน รวมถึงภาพที่มีพื้นหลังทีมีวัตถุต่าง ๆ เต็มหน้าจอที่ไม่เกี่ยวข้อง

ในภาพที่บันทึกได้ จะไม่มีข้อมูลสำคัญ หรือข้อมูลที่ไว้ก็อปปี้สำหรับการทำบัตรปลอมได้ จุดนี้เป็นปัญหาสำคัญที่ไม่มีฐานข้อมูลในลักษณะนี้มาก่อนครับ

สำหรับผู้อ่านที่ต้องการอ่านเพิ่มเติม สามารถอ่านได้ในเปเปอร์ MIDV-500: a dataset for identity document analysis and recognition on mobile devices in video stream จากเว็บ arXiv ครับ และกรณีที่ต้องการดาวน์โหลดฐานข้อมูลไว้ใช้งาน สามารถดาวน์โหลดได้ที่ Github หรือดาวน์โหลดผ่านการติดตั้งไลบรารีใน pip ของไพทอน โดยพิมพ์คำสั่ง

pip install midv500

ตัวไพทอนจะติดตั้งไลบรารี MIDV-500 ไว้ใช้งาน ตัวไลบรารีสามารถแปลงข้อมูล Annotation ของฐานข้อมูล MIDV-500 ให้อยู่ในรูปแบบของ COCO instance segmentation format

นอกเหนือจากนี้ ฐานข้อมูลได้รับการพัฒนาขึ้นมาให้เป็นเวอร์ชันใหม่ โดยฐานข้อมูลที่สร้างขึ้นมาใหม่มีชื่อว่า MIDV-2019 ครับ ฐานข้อมูลนี้แก้ปัญหาเรื่อง Projective Distortion และสภาพแสงสว่างที่แตกต่างกันไป

เขียนโค้ดกัน

เราเขียนโค้ดเพื่อที่จะวัดระยะห่างระหว่างตาดำ การเขียนโค้ดจะมีขั้นตอนดังนี้

  1. จับภาพใบหน้า (Face detection)
  2. Calibrate ระยะการวัด Pixel ต่อ mm โดยแยกส่วนบัตรประชาชนจากภาพ โดยให้ถือบัตรประชาชนให้ชิดริมฝีปากของผู้ที่ต้องการวัดภาพ
  3. จับภาพจุดแลนมาร์คบริเวณดวงตา หรือตาดำ (Facial Landmark Detection)
  4. วัดระยะห่างระหว่างตาดำ (Interpupillary Distance)

เราเขียนโค้ดใน Google Colab ได้เลยครับ ผู้อ่านสามารถดาวน์โหลดไฟล์ ipynb มาทดลองรันบน Google Colab หรืออื่น ๆ ได้ครับ

จับภาพใบหน้า (Face Detection)

ภาพจากเว็บ Wikipedia

การจับภาพใบหน้า หรือเรียกอีกอย่างว่า Face Detection คือการหาตำแหน่ง Face Regions of Interest จากภาพ โดยมีหลายเทคนิคที่เราสามารถใช้ได้เลย ตั้งแต่ Viola-Jones ที่พบได้ในคำสั่งบน OpenCV ที่คนโพสกันไปเยอะมาก หรือใช้เทคนิค dlib หรืออื่น ๆ ครับ อย่างไรก็ดี เราต้องพิจารณาความแม่นยำ ข้อดี ข้อเสียของแต่ละเทคนิค

ในที่นี้ จะใช้เทคนิค FaceBoxes ครับ

การจับจุดแลนมาร์คบนใบหน้า (Facial Landmark Detection)

ภาพจากเว็บ Wikipedia

การจับจุดแลนมาร์คบนใบหน้า หรือเรียกอีกอย่างว่า Facial Landmark Detection เป็นการจับตำแหน่งอวัยวะบนใบหน้าเพื่อใช้สำหรับการประมวลผลในขั้นตอนต่อไป มีหลายเทคนิคที่ใช้ ตั้งแต่รุ่นเก่าเลยก็เป็น Active Appearance Models, Constrained Local Models หรืออื่น ๆ แต่ถ้าเอาง่ายหน่อยก็เป็น dlib (จากเปเปอร์ Ensemble of Regression Trees) หรือ FaceMesh หรืออื่น ๆ

ในตัวอย่าง เราใช้เทคนิค 3DDFA_V2 ครับ

Calibrate ระยะการวัด Pixel และหาระยะห่างระหว่างตาดำ

เอาล่ะ มาเขียนโค้ดกันดีกว่า เราติดตั้งไลบรารีที่จำเป็นโดยการใช้ pip แต่สำหรับการทำ Calibrate เราใช้ไลบรารี

  • OpenCV
  • Numpy
  • PyTorch
  • iglovikov_helper_functions
  • midv500models
  • imutils

ติดตั้งเสร็จแล้ว อัพโหลดภาพเข้า Google Colab จากนั้นนำเข้าภาพโดยใช้คำสั่ง

img = cv2.imread("< Image Path >")

รันใน Google Colab โดยใช้ภาพเราถือบัตรเองที่อัพโหลดเข้าไประบบ จะได้ภาพตามด้านล่างนี้ครับ

ภาพที่จะใช้ทำ Calibrate และหาระยะห่างระหว่างตาดำทั้งสองข้าง

ต่อมา เรานำภาพผ่านการจับใบหน้าโดยใช้เทคนิค Face Detection และอะไรก็ได้เพื่อหา Face Regions of Interest ครับ โดยการใช้คำสั่งตามด้านล่างนี้

img = img[..., ::-1]
boxes = face_boxes(img)

ต่อมาเรานำ Face Regions of Interest (ที่อยู่ในตัวแปร boxes) ไปประยุกต์ใช้ต่อสำหรับการทำ Calibrate เรานำเข้าไลบรารีได้ตามด้านล่างนี้

import albumentations as albu
import torch from iglovikov_helper_functions.utils.image_utils
import load_rgb, pad, unpad from iglovikov_helper_functions.dl.pytorch.utils
import tensor_from_rgb_image from midv500models.pre_trained_models
import create_model

ดาวน์โหลดโมเดล U-NET มาใช้งาน โดยใช้คำสั่งตามด้านล่างนี้

model = create_model("Unet_resnet34_2020-05-19")

กำหนดตัวโมเดลสำหรับการจับภาพที่ไม่ได้เทรนใหม่ (Evaluation model) และนำภาพที่เราจับภาพใบหน้ามาแล้วที่เป็นตำแหน่งแรกมาแยกส่วนบัตรประชาชน เราพิมพ์โค้ดได้ตามด้านล่างนี้

model.eval()
boxes = [boxes[0]]
size_box = len(boxes)
box = boxes[0][x1, y1, x2, y2] = box[:4]x1 = int(x1)y1 = int(y1)x2 = int(x2)y2 = int(y2)# Segment Imageimage = model.eval()
boxes = [boxes[0]]
size_box = len(boxes)
box = boxes[0]

[x1, y1, x2, y2] = box[:4]
x1 = int(x1)
y1 = int(y1)
x2 = int(x2)
y2 = int(y2)

# Segment Image
image = img[y1:y2,x1:x2].copy()
transform = albu.Compose([albu.Normalize(p=1)], p=1)
padded_image, pads = pad(image, factor=32, border=cv2.BORDER_CONSTANT)

# Inference
x = transform(image=padded_image)["image"]
x = torch.unsqueeze(tensor_from_rgb_image(x), 0)
with torch.no_grad():
    prediction = model(x)[0][0]

# Postprocessing
mask = (prediction > 0).cpu().numpy().astype(np.uint8)
mask = unpad(mask, pads)
mask = mask * 255

เราแยกส่วนบัตรประชาชนออกมาแล้วในรูปแบบตัวแปร mask ในตัวโค้ดจะออกมาเป็นอาเรย์ที่มีขนาดเท่าภาพต้นฉบับที่มีตัวแปรระหว่าง 0 กับ 1 แต่ภาพขาวดำปกติมันมีตั้งแต่ 0-255 (8-bit) เรานำ 255 มาคูณตามด้านบน

หลังจากแยกส่วนแล้ว เราต้องการหาความยาวด้านยาวเป็นจำนวน Pixel เราจำเป็นต้องหา Contour ของภาพ โดยใช้คำสั่ง cv2.findContours แล้วเรียง Contour จากบนลงล่าง

# Contour
contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
(cnts, boundingBoxes) = contour.sort_contours(contours, method="top-to-bottom")

เมื่อเรียงมาเรียบร้อยแล้ว เราหาความยาวด้านยาวของบัตรประชาชนที่แยกมาได้ โดยใช้คำสั่ง cv2.boundingRect

# find boundingRect
target_cnt = cnts[0]
x,y,w,h = cv2.boundingRect(target_cnt)

เราจะได้ความยาวมาเรียบร้อย เราแปลงให้อยู่ในรูปอัตราส่วนจำนวน pixel ต่อ mm ได้โดย

# Card size in pixels (compare to 86mm on long size)
distance = w
distancepermm = distance / 86
print(f"card size (pixel) = { distance } compare to (mm) = 85 mm => distance { distancepermm } pixels/mm")

รันใน Google Colab เราจะได้ผลลัพธ์ตามด้านล่างนี้ครับ

ภาพหลังการทำ Calibrate

ต่อมา ในขั้นตอนต่อไป เป็นการหาตำแหน่งอวัยวะบนใบหน้า (Facial Landmark Detection) เราเอาภาพเดิมที่ผ่านการจับภาพใบหน้า (Face Detection) มาใช้งาน ได้ตามโค้ดด้านล่างนี้ที่ใช้เทคนิค 3DDFA_V2 ครับ

param_lst, roi_box_lst = tddfa(img, boxes)
ver_lst = tddfa.recon_vers(param_lst, roi_box_lst, dense_flag=False)

เราจะได้จุดบนใบหน้า 68 จุดออกมา แต่อาเรย์ของ ver_lst อยู่ในรูปแบบอาเรย์ที่มี Shape = [3, 68] เราจำเป็นต้องแปลงให้อยู่ในรูปแบบ Shape = [68, 3] เสียก่อน ได้โดย

x = first_landmark[0,:].reshape(-1, 1)
y = first_landmark[1,:].reshape(-1, 1)
landmark = np.concatenate([x,y], axis = 1)

แล้ว เราจำเป็นต้องหาตำแหน่งตาดำตรงกลางเพื่อหาระยะห่างระหว่างตาดำ แต่ก่อนอื่น เรามาหาตำแหน่งรอบตาดำ และตาขาวก่อน จากภาพนี้

จุดบนอวัยวะบนใบหน้าทั้งหมด 68 จุด

เรานำจุดบนอวัยวะบนใบหน้าตำแหน่งที่ 37-42 สำหรับตาขวา และตำแหน่งที่ 43-48 สำหรับตาซ้าย เพื่อนำมาหาตำแหน่งตาดำตรงกลางสำหรับการหาระยะห่างระหว่างตาดำ ได้ตามด้านล่างนี้

righteye = landmark[36:42]
rightiris = np.array([np.mean(righteye[:, 0]), np.mean(righteye[:, 1])])
lefteye = landmark[42:48]
leftiris = np.array([np.mean(lefteye[:, 0]), np.mean(lefteye[:, 1])])

ต่อมา เราหาระยะห่างระหว่างตาดำทั้งสองข้างได้โดยนำค่า pixel ของภาพต่อระยะห่างที่เป็นหน่วย mm ที่ได้จากการทำ Calibrate มาใช้งานตามโค้ดด้านล่างนี้

# Find IPD
interpupillary_distance = 0.0
for i in range(2):
    interpupillary_distance += (rightiris[i] - leftiris[i])**2
interpupillary_distance = np.sqrt(interpupillary_distance)
interpupillary_distance_mm = interpupillary_distance / distancepermm
print(f"Interpupillary Distance = { interpupillary_distance } which equals = { interpupillary_distance_mm } mm")

ทดลองเริ่มต้นการทำงาน จะได้ผลลัพธ์ตามด้านล่างนี้ครับ

ผลลัพธ์ที่ได้

ฟังดูแล้วไม่ยากเกินไปใช่ไหมล่ะครับ สำหรับผู้อ่านวิธีการวัดระยะห่างระหว่างตาดำ (Interpupillary Distance) วิธีนี้เป็นวิธีหนึ่งแค่นั้นครับ

และอีกอย่าง เทคนิคนี้ยังไม่ได้ทดสอบความแม่นยำกับคนอื่น ๆ (ยกเว้นผู้เขียนเอง) เลยอาจจะต้องเอาไปทดสอบก่อนที่จะนำไปใช้งานบน Production จริงครับ

By Kittisak Chotikkakamthorn

อดีตนักศึกษาฝึกงานทางด้าน AI ที่ภาควิชาวิศวกรรมไฟฟ้า มหาวิทยาลัย National Chung Cheng ที่ไต้หวัน ที่กำลังหางานทางด้าน Data Engineer ที่มีความสนใจทางด้าน Data, Coding และ Blogging / ติดต่อได้ที่: contact [at] nickuntitled.com

Exit mobile version