Categories
Computer Diary

วิธีการทำ Object Detection โดย Nanodet

Nanodet เป็นเทคนิคการทำ Object detection ที่ประหยัดซีพียู และแรม แต่ยังคงความแม่นยำของการหาตำแหน่งวัตถุในภาพ ในบทความนี้แนะนำการ Training และ Inference

Object detection คือขั้นตอนการหาตำแหน่งวัตถุจากภาพโดย AI ตามที่กำหนดไว้ ได้แก่ คน รถยนต์ จักรยาน และอื่น ๆ โดยผลลัพธ์ที่ได้จากการใช้งานเทคนิคนี้จะแสดงผลในรูปแบบกรอบสี่เหลี่ยม Bounding box พร้อมกับจำแนก Class ของภาพที่จับได้ว่าเป็นอะไร

เทคนิคของการทำ Object detection ที่คนนิยมนำไปใช้งานกันก็ได้แก่ Faster R-CNN, Single shot detector (SSD), YOLO (You Only Look Once), RetinaNet เป็นต้น ซึ่งเทคนิคนี้มีการพัฒนาต่อไปเพื่อเพิ่มความแม่นยำของการจับพื้นที่วัตถุในภาพ อย่างไรก็ดีปัญหาหนึ่งเมื่อนำไปใช้ประยุกต์กับแอพพลิเคชันบนเว็บเบราวเซอร์คือความเร็วในการประมวลผล

เมื่อเทคนิคซับซ้อนมากขึ้น โมเดลมีขนาดใหญ่มากขึ้น ความต้องการคอมพิวเตอร์ที่จะนำมาใช้ประมวลผลเพิ่มขึ้น ส่งผลให้การประมวลผลโดยใช้ซีพียูประมวลผลได้ช้าลง ซึ่งจุดนี้มีงานวิจัยที่พัฒนาเทคนิคให้มีขนาดที่เล็กน้อย มีจำนวนพารามิเตอร์ใน Neural Network ที่ลดลง แต่ยังคงความแม่นยำ หรือยอมลดความแม่นยำของการจับวัตถุในภาพลงบ้าง

เทคนิคหนึ่งที่มีขนาดเล็ก ประมวลผลได้เร็ว แต่ยอมให้ความแม่นยำของการจับวัตถุในภาพลดลงได้บ้าง เทคนิคที่จะกล่าวถึงในบทความนี้คือ Nanodet

Nanodet

โมเดล nanodet (ภาพจาก Github ของโมเดล)

Nanodet เป็นเทคนิคการจับวัตถุในภาพ (Object detection) ที่มีขนาดเล็ก ทำงานได้เร็วจนถึงระดับ Real-time บนอุปกรณ์พกพา และมีขนาดที่เล็ก โดยตามลิ้งค์ใน GitHub แจ้งว่าโมเดลมีขนาด 1.8MB กรณีที่ใช้ Float16

โมเดลนี้เป็นโมเดลที่จัดอยู่ในกลุ่ม Fully Convolutional One-stage Object Detection ที่มีส่วนประกอบหลักทั้งหมด 3 ส่วนได้แก่

  • Backbone เป็นส่วนที่ Extract Feature ออกจากภาพที่เป็น input
  • Feature Pyramid เป็นส่วนที่ปรับตัวโมเดลให้รองรับ Feature ของวัตถุที่มีหลายขนาด
  • Head เป็นส่วนทำนายผลลัพธ์ โดยจะทำนายในรูปแบบ Bounding box, Confidence หรืออื่น ๆ

ส่วนโมเดล Nanodet หรือ Nanodet-Plus นี้ใช้ส่วนประกอบ 3 ส่วนตามที่กล่าวถึงแล้วในย่อหน้าข้างบน โดยโมเดลนี้ได้ปรับแต่งให้โมเดลสามารถจับวัตถุในภาพได้เร็ว แต่ยังรักษาความแม่นยำได้โดย

  • Backbone ที่ใช้งาน ShuffleNetV2
  • Feature Pyramid ใช้โมดูล PAFPN (Path Aggregation Feature Pyramid Network) ที่ได้รับการนำเสนอในเทคนิค PANet โมดูลนี้เป็นตัวโมดูลที่มีหน้าที่จับลักษณะของภาพ (Feature) ที่รับผลลัพธ์จากแต่ละ Layer ใน Backbone โดยจุดที่แตกต่างจาก FPN ปกติคือ
    • โมดูลนี้จะรับข้อมูล Low-level information จาก Layer ลำดับต้น ๆ ของ Backbone ที่ข้อมูลรายละเอียดของ Edge, corner รวมถึง Shape Feature ที่ส่งผลให้โมดูลนี้ช่วยจับวัตถุในภาพได้แม่นยำขึ้น
    • สำหรับรายละเอียดโมดูลนี้อ่านได้ที่เปเปอร์นี้ครับ
  • Head เป็นส่วนที่ทำนายผลลัพธ์ของ input โดยแสดงออกเป็น bounding box และ confidence value

