Nick Untitled

Writing as my personal diary

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

Published: 08 December 2021
Share:

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

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

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

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

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

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 Architecture ภาพโครงสร้าง 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 ภาพโครงสร้าง U-NET ในขั้นตอน Contract Path

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

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

Upsampling Path

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

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

  1. Up Sampling แล้วตามด้วย 2x2 Convolutions (หรือเรียกอีกอย่างว่า “up-convolution”) ที่แบ่งครึ่งจำนวน Channels
  2. นำภาพที่ได้จากขั้นตอนการทำ 3x3 Convolutions + ReLU ใน Contract Path ที่ผ่านการ Cropped แล้วมา Concatenate
  3. ทำ 3x3 Convolutions สองรอบ แล้วตามด้วย ReLU
  4. ทำไปจนกระทั่งถึง Layer สุดท้าย เราทำ 1x1 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 Dataset ตัวอย่างรูปภาพในฐานข้อมูล 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)

Face Detection ภาพจากเว็บ Wikipedia

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

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

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

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 โดยใช้ภาพเราถือบัตรเองที่อัพโหลดเข้าไประบบ จะได้ภาพตามด้านล่างนี้ครับ

Input Colab Image ภาพที่จะใช้ทำ 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 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 เราจะได้ผลลัพธ์ตามด้านล่างนี้ครับ

Calibrated Colab Image ภาพหลังการทำ 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)

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

Facial Landmarks

จุดบนอวัยวะบนใบหน้าทั้งหมด 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 Result ผลลัพธ์ที่ได้

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

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