ตอนที่ 8: การเข้าถึงพิกเซล (Pixel Access) ใน C++ อย่างถูกวิธี

1. 🎯 ตอนที่ 8: การเข้าถึงพิกเซล (Pixel Access) ใน C++ อย่างถูกวิธี เพื่อรีดสปีดขั้นสุด!
2. 📖 เปิดฉาก (The Hook)
น้องๆ เคยเขียนโค้ดทำ Image Filter ของตัวเองไหมครับ? สมมติว่า Rule-based Vision ธรรมดาเอาไม่อยู่ เราอยากจะเขียนสมการปรับค่าแสงเฉพาะจุด หรือทำ Custom Thresholding ด้วยตัวเอง พอเขียน Nested for loop วนเช็กพิกเซลทีละจุดปุ๊บ ภาพจากกล้องหน้างานที่เคยรันลื่นๆ 30 FPS กลับกระตุกร่วงลงมาเหลือ 2 FPS กลายเป็นสไลด์โชว์ไปซะงั้น!
ปัญหานี้ไม่ได้อยู่ที่คอมพิวเตอร์ในโรงงานเราช้าครับ แต่อยู่ที่เราเลือกวิธี “เข้าถึงข้อมูลพิกเซล (Pixel Access)” ผิดวิธี! การเข้าถึง Image Matrix ขนาดยักษ์ผิดท่า ก็เหมือนเราเดินเข้าไปในโกดังเพื่อหยิบของทีละชิ้นแล้วเดินกลับออกมา กว่าจะครบสิบล้านชิ้นก็หมดแรงพอดี วันนี้พี่จะมาชงกาแฟ แล้วพาน้องๆ ไปดูเคล็ดวิชาการล้วงแคะแกะเกาข้อมูลพิกเซลทีละจุด ว่าทำยังไงแอปพลิเคชันของเราถึงจะวิ่งเร็วทะลุมิติครับ!
3. 🧠 แก่นวิชา (Core Concepts)
ใน OpenCV (C++) เวลาเราต้องการดึงค่าสี หรือเปลี่ยนค่าสีของพิกเซลใดพิกเซลหนึ่งใน cv::Mat เรามีวิธียอดฮิตอยู่ 2 วิธีหลักๆ ซึ่งมีความเร็วในการประมวลผลแตกต่างกันอย่างสิ้นเชิง:
- 1. การเข้าถึงแบบระบุพิกัดด้วยฟังก์ชัน
.at<Type>()- มันคืออะไร: เป็นฟังก์ชันแบบ Template ที่ให้เราระบุพิกัด (Row, Column) คล้ายกับการจิ้มเลือกเซลล์ในตาราง Excel
- การใช้งานกับภาพสี: เราจะใช้ Type ที่ชื่อว่า
Vec3b(ย่อมาจาก Vector ที่เก็บค่าแบบ 1-byte จำนวน 3 ตัว) ซึ่งก็คือช่องสี B-G-R นั่นเองครับ เราสามารถเข้าถึงได้ด้วยimage.at<Vec3b>(y, x) - ข้อดี/ข้อเสีย: เขียนง่าย อ่านโค้ดเข้าใจทันที และมีความปลอดภัยสูง (ในโหมด Debug มันจะคอยเช็กว่าเราใส่พิกัดเกินขนาดภาพไหม) แต่ข้อเสียคือทำงานช้ามาก ถ้าต้องเอาไปใส่ใน Loop เพื่อวนอ่านพิกเซลทั้งภาพ เพราะมันต้องคำนวณตำแหน่ง Memory Address ใหม่ทุกรอบการวนลูป
- 2. การเข้าถึงด้วย Pointer แบบ
.ptr<Type>()(The Pro Way)- มันคืออะไร: แทนที่จะถามหาพิกเซลทีละตัว เราจะขอ “Pointer ที่ชี้ไปยังจุดเริ่มต้นของแถว (Row)” จากนั้นใช้ทักษะ Pointer Arithmetic ของ C++ วิ่งกวาดรวดเดียวจบทั้งแถว
- ข้อดี/ข้อเสีย: นี่คือวิธีที่เร็วที่สุดและเป็นมาตรฐานของวิศวกร Vision มันข้ามขั้นตอนการเช็กขอบเขตและลดภาระ CPU ในการคำนวณ Address ไปได้มหาศาล ข้อเสียคือต้องระวังเรื่อง Memory Out of Bounds ให้ดี พลาดนิดเดียวโปรแกรมแครช (Crash) ทันที!
เปรียบเทียบความเร็ว (Performance Comparison)
จากการทดสอบวนลูปเพื่อเข้าถึงภาพขนาด Full HD (1920x1080) วิธีการใช้ Pointer สามารถทำงานได้เร็วกว่าการใช้ฟังก์ชัน .at หลายเท่าตัว! (จากหน่วยหลายสิบมิลลิวินาที อาจลดลงเหลือไม่ถึง 3 มิลลิวินาที) ดังนั้นในงาน Real-time Video Processing เราจึงมักจะเขียนอัลกอริทึมด้วย Pointer เสมอครับ