นอกเหนือจากนี้ โมเดล Nanodet-Plus ยังเพิ่ม

  • Generalized Focal Loss สำหรับ Bounding Box และ Confidence
  • Assign guidance module (AGM) และ dynamic soft label assigner (DSLA) สำหรับการปรับ label ของภาพให้เหมาะสมต่อการ train ของโมเดลโดยผ่านการแก้ปัญหา optimal label assignment เพื่อให้โมเดลสามารถตรวจจับวัตถุในภาพได้แม่นยำขึ้นมากกว่าเดิม

Installation

การติดตั้งโมเดลนี้ทำได้โดย

โคลน Github repo มาไว้ในโฟลเดอร์ที่ต้องการด้วยการใช้คำสั่ง

git clone https://github.com/RangiLyu/nanodet.git

ติดตั้งไลบรารีที่จำเป็นโดยติดตั้ง PyTorch, torchvision

pip install torch, torchvision

ติดตั้งไลบรารีที่จำเป็น โดยเข้าไปโฟลเดอร์เดียวกันกับ Nanodet ที่โคลนมาแล้ว

pip3 install -r requirements.txt

ติดตั้ง Nanodet

python setup.py develop

Dataset Preparation

การเตรียมชุดข้อมูลสำหรับการฝึกโมเดล Nanodet หรือ Nanodet-Plus สามารถทำได้โดยจัดระเบียบไฟล์และโฟลเดอร์ของภาพแต่ละภาพในชุดข้อมูล โดยแบ่งเป็นชุดข้อมูล Training และ Validation อันนี้เราทำได้โดยการสร้างโฟลเดอร์ Training และ Validation จากนั้นนำรูปภาพที่มีอยู่ในชุดข้อมูลมาแบ่ง และใส่ในโฟลเดอร์ที่สร้างขึ้น

ต่อมาเราเตรียมเตรียมไฟล์ Annotation ให้อยู่ในรูปแบบ COCO Annotation Format นั้น เราสามารถเตรียมไฟล์ตามรูปแบบที่ปรากฏตามด้านล่างนี้ โดยเซฟเป็นไฟล์ JSON (JavaScript Object Notation)

{
    "images": [< แสดงข้อมูลของแต่ละภาพ >], 
    "annotations": [< แสดงข้อมูลที่ label ในแต่ละภาพ >], 
    "categories": [< แสดงรายละเอียดของ class >],
    "info": < แสดงรายละเอียดของชุดข้อมูล >, 
    "licenses": [< แสดงข้อมูล license >],
}

เมื่อมาดูที่รายละเอียดของไฟล์นี้ จะแบ่งออกเป็นสี่ส่วนได้แก่ images, annotations, categories, info และ licenses

images

ส่วนนี้เป็นส่วนอาเรย์ของข้อมูลในแต่ละภาพ รายละเอียดแสดงตามด้านล่างนี้ครับ

image{
    "id": < id ของภาพ >, 
    "width": < ความกว้าง >, 
    "height": < ความสูง >, 
    "file_name": < ชื่อไฟล์ >, 
    "license": < id ของ license (ดูรายะเอียดการเขียนได้ที่หัวข้อ license ส่วนนี้ไม่บังคับ) >, 
    "flickr_url": < url ของ flickr (ส่วนนี้ไม่บังคับ) >, 
    "coco_url": < url ของ coco (ส่วนนี้ไม่บังคับ) >, 
    "date_captured": <วันที่และเวลาของภาพที่เก็บภาพนี้ได้ (ส่วนนี้ไม่บังคับ) >,
}

annotations

ส่วนนี้แสดงรายละเอียดของ label ในแต่ละภาพ โดยแสดงตำแหน่ง bounding box, แสดงขนาดพื้นที่ของ bounding box ในหน่วย pixel และระบุ class ของวัตถุในกรอบนั้น ๆ

การ label ของ Object detection, Semantic Segmentation, Pose Estimation จะมีรายละเอียดที่แตกต่างกัน ส่วนนี้ผู้อ่านสามารถอ่านได้ในเว็บของ COCO

