รูปปกบทความ

1. 🎯 ตอนที่ 4: หัวใจของ OpenCV - รู้จักกับ cv::Mat โกดังเก็บพิกเซลล้านตัว

2. 📖 เปิดฉาก (The Hook)

น้องๆ ทราบไหมครับว่าในยุคแรกๆ ของการทำ Machine Vision สิ่งที่ทำให้โปรแกรมเมอร์ปวดหัวที่สุดไม่ใช่การคิดค้นสมการคณิตศาสตร์เพื่อทำ Feature Extraction หรอกนะครับ แต่คือเรื่อง “Memory Leaks” หรือหน่วยความจำรั่วไหล! สมัยก่อนเวลาเราดึงภาพจากกล้องความละเอียดสูงๆ เข้ามาประมวลผล โปรแกรมมักจะกิน RAM พุ่งกระฉูดแล้วก็แครช (Crash) ดับไปดื้อๆ กลางโรงงานเลย เหตุผลเพราะโครงสร้างข้อมูลยุคเก่ามันบังคับให้เราต้อง “จอง” และ “คืน” พื้นที่หน่วยความจำด้วยตัวเองทุกเฟรม พลาดลืมคืนครั้งเดียวก็พังเลยครับ

โชคดีที่ OpenCV ยุคใหม่ได้ส่งฮีโร่ตัวจริงเข้ามาช่วยกู้สถานการณ์ ฮีโร่ตัวนี้เปรียบเสมือนหัวใจของการทำ Image Processing ในปัจจุบัน มันฉลาด ทรงพลัง และจัดการตัวเองได้ ฮีโร่ตัวนั้นมีชื่อว่า cv::Mat ครับ วันนี้พี่จะขอจิบกาแฟสบายๆ แล้วพาน้องๆ ไปแงะดูเครื่องยนต์เบื้องหลังกันว่า เจ้า cv::Mat คืออะไร ทำไมมันถึงมาแทนที่โครงสร้างยุคเก่าอย่าง IplImage ได้อย่างสมบูรณ์แบบ!

3. 🧠 แก่นวิชา (Core Concepts)

cv::Mat (Matrix) คืออะไร? ในทาง Computer Vision “ภาพ” 1 ภาพ ก็คือตารางตัวเลขขนาดยักษ์ (Matrix) ตัวแปร cv::Mat จึงถูกสร้างขึ้นมาเพื่อเป็น โครงสร้างอาร์เรย์แบบหนาแน่น (Dense n-dimensional array) ที่ใช้เก็บข้อมูลทางคณิตศาสตร์ ไม่ว่าจะเป็นเวกเตอร์, ภาพ Grayscale (ตาราง 2 มิติ), หรือภาพสี BGR (ตาราง 3 มิติ) เปรียบง่ายๆ มันก็คือตาราง Excel ขนาดยักษ์ที่เก็บค่าความสว่างของแสงในแต่ละพิกเซลนั่นเองครับ

ลาก่อน IplImage ยุคแห่งความเหนื่อยล้า ใน OpenCV ยุคแรก (ที่ยังเป็นภาษา C) เราใช้โครงสร้างที่ชื่อว่า IplImage ในการเก็บข้อมูลภาพ ข้อเสียที่ร้ายแรงที่สุดของมันคือเราต้องคอยจัดการหน่วยความจำเอง (Manual Memory Management) ทำให้โค้ดดูวุ่นวายและเสี่ยงต่อการเกิด Bug แต่พอ OpenCV ย้ายมาใช้สถาปัตยกรรม C++ ตัว cv::Mat ก็เข้ามาแทนที่ เพราะมันมีระบบจัดการหน่วยความจำอัตโนมัติ (Automatic Memory Management) ทำให้เราโยนเรื่องปวดหัวทิ้งไปได้เลย

ความลับของสถาปัตยกรรม Header และ Data Pointer เพื่อให้การประมวลผลวิดีโอระดับ 60 FPS ทำงานได้ลื่นไหล cv::Mat ถูกออกแบบให้แบ่งโครงสร้างออกเป็น 2 ส่วนหลักๆ:

  1. Matrix Header: เป็นโครงสร้างขนาดเล็กที่เก็บ “ข้อมูลอธิบายภาพ” (Metadata) เช่น ขนาดภาพ (rows, cols), ชนิดข้อมูล (type เช่น 8-bit, float), และช่องสี (channels)
  2. Data Pointer: เป็นตัวชี้ (Pointer) ที่ชี้ไปยัง “โกดัง” ที่เก็บข้อมูลพิกเซลล้านๆ ตัวจริงๆ ในหน่วยความจำ (RAM)

ประหยัด RAM ขั้นสุดด้วย Reference Counting ถ้าน้องๆ สั่งสมการ Mat B = A; คอมพิวเตอร์ไม่ได้ทำการก๊อปปี้ข้อมูลพิกเซลสิบล้านตัวไปสร้างรูปใหม่นะครับ! สิ่งที่ OpenCV ทำคือ “การก๊อปปี้แค่ Header” แล้วให้ Pointer ของ B ชี้ไปที่โกดังข้อมูลเดียวกับ A ระบบนี้เรียกว่า Reference Counting (การนับจำนวนผู้อ้างอิง) ทุกครั้งที่มีคนชี้ไปที่โกดัง ตัวนับจะบวก 1 และเมื่อฟังก์ชันทำงานเสร็จ หรือตัวแปรถูกทำลาย ระบบจะลบค่าตัวนับลงทีละ 1 ข้อมูลพิกเซลขนาดยักษ์จะถูกลบออกจาก RAM ทิ้งจริงๆ ก็ต่อเมื่อตัวนับเหลือ 0 เท่านั้น (ไม่มีใครใช้งานมันแล้ว)กลไกนี้ทำให้การส่งข้อมูลภาพหากันระหว่างฟังก์ชัน มีความเร็วระดับความเร็วแสง (O(1) complexity) เพราะไม่ต้องก๊อปปี้ Data เลยครับ!