4. 💻 ร่ายมนต์คำสั่ง (Show me the Code)
ลองมาดูโค้ด C++ เปรียบเทียบการทำงานของทั้งสองวิธีกันครับ สมมติว่าเราต้องการสร้าง Custom Filter ที่จะเปลี่ยนค่าสีน้ำเงิน (Blue Channel) ให้เป็นเลข 0 ทั่วทั้งภาพ
#include <opencv2/opencv.hpp>
#include <iostream>
#include <chrono> // สำหรับจับเวลา
using namespace cv;
using namespace std;
using namespace std::chrono;
int main() {
// 1. โหลดภาพสีต้นฉบับ
Mat image = imread("factory_camera.jpg", IMREAD_COLOR);
if(image.empty()) return -1;
// สร้างภาพเปล่า 2 ภาพไว้รอรับผลลัพธ์
Mat resultAt = image.clone();
Mat resultPtr = image.clone();
// ========================================================
// วิธีที่ 1: ใช้ฟังก์ชัน .at<Vec3b>() (ปลอดภัยแต่อืด)
// ========================================================
auto start = high_resolution_clock::now();
for(int y = 0; y < resultAt.rows; y++) {
for(int x = 0; x < resultAt.cols; x++) {
// ดึงค่าเวกเตอร์สีของพิกเซลที่ (y, x) ออกมา
// จำไว้ว่าช่องสีคือ 0=Blue, 1=Green, 2=Red
resultAt.at<Vec3b>(y, x) = 0; // ลบสีน้ำเงินทิ้ง
}
}
auto stop = high_resolution_clock::now();
auto durationAt = duration_cast<microseconds>(stop - start);
// ========================================================
// วิธีที่ 2: ใช้ Pointer .ptr<Vec3b>() (ซิ่งทะลุมิติ)
// ========================================================
start = high_resolution_clock::now();
for(int y = 0; y < resultPtr.rows; y++) {
// ขอ Pointer ชี้ไปที่จุดเริ่มต้นของแถวที่ y
Vec3b* row_ptr = resultPtr.ptr<Vec3b>(y);
for(int x = 0; x < resultPtr.cols; x++) {
// วิ่งทะลวงไปตามแกน X ของแถวนั้นๆ ได้เลย!
row_ptr[x] = 0;
}
}
stop = high_resolution_clock::now();
auto durationPtr = duration_cast<microseconds>(stop - start);
// 3. แสดงผลลัพธ์การจับเวลา
cout << "Time using .at<Vec3b>(): " << durationAt.count() / 1000.0 << " ms" << endl;
cout << "Time using Pointer .ptr: " << durationPtr.count() / 1000.0 << " ms" << endl;
imshow("Result Pointer", resultPtr);
waitKey(0);
return 0;
}5. 🛡️ เคล็ดลับจากคัมภีร์ลับ (Under the Hood / Pro-Tips)
- The Continuous Matrix Trick: ถ้าน้องๆ อยากให้เร็วขึ้นไปอีกขั้น! ปกติแล้ว
cv::Matอาจมีช่องว่างของ Memory คั่นระหว่างแถว (เพื่อให้จัดเรียง Address ลงล็อก CPU) แต่ถ้าเราเช็กด้วยฟังก์ชันimage.isContinuous()แล้วพบว่าเป็นจริง (True) แปลว่าข้อมูลทั้งภาพถูกจัดเรียงเป็นเส้นตรงยาวๆ เราสามารถใช้ Pointer วิ่งรวดเดียวลูปเดียวตั้งแตชิ้นแรกยันชิ้นสุดท้าย (แทนที่จะทำ 2 ลูปแบบ Row/Col) ได้เลยครับ! - ใช้ฟังก์ชันสำเร็จรูปเสมอถ้ามีโอกาส: แม้เราจะรู้ว่า Pointer เร็วแค่ไหน แต่ถ้า OpenCV มีอัลกอริทึมสำเร็จรูปให้ใช้อยู่แล้ว (เช่น จะคูณค่า หรือทำ Threshold) ให้ใช้ฟังก์ชันระดับสูงของ OpenCV เถอะครับ! เพราะฟังก์ชันพวกนั้นถูกทำ Hardware Acceleration รีดสเปกขั้นสุดด้วย SIMD, AVX2 (Universal Intrinsics) ซึ่งทะลวง Memory ได้รวดเร็วกว่าที่เราเขียน
forloop เองแบบเทียบไม่ติดเลยครับ
6. 🏁 บทสรุป (To be continued…)
จะเห็นได้ว่าในฐานะวิศวกร Computer Vision เราไม่ได้แค่ต้องรู้ว่าฟังก์ชันไหนใช้ทำอะไร แต่ต้องรู้ถึงกลไกการวิ่งของข้อมูลใน Memory ด้วย การเลือกใช้ Pointer ในจุดที่เหมาะสมจะทำให้ Vision App ของเราสเกลไปรันบนกล้องความละเอียดสูงๆ หรือบอร์ด Embedded ขนาดเล็กได้อย่างสบายๆ ครับ
ตอนนี้เราปรับเปลี่ยนค่าพิกเซลเป็นแล้ว ในตอนต่อไป เราจะเริ่มนำวิชาพื้นฐานทั้งหมดมาผสานกัน เพื่อสร้าง Feature Extraction เท่ๆ อย่างการ “กรองหาขอบวัตถุ (Edge Detection)” ด้วยคณิตศาสตร์ระดับ Convolution กันครับ เตรียมรับความสนุกได้เลย!
ต้องการที่ปรึกษาด้านการพัฒนาระบบ AI Camera หรือ Machine Vision ให้กับโรงงานของคุณ? ทีมงาน WP Solution พร้อมให้บริการออกแบบและติดตั้งระบบแบบครบวงจร ดูรายละเอียดบริการของเราได้ที่: www.wpsolution2017.com หรือพูดคุยปรึกษาเบื้องต้นได้ที่ Line: wisit.p