annotation {
    "id": < id ของ label >, 
    "image_id": < id ของภาพ ดูรายละเอียดได้ที่หัวข้อ images >, 
    "category_id": < id ของ class >, 
    "area": < พื้นที่ของ bounding box >, 
    "bbox": < bounding box โดยเขียนในรูปแบบ [x,y,width,height] โดย x,y เป็นตำแหน่งมุมบนซ้ายของ bounding box ที่ต้องการ label ส่วน width และ height เป็นความกว้าง และความสูง >, 
    "iscrowd": < ระบุว่าการ label นี้เป็นการ label ของวัตถุหนึ่งชิ้น (เขียนด้วย 0) หรือ label กลุ่มของวัตถุนั้น ๆ (เขียนด้วย 1) โดยในบทความนี้จะเป็นการ label ของวัตถุหนึ่งชิ้นที่แทนด้วย 0 ครับ > ,
}

categories

ส่วนนี้เป็นการระบุรายละเอียดในแต่ละ class สำหรับการทำ Object detection โดยตัวอย่าง class ที่ระบุได้แก่ คน จักรยาน ต้นไม้ เป็นต้น

categories[{
    "id": < id ของ class >, 
    "name": < ชื่อ class >, 
    "supercategory": < ชื่อกลุ่มของ class >,
}]

info

info เป็นการแสดงรายละเอียดของชุดข้อมูลที่เราเตรียมขึ้นมาสำหรับการฝึก ตรวจสอบ หรือทดสอบโมเดลที่เราต้องการ สำหรับรายละเอียดส่วนนี้แสดงตามด้านล่างนี้ครับ

info {
    "year": < แสดงปีของชุดข้อมูลนี้ >, 
    "version": < version ของชุดข้อมูล >, 
    "description": < คำอธิบาย >, 
    "contributor": < ผู้สนับสนุน หรือผู้จัดทำชุดข้อมูล >, 
    "url": < ที่อยู่ของชุดข้อมูล >, 
    "date_created": < วันที่และเวลาที่จัดทำชุดข้อมูล >,
}

ส่วนนี้ไม่บังคับว่าต้องมีในไฟล์ Annotations ผู้อ่านไม่จำเป็นต้องกรอกครับ แต่ถ้ากรอกเพื่อเป็นข้อมูลก็จะทำให้คนที่เข้ามาดูไฟล์ Annotations แล้วทราบรายละเอียดของชุดข้อมูลนี้ครับ

licenses

ส่วนนี้แสดงลิขสิทธิ์ของแต่ละภาพที่อยู่ในชุดข้อมูล รายละเอียดแสดงตามด้านล่างนี้

ส่วนนี้ไม่ได้บังคับว่าต้องมีครับ ผู้อ่านไม่จำเป็นต้องใส่ license ลงไปในไฟล์​ Annotations ครับ

license{
    "id": < id ของ license >, 
    "name": < ชื่อ license >, 
    "url": < url >,
}

ตัวอย่าง

ตัวอย่างของการเขียนไฟล์ Annotation สำหรับชุดข้อมูลเพื่อทำ Object Detection บริเวณปากโดยใช้ชุดข้อมูล CelebA-MaskHQ มีรายละเอียดตามด้านล่างนี้ครับ

{"categories": [
    {"id": 0, "name": "mouth", "supercategory": "organ"}
    ], 
 "images": [
    {
         "id": 0, 
         "file_name": "7259.jpg", 
         "height": 640, "width": 640
    }, 
    {
         "id": 1, 
         "file_name": "726.jpg", 
         "height": 640, "width": 640
    }, 
    {
         "id": 2, 
         "file_name": "7260.jpg", 
         "height": 640, "width": 640
    } ...],
 "annotations": [
    {
        "id": 0, 
        "image_id": 0, 
        "category_id": 0, 
        "bbox": [246, 242, 171, 119], 
        "area": 10370, 
        "iscrowd": 0
    }, 
    {
        "id": 1, 
        "image_id": 1, 
        "category_id": 0, 
        "bbox": [230, 218, 182, 143], 
        "area": 13260, 
        "iscrowd": 0
    }, 
    {
        "id": 2, 
        "image_id": 2, 
        "category_id": 0, 
        "bbox": [238, 250, 172, 133], 
        "area": 11685, 
        "iscrowd": 0
    }, ...]
}

