วันพฤหัสบดีที่ 11 ตุลาคม พ.ศ. 2561

หลักการทำงานของโค้ด Smart Mirror & Hand Detection ( Part 1 ส่วนการเปิดหน้าเว็บและการรับค่าจากกล้อง Kinect )

ไฟล์การทำงานจะถูกแบ่งออกเป็น 3 ส่วนคือ
1. keymap.py ที่จะทำหน้าที่เปิดหน้าเว็บ และรับข้อมูล input จาก kinect มา map กับปุ่มของ keyboard เพื่อสั่งการ
2. kinect_hand.py ทำหน้าที่รับข้อมูลจากกล้อง kinect มาทำการประมวลผลและตรวจสอบว่าแต่ละ blob เป็นมือหรือไม่ และมีการเคลื่อนไหวอย่างไร เพื่อส่งผลที่ได้ไปยัง keymap
3. index.html เป็นไฟล์หน้าเว็บที่เอาไว้แสดงบน smart mirror

โดยการทำงานเริ่มแรกนั้นจะเริ่มจาก keymap.py

  • ขั้นแรกจะทำการ import library สำคัญๆ ทั้ง os, pynput.keyboard, time, threading รวมไปถึง kinect_hand.py เพื่อใช้ฟังค์ชันภายในไฟล์
  • เป็นการทำงานโดยแบ่ง Thread ออกเป็น 2 ตัว คือ Thread ของเว็บ และ Thread ของการทำ keyboard mapping
  • Thread ของเว็บจะเริ่มทำงานก่อนโดยทำการเปิดหน้าเว็บจากไฟล์ index.html โดยใช้ฟังค์ชัน webbrowser.open
  • Thread ของการทำ keyboard mapping จะเริ่มทำงานต่อจากการเปิดเว็บ
  • สร้างตัวแปรชื่อ keyboard ทำหน้าที่เป็น controller เพื่อสั่งการเหมือนการกดปุ่มบน keyboard
  • ใช้ os.system("sleep 5") เพื่อให้รอ 5 วินาทีก่อนทำงานขั้นถัดไป
  • keyboard.press(Key.f11) และ keyboard.release(Key.f11) ใช้แทนการกดและปล่อยปุ่ม f11 เพื่อให้หน้าจอเข้าสู่โหมด fullscreen
  • กำหนดค่า fps เริ่มต้นเป็น 0
  • ให้ last gesture หรือท่าการเคลื่อนไหวสุดท้ายเป็น "undefined action" หรือการเคลื่อนไหวที่ระบุไม่ได้
  • เก็บค่า t0 เป็นเวลาเริ่มต้นจาก time.clock() จากนั้นจึงเข้าสู่ Loop
  • ตัวแปร input จะทำหน้าที่เสมือนกับไฟล์ kinect_hand โดยในที่นี้จะเรียกฟังค์ชัน get_input() เพื่อทำการเก็บข้อมูลจากกล้อง Kinect
