ทำความเข้าใจงาน Segmentation ในโลกการแพทย์
หากคุณกำลังก้าวเข้าสู่สายงาน Machine Learning (การสอนให้คอมพิวเตอร์เรียนรู้จากข้อมูล) ในวงการแพทย์ คุณอาจเคยได้ยินเรื่อง Segmentation (การแบ่งส่วนภาพเพื่อระบุขอบเขตวัตถุ) มาบ้าง งานนี้คือการสอนให้ AI ระบุว่าพื้นที่ไหนในภาพถ่ายทางการแพทย์คือเนื้องอก ซึ่งเป็นงานที่ท้าทายมากสำหรับมือใหม่
โดยปกติแล้ว รังสีแพทย์จะทำเครื่องหมายเนื้องอกบนภาพ CT Scan (ภาพถ่ายเอกซเรย์คอมพิวเตอร์แบบสามมิติ) ด้วยการลากเส้นตรงผ่านจุดที่กว้างที่สุด เรียกว่า RECIST (เกณฑ์การประเมินผลการรักษาในก้อนมะเร็ง) ซึ่งเป็นมาตรฐานที่ใช้กันทั่วโลก แต่เส้นตรงนี้บอกแค่ความกว้าง ไม่ได้บอกรูปร่างสามมิติที่แท้จริง
เป้าหมายของเราในบทความนี้คือการเปลี่ยน เส้น RECIST (เส้นบอกขนาดเนื้องอกแบบสองมิติ) ให้กลายเป็น 3D Segmentation Mask (หน้ากากภาพสามมิติ) ที่ครอบคลุมเนื้อเยื่อทั้งหมด โดยใช้ระบบที่ชื่อว่า Lumina ซึ่งเป็นวิธีประหยัดเวลาและแม่นยำกว่าการให้คนมานั่งวาดขอบเขตทีละชั้นภาพ
เตรียมข้อมูลให้พร้อมก่อนเขียนโค้ด
ก่อนที่เราจะเริ่มเขียนโปรแกรม เราต้องทำความเข้าใจ Dataset (ชุดข้อมูลที่ใช้ฝึก AI) ของเราก่อน ข้อมูลทางการแพทย์มักมาในรูปแบบไฟล์พิเศษ เช่น .npz ซึ่งเป็นไฟล์บีบอัดของ NumPy (ไลบรารีสำหรับจัดการตัวเลขใน Python) ที่เก็บข้อมูลเป็นโครงสร้างแบบอาร์เรย์ (ตารางตัวเลขหลายมิติ)
สิ่งที่มือใหม่มักพลาดคือเรื่อง Coordinate Order (ลำดับของพิกัดตำแหน่ง) ในทางคณิตศาสตร์เราอาจคุ้นเคยกับ (X, Y, Z) แต่ในโลกของโปรแกรมมิ่งและการประมวลผลภาพ NumPy มักใช้ลำดับ (Z, Y, X) ถ้าเราสลับลำดับกัน ข้อมูลจะเพี้ยนทันทีและทำให้การระบุตำแหน่งเนื้องอกผิดพลาดไปจากความเป็นจริง
เราต้องตรวจสอบเสมอว่าข้อมูลมีการปรับ Brightness (ความสว่างของภาพ) มาแล้วหรือยัง ในกรณีของ Lumina ข้อมูลถูกปรับมาให้อยู่ในช่วง 0-255 แล้ว เราจึงไม่ต้องทำขั้นตอน Normalization (การปรับข้อมูลให้อยู่ในสเกลมาตรฐาน) แบบปกติ ให้เขียนฟังก์ชันแปลงพิกัดเพื่อความปลอดภัยดังนี้
import numpy as np
def _to_zyx(vec3, order):
# เปลี่ยนพิกัดจาก XYZ เป็น ZYX เพื่อให้ตรงกับ NumPy
vec3 = np.asarray(vec3, dtype=float).ravel()
if order == "xyz":
return vec3[::-1].copy() # สลับตำแหน่งข้อมูล
if order == "zyx":
return vec3.copy()
raise ValueError("รูปแบบพิกัดไม่ถูกต้อง")
โค้ดนี้ทำหน้าที่รับค่าพิกัดเข้ามาแล้วตรวจสอบว่าอยู่ในรูปแบบไหน ถ้าเป็น xyz เราจะใช้คำสั่ง [::-1] เพื่อกลับลำดับข้อมูลให้กลายเป็น zyx ตามที่ต้องการ ผลลัพธ์ที่ได้คืออาร์เรย์ที่พร้อมนำไปคำนวณตำแหน่งเนื้องอกในพื้นที่สามมิติได้ถูกต้อง
แปลงเส้น RECIST ให้เป็นข้อมูลสอน AI
เมื่อเรามีภาพ CT แล้ว เราต้องบอก AI ว่า "นี่คือจุดที่หมอทำเครื่องหมายไว้" เราจะแปลงเส้น RECIST ให้เป็นข้อมูลที่โมเดลมองเห็นได้ง่ายขึ้น โดยการสร้างสิ่งที่เรียกว่า Heatmap (แผนภาพความร้อนที่แสดงค่าความเป็นไปได้) รอบจุดปลายของเส้นตรงนั้น
เราจะวาดเส้น RECIST ให้มีความหนา 3 พิกเซล และสร้าง Gaussian (ฟังก์ชันความน่าจะเป็นรูปทรงระฆังคว่ำ) รอบจุดปลายทั้งสองข้าง ยิ่งเข้าใกล้จุดปลาย ค่าความเข้มข้นใน Heatmap จะยิ่งสูง ซึ่งช่วยให้ AI โฟกัสไปที่บริเวณเนื้องอกได้แม่นยำกว่าการบอกแค่พิกัดจุดเดียว
มือใหม่ควรระวังเรื่อง Resolution (ความละเอียดของภาพ) อย่าใช้วิธีการย่อขยายภาพ (Resize) เพราะจะทำให้เส้นที่วาดไว้ขาดหายหรือบางเกินไป ให้ใช้วิธีคำนวณจุดปลายใหม่ทุกครั้งที่เปลี่ยนความละเอียดของภาพ เพื่อให้ AI ได้รับข้อมูลที่คงที่และชัดเจนที่สุด
# ตัวอย่างการสร้าง Heatmap รอบจุดปลาย
def _endpoint_heatmap(endpoints_zyx, shape, sigma):
# สร้างพื้นที่ว่างขนาดเท่าภาพ
heat = np.zeros(shape, dtype=np.float32)
# วนลูปวาดค่าความเข้มข้นรอบจุดปลาย
# คำนวณค่าด้วยฟังก์ชัน Gaussian
return heat
ฟังก์ชันนี้จะสร้างตารางตัวเลขขึ้นมาหนึ่งชุดโดยเริ่มจาก 0 แล้วค่อยๆ เติมค่าความน่าจะเป็นลงไปในตำแหน่งที่กำหนด ผลลัพธ์ที่ได้จะเป็นภาพซ้อนทับ (Overlay) ที่บอกใบ้ให้ AI ทราบว่าเนื้องอกเริ่มต้นและสิ้นสุดที่ตรงไหนในภาพ CT
สร้างโมเดล 3D Segmentation
ตอนนี้เรามาถึงขั้นตอนการสร้าง Neural Network (โครงข่ายประสาทเทียม) กันแล้ว เราจะใช้สถาปัตยกรรมที่ออกแบบมาสำหรับงานภาพสามมิติโดยเฉพาะ เพื่อรับข้อมูล 3 ช่องทาง คือ ภาพ CT, เส้น RECIST, และ Heatmap ที่เราสร้างไว้ในขั้นตอนก่อนหน้า
หัวใจสำคัญคือการเลือก Loss Function (ฟังก์ชันวัดความผิดพลาด) ที่เหมาะสม งานนี้เราไม่เพียงแค่ต้องการความแม่นยำของจุด แต่ต้องการความแม่นยำของ Boundary (ขอบเขตของเนื้องอก) ด้วย เราจึงต้องใส่ข้อมูลขอบเขตเข้าไปในสมการเพื่อบังคับให้โมเดลวาดเส้นขอบได้คมชัด
การฝึกโมเดลประเภทนี้ต้องใช้หน่วยความจำสูงมาก ดังนั้นเราควรเริ่มจากชุดข้อมูลขนาดเล็กก่อน เพื่อทดสอบว่า Pipeline (ขั้นตอนการทำงานต่อเนื่อง) ของเราทำงานได้จริงหรือไม่ อย่าเพิ่งรีบโยนข้อมูลทั้งหมดเข้าเครื่อง เพราะอาจทำให้โปรแกรมค้างหรือหน่วยความจำเต็มได้
แปลงผลลัพธ์เป็นหน้ากากเนื้องอก
หลังจากโมเดลทำงานเสร็จ มันจะส่งค่าออกมาเป็น Probability Map (แผนที่แสดงค่าความน่าจะเป็น) ซึ่งเป็นตัวเลขระหว่าง 0 ถึง 1 บอกว่าแต่ละจุดคือเนื้องอกหรือไม่ เราต้องเปลี่ยนตัวเลขเหล่านี้ให้กลายเป็นหน้ากาก (Mask) ที่มีค่าเป็น 0 หรือ 1 เท่านั้น
เราจะใช้เทคนิค Thresholding (การกำหนดเกณฑ์ตัดสิน) เช่น ถ้าค่าความน่าจะเป็นสูงกว่า 0.5 ให้ถือว่าเป็นเนื้องอก นอกจากนี้เราต้องใช้คำสั่ง Connected-component (การหาพื้นที่ที่เชื่อมต่อกัน) เพื่อคัดเลือกเฉพาะส่วนที่เชื่อมต่อกับเส้น RECIST จริงๆ ออกมา เพื่อป้องกันไม่ให้ AI เลือกจุดรบกวนอื่นๆ ในภาพ
เทคนิคนี้สำคัญมากเพราะ AI อาจจะทายผลผิดพลาดในจุดที่ไม่ได้เกี่ยวข้องกับเนื้องอกเป้าหมาย การบังคับให้ผลลัพธ์ต้องเชื่อมกับเส้น RECIST จะช่วยเพิ่มความน่าเชื่อถือให้กับงานวินิจฉัยของแพทย์ได้เป็นอย่างดี
การเพิ่มประสิทธิภาพในการคำนวณ
เนื่องจาก Lumina ถูกออกแบบมาให้รันบน CPU (หน่วยประมวลผลกลาง) ภายใต้ข้อจำกัดเรื่องเวลา 60 วินาที เราจึงต้องจัดการทรัพยากรให้ดี สิ่งที่ทำได้คือการ Pin Thread Count (การล็อกจำนวนการทำงานของหน่วยประมวลผล) เพื่อไม่ให้ระบบดึงทรัพยากรไปใช้เกินความจำเป็น
เราต้องมีการจัดสรร Inference Passes (จำนวนรอบการประมวลผล) ให้เหมาะสม หากภาพไหนมีขนาดใหญ่เกินไป เราอาจต้องทำการแบ่งส่วนภาพออกเป็นชิ้นเล็กๆ (Patching) แล้วค่อยนำมารวมกันใหม่ในภายหลัง เพื่อให้ทำงานได้ภายในเวลาที่กำหนด
การทำโปรเจกต์สายนี้ สิ่งที่สำคัญที่สุดคือการ Measure (การวัดผล) ทุกขั้นตอน เราต้องบันทึกไว้ว่าการปรับแต่งแต่ละอย่างช่วยลดเวลาหรือเพิ่มความแม่นยำได้เท่าไหร่ เพื่อให้เรานำความรู้นี้ไปใช้ในโปรเจกต์ถัดไปได้อย่างมั่นใจ
สรุป: บทเรียนจากการทำโปรเจกต์จริง
การเปลี่ยนเส้น RECIST ให้เป็นหน้ากากสามมิติไม่ใช่แค่เรื่องของโค้ด แต่เป็นเรื่องของการเข้าใจธรรมชาติของข้อมูลทางการแพทย์ สิ่งที่สำคัญที่สุดคือการตรวจสอบข้อมูลก่อนเริ่มงานทุกครั้ง เพราะข้อมูลในโลกจริงมักไม่สะอาดเหมือนในบทเรียน
บทเรียนสำคัญ 3 ข้อที่อยากฝากไว้คือ: (1) Validation Data (ข้อมูลที่ใช้ทดสอบความแม่นยำ) ต้องเป็นตัวแทนของข้อมูลจริงทั้งหมด (2) ต้องรู้ขีดจำกัดของขั้นตอนการเตรียมข้อมูลเสมอ และ (3) ต้องจดบันทึกที่มาของตัวเลขทุกตัวเพื่อให้เราย้อนกลับมาตรวจสอบได้
หากคุณอยากเก่งขึ้น ลองหยิบไฟล์ CT Scan มาทดลองทำตามขั้นตอนเหล่านี้ดูครับ เริ่มจากเขียนฟังก์ชันแปลงพิกัดให้แม่นยำ แล้วค่อยๆ พัฒนาส่วนอื่นๆ ต่อไป การเป็นโปรแกรมเมอร์ที่เก่งไม่ได้เกิดจากการจำโค้ดได้ทั้งหมด แต่เกิดจากการลองผิดลองถูกและแก้ปัญหาจริงด้วยตัวเองครับ
ที่มา: How to Turn a RECIST Line into a 3D Tumor Segmentation Mask — freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More