ข้อมูล Annotation แบ่งออกเป็น 3 ส่วน ได้แก่

  • csategories แสดงข้อมูลของแต่ละ class โดยในตัวอย่างมี 1 class ที่ต้องการคือปาก
  • images แสดงรายละเอียดข้อมูลของแต่ละภาพที่นำมาใช้เป็นชุดข้อมูลสำหรับการทำ Training
  • annotations แสดงรายละเอียด Label ของแต่ละภาพ

การตั้งค่าการ Training และ Validation

การตั้งค่าสำหรับการทำ Training และ Validation อันนี้ผู้อ่านทำได้โดยการเข้าไปในโฟลเดอร์ config เมื่อเข้ามาแล้วจะปรากฏไฟล์การตั้งค่าที่อยู่ในโฟลเดอร์นั้น แต่ละไฟล์จะมีชื่อที่แตกต่างกันไปขึ้นกับโมเดล Nanodet ที่ใช้สำหรับการ Training และ Validation

การเลือกใช้โมเดล ผู้อ่านสามารถก็อปปี้ไฟล์โมเดลได้ตามที่ต้องการ และวางไว้ในโฟลเดอร์นั้น จากนั้นตั้งค่าโดยทำตามด้านล่างนี้

  1. เปลี่ยนที่อยู่ save_dir สำหรับการเก็บไฟล์ snapshot สำหรับการ Training โมเดล
  2. เปลี่ยน num_classes ในหัวข้อ model -> arch -> head กับ aux_head ให้เข้ากันได้กับจำนวน classes ที่เราระบุไว้ในไฟล์ Annotations
  3. เปลี่ยนตำแหน่งไฟล์ภาพ และไฟล์ Annotations ในหัวข้อ data -> train และ val ตรงส่วน img_path, ann_path
  4. กำหนด GPU, batch size และจำนวน Worker ได้ที่หัวข้อ device
  5. กำหนดรายการระบุชื่อ Classes ได้ที่หัวข้อ class_names

นอกเหนือจากการตั้งค่าตามด้านบนนี้แล้ว ผู้อ่านสามารถตั้งค่าในไฟล์นั้นเพิ่มเติมได้อีกว่าต้องการระบุ

  • input_size
  • data pipeline สำหรับ training และ validation เพื่อทำ data augmentation และอื่น ๆ ที่หัวข้อ data -> train หรือ val -> pipeline
  • Optimizer กับ learning rate และ weight decay ที่หัวข้อ schedule -> optimizer
  • จำนวน Epoch ตรงหัวข้อ schedule -> total_epochs
  • และอื่น ๆ อันนี้ผู้อ่านสามารถเช็คได้ในไฟล์ config แต่ละไฟล์ครับ

Training

การฝึกโมเดลสามารถทำได้โดยการพิมพ์คำสั่งตามด้านล่างนี้ครับ

python tools/train.py < ตำแหน่งไฟล์ config ที่เราตั้งค่า >

เมื่อพิมพ์คำสั่งนี้แล้ว กดปุ่ม Enter ระบบจะ Train ตัวโมเดลจนถึงจำนวนรอบ Epoch ที่เราระบุไว้ครับ

Demo

กรณีที่ต้องการทดลองใช้โมเดลที่สร้างขึ้น ผู้อ่านสามารถพิมพ์คำสั่งตามด้านล่างนี้ได้ครับ โดยแบ่งตาม input ของภาพที่ต้องการ

อันแรก กรณีที่ input เป็นไฟล์ภาพ

python demo/demo.py image --config < ตำแหน่งไฟล์ config ที่เราสร้างขึ้น > --model < ตำแหน่งไฟล์โมเดลที่ผ่านการ Training> --path < ตำแหน่งไฟล์ภาพ >

อันต่อมา กรณีที่ input เป็นไฟล์วิดีโอ

python demo/demo.py video --config < ตำแหน่งไฟล์ config ที่เราสร้างขึ้น > --model < ตำแหน่งไฟล์โมเดลที่ผ่านการ Training> --path < ตำแหน่งไฟล์วิดีโอ >

อันสุดท้าย กรณีที่ input เป็น webcam

python demo/demo.py webcam --config < ตำแหน่งไฟล์ config ที่เราสร้างขึ้น > --model < ตำแหน่งไฟล์โมเดลที่ผ่านการ Training> --camid < id ของกล้อง >

Inference

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

นำเข้าไลบรารีที่จำเป็น โดยนำเข้าไลบรารี PyTorch, OpenCV, OS และไบรารี nanodet

import os, cv2, torch