ขั้นต่อมาจะอยู่ในส่วนการทำงานของ kinect_hand.py
  • ก่อนจะทำงานในส่วนของฟังค์ชัน get_input() ในไฟล์จะทำการ import library สำหรับทำการคำนวณและจัดการเกี่ยวกับ image processing โดยเฉพาะเช่น numpy, openCV2, math, pygame, time และ freenect สำหรับรับข้อมูลจากกล้อง Kinect
  • ทำการประกาศตัวแปรหลักๆคือ xsize, ysize, blobs สำหรับเก็บข้อมูล, buffer_size = 3, blobs_movement ที่เก็บค่าในรูปของ dict หรือ {} ส่วน blob_buffer และ old_id จะมีลักษณะเป็น
    [[]] * buffer_size ซึ่งจะได้ array 1 ตัว ที่ภายในบรรจุ array ไว้ค่าของ buffer_size ซึ่งในที่นี้คือ 3 โดยมีหน้าตาดังนี้

  • ในฟังค์ชัน get_input() จะทำการเรียกใช้ xsize และ ysize แบบ pass by reference และส่งค่าที่เรียกมาไปฟังค์ชัน pygame_refresh(xsize ,ysize)
  • ในฟังค์ชัน pygame_refresh() จะทำการเรียกใช้ตัวแปรอื่นๆแบบ pass by reference คือ blobs, blob_buffer, old_id และ blobs_movement โดยจะทำการล้างค่าใน blobs ทุกครั้งเพื่อเตรียมรับค่าใหม่ ( blobs = [] )
  • ต่อมาจะทำการเรียกฟังค์ชัน update_old_id() โดยเรียกใช้ old_id มาและนำเข้า Loop แรกซึ่งวนตามจำนวน buffer_size คือ 3 และทำการล้างค่าใน array old_id ที่ i ก่อนเข้า Loop ที่ 2 ที่วนตามความกว้างของ blobs_buffer ตัวที่ i จากนั้นทำการนำ id ของ blobs_buffer ชุดที่ i ตัวที่ j
    ( blobs_buffer[i][j].id ) ไปใส่ใน old_id ตัวที่ i โดยการทำงานคร่าวๆจะมีลักษณะและผลคล้ายรูปดังนี้

  • ต่อไปจะเรียกใช้ฟังค์ชัน get_contour เพื่อเก็บค่าไว้ในตัวแปร cs โดยส่ง xsize และ ysize ไปด้วย
  • ใน get_contour จะทำงานเริ่มจากไปนำข้อมูลจากกล้อง Kinect จากคำสั่ง sync_get_depth() มาเก็บไว้ในตัวแปร depth
  • แปลงข้อมูลให้เป็น float32 จากคำสั่ง astype(np.float32)
  • กลับภาพตามแนวแกน y โดยใช้คำสั่ง cv2.flip(depth, 1) ถ้าเปลี่ยนจาก 1 เป็น 0 จะเป็นหมุนตามแนวแกน x ถ้าเป็น -1 จะหมุนทั้ง 2 แกน
  • ทำการปรับขนาดของภาพตามค่า xsize และ ysize จาก cv2.resize(depth,(xsize,ysize))
  • ทำการเบอลภาพและลบ gaussian noise ในภาพโดยใช้คำสั่ง cv2.GaussianBlur(depth, (5,5), 0) โดย (5,5) คือความกว้างและความสูงของ Kernel size ตามลำดับ และ 0 คือค่าเบี่ยงเบนมาตรฐานของ gaussian kernel ซึ่งจะมีทั้งของแกน X และ Y หากให้ Y เป็น 0 ค่าของ X จะเป็น 0 เช่นกัน และคำนวณโดยใช้ Kernel size เป็นเกณฑ์แทน โดยภาพซ้ายและขวาจะเป็นภาพก่อนและหลังทำ gaussian blur ตามลำดับ

  • ทำการตัดขอบของวัตถุในภาพเพื่อลบ Noise โดยใช้คำสั่ง cv2.erode(depth, None, iterations=1) โดย None คือไม่ใส่ kernel ในการคำนวณและทำซ้ำเพียง 1 รอบ ( iterations=1 )
  • ทำการเพิ่มขนาดของวัตถุที่ถูกตัดขอบไปและเพิ่มจุดที่เสียหายจาก Noise โดยใช้คำสั่ง cv2.dilate(depth, None, iterations=1) ซึ่งภาพที่ได้จะมีลักษณะดังนี้ ตั้งแต่ก่อนทำ erode หลังทำ erode และทำ dilate ตามลำดับ
  • หาค่าความลึกของมือที่ต่ำที่สุด ( min_hand_depth ) โดยใช้คำสั่ง np.amin โดยจะดึงค่าที่น้อยที่สุดใน array ออกมาลบด้วย 10
  • หาค่า max_hand_depth โดยเอา min_hand_depth มาบวกกับค่า hand_depth ซึ่งกำหนดไว้ที่ 80
  • จากนั้นจะทำการเช็คเงื่อนไข หาก max_hand_depth มีค่ามากกว่า 700 จะกำหนดให้ค่า max_hand_depth = 700
  • จากนั้นจะทำการแปลงข้อมูลโดยใช้คำสั่ง cv2.threshold(depth, max_hand_depth, min_hand_depth, cv2.THRESH_BINARY_INV) เพื่อเปลี่ยนให้ค่าที่น้อยกว่า max_hand_depth หรือ Threshold value ให้กลายเป็นค่าสีที่เรากำหนดหรือค่า min_hand_depth ซึ่งใช้ THRESH_BINARY_INV

    การทำ THRESH_BINARY_INV คือการ invert ข้อมูลโดยเปลี่ยนให้ค่าที่มากกว่า Threshold value ให้กลายเป็น 0 หรือสีดำทั้งหมด ส่วนค่าที่น้อยกว่า Threshold value จะเปลี่ยนเป็นค่าสีที่เรากำหนดซึ่งอยู่ในช่วง [ 0 - 255 ] สามารถดูเปรียนเทียบกับแบบปกติได้ดังนี้ ( ในรูปให้ค่า Threshold อยู่กึ่งกลางพอดี )

  • ทำการแปลงค่าที่ได้หลังจากทำ Threshold ให้อยู่ในช่วงข้อมูล 8-bit [ 0 - 255] โดยใช้คำสั่ง
    cv2.convertScaleAbs(BW) 
  • นำค่า Threshold ไปทำการหาเส้นขอบของภาพหรือ Contour ของภาพจากคำสั่ง
    cv2.findContours(BW,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) โดย RETR_TREE เป็น mode ที่ทำการเก็บข้อมูลทั้งหมดมาทำเป็น contour และ CHAIN_APPROX_SIMPLE คือ method ที่จะเอาเฉพาะจุด End Point จากข้อมูล contour ทั้งหมดเช่น มุมของภาพดังรูป

  • ข้อมูลที่ทำ contour จะเก็บไว้ในตัวแปร cs และสร้าง array cs_f เพื่อเตรียมขั้นตอนสุดท้าย
  • นำข้อมูล cs เข้า Loop โดยวนตามจำนวนข้อมูลใน cs และเช็คเงื่อนไขว่า ข้อมูล cs ตัวที่ i ตัวใดมีค่า contour area มากกว่า 500 ให้บรรจุเข้าไปใน array cs_f จากโค้ดด้านล่าง

  • ลบข้อมูลใน array depth และ BW ทั้งหมดจากคำสั่ง depth,BW และ return array cs_f  ไปยังตัวแปร cs ที่เรียกในฟังค์ชัน pygame_refresh