รูปประกอบ

4. 💻 ร่ายมนต์คำสั่ง (Show me the Code)

ลองมาดูโค้ด C++ เพื่อพิสูจน์เวทมนตร์ของการจัดการ Memory ใน cv::Mat กันครับ สังเกตคอมเมนต์ที่พี่อธิบายไว้นะครับ:

#include <opencv2/opencv.hpp>
#include <iostream>

using namespace cv;
using namespace std;

int main() {
    // 1. สร้างภาพต้นฉบับ (สมมติว่าเป็นภาพขนาด 1000x1000 พิกเซล ชนิด 64-bit float)
    // ตรงนี้ระบบจะจอง Memory ขนาดใหญ่ (ประมาณ 8MB)
    Mat A(1000, 1000, CV_64F);
    
    // 2. การสร้างตัวแปร B ให้เท่ากับ A (Reference Counting)
    // การทำงานนี้ไวมาก (O(1) operation) เพราะก๊อปปี้แค่ Header ไม่ได้ก๊อปปี้พิกเซล!
    Mat B = A;
    
    // 3. ลองดึงเฉพาะแถวที่ 3 ของภาพมาไว้ใน C
    // นี่ก็ไม่ได้ก๊อปปี้ Data ข้อมูล C ยังชี้ไปที่โกดังเดียวกับ A และ B
    Mat C = B.row(3);
    
    // 4. การทำ "Deep Copy" (ก๊อปปี้โกดังข้อมูลแยกออกมาเป็นของตัวเอง)
    // ถ้าเราต้องการภาพใหม่ที่ไม่เกี่ยวข้องกันเลย ต้องใช้คำสั่ง .clone() หรือ .copyTo()
    Mat D = B.clone();
    
    // 5. ปล่อยภาพ B ทิ้ง
    // ถึง B จะโดนทำลายไป แต่ข้อมูลพิกเซลยังอยู่ใน RAM เพราะ A และ C ยังชี้ไปที่โกดังนั้นอยู่ (Reference count ยังไม่เป็น 0)
    B.release();
    
    cout << "เวทมนตร์ Reference Counting ทำงานเสร็จสมบูรณ์!" << endl;
    return 0;
}

5. 🛡️ เคล็ดลับจากคัมภีร์ลับ (Under the Hood / Pro-Tips)

  • ระวังกับดักของการแชร์ Memory: เนื่องจาก Mat B = A; เป็นการแชร์โกดังข้อมูลเดียวกัน ถ้าน้องๆ ไปทำ Thresholding หรือวาด Bounding Box ลงบนภาพ B ภาพ A ก็จะโดนขีดเขียนไปด้วยทันที! ถ้าตั้งใจจะปรับแต่งภาพโดยไม่ให้กระทบต้นฉบับ จำไว้เสมอว่าต้องร่ายเวท Mat B = A.clone(); เท่านั้นครับ
  • ROI (Region of Interest): น้องๆ สามารถใช้กรอบสี่เหลี่ยม (เช่น cv::Rect) ในการครอบตัดเฉพาะบางส่วนของภาพได้ (เช่น Mat roi(img, Rect(10,10,100,100));) ข่าวดีคือ การทำ ROI ก็ไม่ได้ก๊อปปี้ Memory ใหม่เช่นกันครับ มันฉลาดมากและช่วยลดภาระ CPU เวลาต้องทำ Object Detection เฉพาะจุดไปได้มหาศาล
  • คำสั่งอำนวยความสะดวก: ถ้าอยากได้ตารางเปล่าๆ ที่มีแต่เลข 0 หรือเลข 1 พี่แนะนำให้ใช้ท่าไม้ตายสไตล์ Matlab อย่าง Mat::zeros() หรือ Mat::ones() ไปเลยครับ เขียนสั้นและอ่านง่ายกว่าเยอะ

6. 🏁 บทสรุป (To be continued…)

เป็นยังไงบ้างครับ กับพระเอกของเราอย่าง cv::Mat เมื่อเราเข้าใจกลไก Header, Data pointer และ Reference Counting แล้ว เราก็สามารถจัดการกับข้อมูลภาพระดับ Full HD หรือ 4K ได้อย่างสบายใจไร้กังวลเรื่อง Memory Leak ครับ

ในตอนต่อไป พี่จะพาน้องๆ มุดลงไปดู “ระดับพิกเซล” กันแบบเจาะลึก เราจะมาดูกันว่า OpenCV มีคำสั่งอะไรให้เราเข้าไปดึงหรือเปลี่ยนค่าสีของจุดพิกเซลแต่ละจุด เพื่อทำ Image Filtering แบบ Custom กันบ้าง รอติดตามได้เลยครับ!


ต้องการที่ปรึกษาด้านการพัฒนาระบบ AI Camera หรือ Machine Vision ให้กับโรงงานของคุณ? ทีมงาน WP Solution พร้อมให้บริการออกแบบและติดตั้งระบบแบบครบวงจร ดูรายละเอียดบริการของเราได้ที่: www.wpsolution2017.com หรือพูดคุยปรึกษาเบื้องต้นได้ที่ Line: wisit.p