ตอนที่ 4: หัวใจของ OpenCV - รู้จักกับ cv::Mat

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 ส่วนหลักๆ:
- Matrix Header: เป็นโครงสร้างขนาดเล็กที่เก็บ “ข้อมูลอธิบายภาพ” (Metadata) เช่น ขนาดภาพ (rows, cols), ชนิดข้อมูล (type เช่น 8-bit, float), และช่องสี (channels)
- 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