>>> Continue Part 2
แหล่งอ้างอิง

contour : https://docs.opencv.org/2.4/modules/imgproc/doc/structural_analysis_and_shape_descriptors.html
https://docs.opencv.org/3.3.1/d4/d73/tutorial_py_contours_begin.html

convertScaleAbs : https://docs.opencv.org/2.4/modules/core/doc/operations_on_arrays.html

Threshold :
https://medium.com/@a5730051/threshold-opencv-%E0%B8%84%E0%B8%B7%E0%B8%AD%E0%B8%AD%E0%B8%B0%E0%B9%84%E0%B8%A3-768214f155eb
https://skconann.blogspot.com/2018/02/image-simple-thresholding.html

Erosion & Dilation :
https://wiki.morange.co.th/Morphological_Transformations

GaussianBlur :
https://docs.opencv.org/3.1.0/d4/d13/tutorial_py_filtering.html

Flip :
https://docs.opencv.org/3.4.2/d2/de8/group__core__array.html#gaca7be533e3dac7feb70fc60635adf441

โค้ดส่วนที่ใช้งาน

kinect_hand.py


def get_contours(xsize,ysize):
    (depth,_) = get_depth()
    depth = depth.astype(np.float32)
    depth = cv2.flip(depth, 1)
    depth = cv2.resize(depth,(xsize,ysize))
    depth = cv2.GaussianBlur(depth, (5,5), 0)
    depth = cv2.erode(depth, None, iterations=1)
    depth = cv2.dilate(depth, None, iterations=1)
    min_hand_depth = np.amin(depth)-10
    hand_depth = 80
    max_hand_depth = min_hand_depth + hand_depth
    if max_hand_depth > 700 :
        max_hand_depth = 700
    (_,BW) = cv2.threshold(depth, max_hand_depth, min_hand_depth, cv2.THRESH_BINARY_INV)
    BW = cv2.convertScaleAbs(BW)
    #BW = cv2.resize(BW,(xsize,ysize))
    cs,_ = cv2.findContours(BW,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
    cs_f = []
    for i in range(len(cs)):
        if cv2.contourArea(cs[i]) > 500:
            cs_f.append(cs[i])
    del depth,BW
    return cs_f

blobs = []
buffer_size = 3
blobs_buffer = [[]] * buffer_size
old_id = [[]] * buffer_size
blobs_movement = {}

def update_old_id():
    global old_id
    for i in range(buffer_size):
        old_id[i] = []
        for j in range(len(blobs_buffer[i])):
            old_id[i].append(blobs_buffer[i][j].id)

def pygame_refresh(xsize,ysize):
    global blobs
    blobs = []
    global blobs_buffer
    global old_id
    global blobs_movement
    update_old_id()
    cs = get_contours(xsize,ysize)
    for i in range(len(cs)):
        blob = BlobAnalysis(cs[i])
        blob = blobs_track(blob,i,0)
        blobs.append(blob)
    for i in range(buffer_size):
        if i == 0:
            blobs_buffer[i] = blobs
        else:
            blobs_buffer[i] = blobs_buffer[i-1]
    return 1

xsize,ysize = 280,210

def get_input():
    global xsize,ysize
    pygame_refresh(xsize,ysize)

ไม่มีความคิดเห็น:

แสดงความคิดเห็น