from nanodet.data.batch_process import stack_batch_img
from nanodet.data.collate import naive_collate
from nanodet.data.transform import Pipeline
from nanodet.model.arch import build_model
from nanodet.util import Logger, cfg, load_config, load_model_weight
from nanodet.util.path import mkdir

นำเข้าไฟล์ Config

load_config(cfg, < ตำแหน่งไฟล์ config ที่เราบันทึกไว้ >)
logger = Logger(-1, use_tensorboard=False)

นำเข้าโมเดลที่ผ่านการทำ Training

model = build_model(cfg.model)
ckpt = torch.load(< ตำแหน่งไฟล์โมเดลที่ผ่าน Training >, map_location=lambda storage, loc: storage)
load_model_weight(model, ckpt, logger)

ตั้งค่าให้ใช้การ์ดจอสำหรับการตรวจจับภาพ และตั้งค่าให้โมเดลไม่ได้ทำ Training

device = torch.device('cuda')
model = model.to(device).eval()

ตรวจจับวัตถุในภาพ

def inference(cfg, model, img, device):
    # นำเข้าภาพ และตั้งค่ารายละเอียดของภาพก่อนการทำ Object detection
    img_info = {"id": 0}
    img_info["file_name"] = os.path.basename(img)
    img = cv2.imread(img)
    height, width = img.shape[:2]
    img_info["height"] = height
    img_info["width"] = width
    meta = dict(img_info=img_info, raw_img=img, img=img)

    # กำหนด data pipeline สำหรับการทำ preprocess ภาพ โดยดึงข้อมูลจากไฟล์การตั้งค่าที่เราได้สร้างไว้
    pipeline = Pipeline(cfg.data.val.pipeline, cfg.data.val.keep_ratio)

    # Preprocess
    meta = pipeline(None, meta, cfg.data.val.input_size)

    # แปลงให้อยู่ในรูปตัวแปร PyTorch tensor
    meta["img"] = torch.from_numpy(meta["img"].transpose(2, 0, 1)).to(device)
    meta = naive_collate([meta])
    meta["img"] = stack_batch_img(meta["img"], divisible=32)

    # ทำ Object detection
    with torch.no_grad():
        results = model.inference(meta)

    # คืนค่าผลลัพธ์
    return meta, results
meta, res = inference(cfg, model, < ตำแหน่งภาพที่เราต้องการให้ทำ Object detection >, device)

แสดงผลลัพธ์ที่ได้ด้วยการวาด Bounding box

from nanodet.util import overlay_bbox_cv

result = overlay_bbox_cv(meta['raw_img'][0], res[0], cfg.class_names, score_thresh=< กำหนดค่า Confidence ขั้นต่ำสำหรับการวาด Bounding box โดยกำหนดไว้ที่ 0.35 >)

เมื่อวาดเสร็จแล้ว การแสดงผลลัพธ์ผู้อ่านสามารถใช้ไลบรารี matplotlib, OpenCV ผ่านการใช้ฟังก์ชัน imshow หรืออื่น ๆ ได้ครับ โดยในบทความนี้จะใช้ matplotlib

from matplotlib import pyplot as plt

plt.imshow(cv2.cvtColor(result, cv2.COLOR_BGR2RGB))
plt.show()

เมื่อพิมพ์โค้ดเสร็จแล้ว ทดลองรันโมเดลนี้ ตัวโค้ดจะแสดงผลลัพธ์ของการทำ Object Detection ครับ

ในบทความจะทดลองทำภาพของ Tony Woodsome มาครอบบริเวณครึ่งล่างของใบหน้าแล้วทดลองรันโมเดล ผลลัพธ์ที่ได้จะแสดงตามด้านล่างนี้ครับ

ตัวอย่างผลลัพธ์การทำ Object Detection โดยให้ค้นหา Bounding Box บริเวณปากของ Tony Woodsome

Deployment

เพื่อฝึกโมเดลนี้เรียบร้อย ผู้อ่านสามารถนำโมเดลไปใช้งานต่อได้ โดยสามารถส่งออกโมเดลเป็น ONNX, NCNN, OpenVINO หรือใช้งานบน Android ได้ครับ

รายละเอียดของการส่งออกโมเดลนี้อ่านได้ที่ Github repo ของโมเดลนี้ครับ

By Kittisak Chotikkakamthorn

My name is Nick (or Kittisak Chotikkakamthorn) who is working as a research assistant in biomedical engineering, Mahidol University. I research artificial intelligence in medicine about computer vision, which I have to write the code. It is the skill that I am practicing.