5 วัดขนาดและสถานีตรวจ
สอบเทียบกล้อง วัดขนาดเหรียญหรือแหวนรอง ศึกษาความทำซ้ำได้ แล้วส่งผล OK/NG ไปบอร์ด Arduino
โจทย์
สายการผลิตชิ้นงานกลมแบน (แหวนรอง เหรียญ ฝาโลหะ) ต้องการสถานีที่วัดเส้นผ่านศูนย์กลางทุกชิ้น แล้วให้ไฟเขียวเมื่อผ่านและไฟแดงเมื่อไม่ผ่าน คำถามสำคัญไม่ใช่ “วัดได้ไหม” แต่คือ “วัดได้ดีพอสำหรับ tolerance นี้ไหม”
ใบงาน 2 พบว่าค่า k ค่าเดียวผิดเมื่อชิ้นงานอยู่ที่มุมภาพ สาเหตุหลักคือ lens distortion เลนส์ทำให้เส้นตรงที่ขอบภาพโค้ง ใบงานนี้แก้ด้วยการสอบเทียบกล้องเต็มรูปแบบ
Checkerboard x 15 views -> calibrateCamera -> undistort (ส่วนที่ 1)
Backlight -> contour -> diameter (mm) x 30 -> %GRR (ส่วนที่ 2)
Size vs tolerance -> OK/NG -> Serial -> UNO R4 -> LED (ส่วนที่ 3)
เมื่อจบใบงาน นักศึกษาควรสามารถ
- สอบเทียบกล้องด้วย checkerboard และแก้ภาพให้หาย distortion
- วัดขนาดชิ้นงานบน backlight เป็น mm และเทียบกับเวอร์เนียร์
- ศึกษาความทำซ้ำได้ของการวัด และตัดสินว่าระบบใช้กับ tolerance ที่กำหนดได้หรือไม่
- ส่งผลตัดสินไปบอร์ดควบคุม และบันทึกผลทุกชิ้น
อุปกรณ์:
- กล้องพร้อมขาตั้ง และโทรศัพท์หรือแท็บเล็ตเครื่องที่ 2 เป็น backlight (เปิดภาพขาว วางกระดาษทับ แบบใบงาน 3)
data/checkerboard.pdfพิมพ์ขนาดจริง 100% แล้วติดบนแผ่นแข็งให้เรียบ- ชิ้นงาน: เหรียญ 1, 5 และ 10 บาท อย่างละ 2 เหรียญ (หรือแหวนรอง M3–M8 ถ้ามี)
- เวอร์เนียร์คาลิปเปอร์
- Arduino UNO R4 WiFi, LED เขียวและแดงพร้อมตัวต้านทาน 220 Ω
ใบบันทึกผล: ดาวน์โหลด PDF เปิดบน tablet แล้วเขียนผลลงในตารางและกรอบคำตอบ ส่งเป็น PDF เมื่อจบคาบ
โค้ดตั้งต้น
รันโค้ดนี้ให้ได้ผลก่อนเริ่มส่วนที่ 1 ดาวน์โหลดได้ที่ code/05_start.py โค้ดนี้รันได้ด้วยตัวเอง ไม่ต้องใช้โค้ดจากใบงานก่อนหน้า แก้ SOURCE ให้ตรงกับกล้องหรือไฟล์ที่ใช้ (ใบงาน 0)
"""Worksheet 5 starter: show the camera and find the checkerboard corners live.
Run: python 05_start.py (s = save frame, q = quit)
"""
import sys
import cv2
SOURCE = 0 # 0 = USB webcam, or "http://192.168.1.50:8080/video"
PATTERN = (9, 6) # inner corners of data/checkerboard.pdf (columns, rows)
SQUARE = 25.0 # mm
if isinstance(SOURCE, int) and sys.platform == "win32":
cap = cv2.VideoCapture(SOURCE, cv2.CAP_DSHOW)
else:
cap = cv2.VideoCapture(SOURCE)
saved = 0
while True:
ret, frame = cap.read()
if not ret:
print("Failed to grab frame: check SOURCE")
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
found, corners = cv2.findChessboardCorners(gray, PATTERN)
view = frame.copy()
if found:
cv2.drawChessboardCorners(view, PATTERN, corners, found)
cv2.putText(view, "board found" if found else "board not found", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 200, 0) if found else (0, 0, 255), 2)
cv2.imshow("Worksheet 5", view)
key = cv2.waitKey(1) & 0xFF
if key == ord('s'):
cv2.imwrite(f"board_{saved:02d}.png", frame)
print("saved", f"board_{saved:02d}.png")
saved += 1
elif key == ord('q'):
break
cap.release()
cv2.destroyAllWindows()| ปุ่ม | ทำอะไร |
|---|---|
s |
บันทึกภาพ |
q |
หยุดโปรแกรม |
ตรวจว่าพร้อม: เห็นจุดมุมสีของ checkerboard และข้อความ board found
ส่วนที่ 1: สอบเทียบกล้อง
เก็บภาพ checkerboard
checkerboard มี 10 x 7 ช่อง ช่องละ 25 mm จึงมี มุมด้านใน 9 x 6 จุด ที่รู้ตำแหน่งจริงแน่นอน OpenCV หามุมเหล่านี้ในภาพได้ละเอียดกว่า 1 pixel จากภาพหลายมุมมอง จึงคำนวณได้ทั้งความยาวโฟกัส จุดศูนย์กลางภาพ และค่า distortion ของเลนส์
import cv2
import numpy as np
PATTERN = (9, 6) # inner corners (columns, rows)
SQUARE = 25.0 # mm
objp = np.zeros((PATTERN[0] * PATTERN[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:PATTERN[0], 0:PATTERN[1]].T.reshape(-1, 2) * SQUARE
obj_points, img_points = [], []
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
cap = cv2.VideoCapture(0)
while len(obj_points) < 15:
ret, frame = cap.read()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
found, corners = cv2.findChessboardCorners(gray, PATTERN)
view = frame.copy()
if found:
cv2.drawChessboardCorners(view, PATTERN, corners, found)
cv2.putText(view, f"{len(obj_points)}/15 (c = capture)", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 0, 255), 2)
cv2.imshow("Calibration", view)
if cv2.waitKey(1) & 0xFF == ord('c') and found:
corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
obj_points.append(objp)
img_points.append(corners)
err, K, dist, rvecs, tvecs = cv2.calibrateCamera(obj_points, img_points,
gray.shape[::-1], None, None)
print("reprojection error (pixel):", err)
print("camera matrix K:\n", K)
print("distortion (k1 k2 p1 p2 k3):", dist.ravel())
np.savez("calib.npz", K=K, dist=dist)ถือแผ่นให้เต็มภาพบ้าง อยู่ที่มุมภาพบ้าง เอียงหลายทิศ (ไม่เกิน 45 องศา) และถือให้นิ่งตอนกด c ถ้า reprojection error เกิน 1 pixel ให้เก็บภาพใหม่
| ค่า | ผลที่ได้ |
|---|---|
| reprojection error (pixel) | |
| f_x, f_y (pixel) | |
| c_x, c_y (pixel) | |
| k_1, k_2 |
แก้ภาพและหาค่า k
data = np.load("calib.npz")
K, dist = data["K"], data["dist"]
ret, frame = cap.read()
undist = cv2.undistort(frame, K, dist)
cv2.imshow("Original", frame)
cv2.imshow("Undistorted", undist)วาง checkerboard ราบ บนจอที่ใช้เป็น backlight (แผ่นแข็งวางทับจอได้) เพื่อให้อยู่ระนาบเดียวกับที่จะวางชิ้นงาน แล้วหาค่า mm/pixel จากระยะระหว่างมุมที่อยู่ติดกันในภาพที่แก้แล้ว ค่านี้เฉลี่ยจาก 93 คู่จุด จึงแม่นกว่าการคลิกไม้บรรทัดในใบงาน 2 มาก
gray = cv2.cvtColor(undist, cv2.COLOR_BGR2GRAY)
found, corners = cv2.findChessboardCorners(gray, PATTERN)
corners = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
grid = corners.reshape(PATTERN[1], PATTERN[0], 2) # rows x columns x (x, y)
d_px = np.concatenate([np.linalg.norm(np.diff(grid, axis=1), axis=2).ravel(),
np.linalg.norm(np.diff(grid, axis=0), axis=2).ravel()])
k = SQUARE / d_px.mean()
print(f"k = {k:.4f} mm/pixel (S.D. of spacing {d_px.std():.2f} pixel)")ค่า k ใช้ได้เฉพาะระยะกล้องและระนาบที่สอบเทียบ ถ้ากล้องขยับ เปลี่ยน zoom/focus หรือเปลี่ยนเครื่องที่ใช้เป็น backlight (ความหนาต่างกัน) ต้องหา k ใหม่ จำผลจากใบงาน 2 ที่ยกชิ้นงานสูง 3 cm แล้ว error เพิ่ม
วัดซ้ำตารางสุดท้ายของใบงาน 2 โดยใช้ภาพที่แก้แล้ว
| เงื่อนไข | error ก่อนแก้ (mm) | error หลังแก้ (mm) |
|---|---|---|
| ชิ้นงานกลางภาพ | ||
| ชิ้นงานที่มุมภาพ |
ส่วนที่ 2: วัดขนาดและความทำซ้ำได้
วัดเส้นผ่านศูนย์กลางบน backlight
เอา checkerboard ออกโดยไม่ขยับกล้องและจอ ปิดไฟห้อง ชิ้นงานจะเป็นเงาดำบนพื้นสว่าง (ใบงาน 3) ใช้ RETR_CCOMP เพื่อแยกขอบนอกออกจากรูตรงกลาง (ถ้าเป็นแหวนรอง)
MIN_AREA = 300
def measure(frame):
undist = cv2.undistort(frame, K, dist)
gray = cv2.cvtColor(undist, cv2.COLOR_BGR2GRAY)
_, bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
contours, hier = cv2.findContours(bw, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_NONE)
parts = []
for i, cnt in enumerate(contours):
if hier[0][i][3] != -1 or cv2.contourArea(cnt) < MIN_AREA:
continue # keep outer contours only
area = cv2.contourArea(cnt)
d_area = 2 * np.sqrt(area / np.pi) * k # diameter of a circle with the same area
(x, y), r = cv2.minEnclosingCircle(cnt)
d_circle = 2 * r * k
parts.append(((int(x), int(y)), d_area, d_circle))
return undist, partsวัดชิ้นงานแต่ละขนาดเทียบกับเวอร์เนียร์ (ค่าอ้างอิง)
| ชิ้นงาน | เวอร์เนียร์ (mm) | d จากพื้นที่ (mm) | d จาก minEnclosingCircle (mm) | e (mm) |
|---|---|---|---|---|
| 1 บาท | ||||
| 5 บาท | ||||
| 10 บาท |
ความทำซ้ำได้ (repeatability)
วัดเหรียญเดียวกัน 30 ครั้ง โดย ยกออกแล้ววางใหม่ทุกครั้ง ให้ตำแหน่งและมุมเปลี่ยนไปเล็กน้อยเหมือนในสายการผลิตจริง
values = []
while len(values) < 30:
ret, frame = cap.read()
undist, parts = measure(frame)
for (cx, cy), d, _ in parts:
cv2.putText(undist, f"{d:.2f} mm", (cx - 40, cy), cv2.FONT_HERSHEY_SIMPLEX,
0.7, (0, 0, 255), 2)
cv2.imshow("Gauge", undist)
if cv2.waitKey(1) & 0xFF == ord(' ') and len(parts) == 1: # space = record
values.append(parts[0][1])
print(len(values), f"{parts[0][1]:.3f}")
values = np.array(values)
print(f"mean = {values.mean():.3f} mm, S.D. = {values.std(ddof=1):.3f} mm")เทียบความกระจายของการวัดกับความกว้างของช่วง tolerance (USL − LSL) แบบย่อของ Gage R&R ที่ใช้รับรองเครื่องมือวัดในโรงงาน
\%\mathrm{GRR} \approx \frac{6\,\sigma_{\mathrm{meas}}}{\mathrm{USL} - \mathrm{LSL}} \times 100\%
| %GRR | ความหมาย |
|---|---|
| ต่ำกว่า 10% | ระบบวัดใช้ได้ |
| 10–30% | ใช้ได้แบบมีเงื่อนไข ขึ้นกับความสำคัญของงาน |
| สูงกว่า 30% | ใช้ไม่ได้ ต้องปรับปรุงระบบวัด |
| รายการ | ค่า |
|---|---|
| ค่าเฉลี่ย (mm) | |
| \sigma_{\mathrm{meas}} (mm) | |
| 1 pixel = กี่ mm | |
| %GRR ถ้า tolerance ±0.5 mm | |
| %GRR ถ้า tolerance ±0.1 mm |
ส่วนที่ 3: สถานีตรวจ
บอร์ดรับผล
ต่อ LED เขียวที่ขา D2 และ LED แดงที่ขา D3 (ผ่านตัวต้านทาน 220 Ω) แล้วอัปโหลดสเก็ตช์นี้ บอร์ดรอรับตัวอักษร 1 ตัวทาง Serial
const int GREEN = 2;
const int RED = 3;
void setup() {
Serial.begin(115200);
pinMode(GREEN, OUTPUT);
pinMode(RED, OUTPUT);
}
void loop() {
if (Serial.available()) {
char c = Serial.read();
digitalWrite(GREEN, c == 'G'); // G = OK
digitalWrite(RED, c == 'R'); // R = NG, anything else = both off
}
}โปรแกรมสถานีตรวจ
ผลจากภาพเดียวอาจผิดเพราะมือยังไม่ออกจากภาพหรือชิ้นงานยังไม่นิ่ง จึงตัดสินเมื่อได้ผลเดียวกัน 5 ภาพติดกัน และส่งผลไปบอร์ดเพียงครั้งเดียวต่อชิ้น
import csv
import time
import serial
NOMINAL, TOL = 24.0, 0.5 # mm: e.g. a 5-baht coin; use your vernier value
ser = serial.Serial("COM5", 115200, timeout=1) # check the port in Device Manager
time.sleep(2) # the board resets when the port opens
log = open("inspection_log.csv", "a", newline="")
writer = csv.writer(log)
history, sent = [], None
while True:
ret, frame = cap.read()
undist, parts = measure(frame)
if len(parts) == 1:
d = parts[0][1]
result = "G" if ______________________________ else "R" # inside NOMINAL +- TOL?
else:
result = "0" # no part (or more than one)
history = (history + [result])[-5:]
if len(history) == 5 and len(set(history)) == 1 and result != sent:
ser.write(result.encode())
sent = result
if result != "0":
writer.writerow([time.strftime("%Y-%m-%d %H:%M:%S"), f"{d:.3f}",
"OK" if result == "G" else "NG"])
log.flush()
print(result, f"{d:.3f} mm")
cv2.imshow("Station", undist)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
ser.write(b"0")
ser.close()
log.close()ตั้ง NOMINAL เป็นขนาดของเหรียญ 5 บาทที่วัดด้วยเวอร์เนียร์ แล้ววางเหรียญ 1, 5 และ 10 บาทสลับกัน 10 ครั้ง (ควรได้ OK เฉพาะเหรียญ 5 บาท) แล้วบันทึกผล และวัดเวลาตั้งแต่วางชิ้นงานจนไฟติดด้วยการถ่ายวิดีโอด้วยโทรศัพท์ (นับ frame ระหว่างสองเหตุการณ์)
| รายการ | ค่า |
|---|---|
| ชิ้นที่ตัดสินถูก / ทั้งหมด | |
| เวลาตั้งแต่วางชิ้นงานจนไฟติด (ms) | |
| ชิ้นต่อนาทีสูงสุดที่ทำได้ |
- ระบบของคุณใช้ตรวจชิ้นงานที่ tolerance ±0.1 mm ได้หรือไม่ ถ้าไม่ได้ ต้องปรับอะไร (ความละเอียดกล้อง ระยะกล้อง แสง หรือวิธีวัด) และแต่ละทางแลกกับอะไร
- เหตุใด d จากพื้นที่กับ d จาก minEnclosingCircle จึงต่างกัน ควรใช้ค่าใดถ้าชิ้นงานมีรอยบิ่นที่ขอบ
- การรอให้ได้ผลเดียวกัน 5 ภาพติดกัน ช่วยเรื่องใด และทำให้เสียอะไร (เชื่อมกับใบงาน 3)
- กลับไปดูรายการ “ยังขาดอะไร” ที่เขียนไว้ท้ายใบงาน 1 ตอนนี้ระบบของคุณมีข้อใดแล้ว และยังขาดข้อใด
ทบทวน
- lens distortion ทำให้ k ค่าเดียวผิดที่ขอบภาพ การสอบเทียบด้วย checkerboard แก้ได้ และให้ k ที่แม่นกว่าการคลิกไม้บรรทัด
- backlight + Otsu + contour เป็นสูตรพื้นฐานของงานวัดขนาดชิ้นงานแบน และต้องสอบเทียบที่ระนาบเดียวกับชิ้นงาน
- ความแม่นของค่าเฉลี่ยไม่พอ ต้องรู้ความกระจาย (repeatability) และเทียบกับ tolerance ด้วย %GRR
- สถานีตรวจที่ใช้งานได้ต้องตัดสินอย่างมั่นคง ส่งผลให้อุปกรณ์อื่น และบันทึกผลทุกชิ้นเพื่อการตรวจสอบย้อนกลับ