ตำแหน่ง Vision AI สาย Pandora ต้องตรวจตำหนิบนโลหะมันวาวที่สะท้อนแสงรุนแรง มีข้อมูล label น้อย และพื้นผิวโค้ง บทนี้รวมเทคนิคหน้างานที่มักถูกถามในสัมภาษณ์: ลดแสงสะท้อน, segment ตำหนิแบบ zero-shot, คลี่ผิวโค้งก่อน OCR และสร้างข้อมูลตำหนิสังเคราะห์เมื่อตัวอย่างจริงหายาก
บริบท: ตรวจตำหนิเครื่องประดับ
โจทย์จริงคือชิ้นงานส่วนใหญ่เป็นของดี ตำหนิ (รอยขีด รูเข็ม คราบ) มีน้อยและหลากหลาย ภาพถ่ายในไลน์ผลิต มีแสงสะท้อนแบบ specular ที่ทำให้ pixel อิ่มตัวจนกลบรายละเอียดผิว เป้าหมายของ pipeline ขั้นต้นคือทำให้ภาพ “สะอาด” และได้ mask ของตำหนิที่แม่นพอจะวัดขนาดต่อในขั้น QC
ลดแสงสะท้อน: bilateral filter + inpainting
Gaussian blur ลด noise ได้แต่เบลอขอบตำหนิไปด้วย bilateral filter ถ่วงน้ำหนักทั้งระยะเชิงพื้นที่ และความต่างของความเข้ม จึง smooth เฉพาะบริเวณที่คล้ายกัน รักษาขอบคมไว้ ส่วน specular highlight ที่ทำให้ pixel อิ่มตัวนั้นข้อมูลผิวจริงหายไปแล้ว ต้องตรวจจับเป็น mask แล้ว inpaint สังเคราะห์ผิวใหม่จากรอบข้าง
1import cv22import numpy as np345def reduce_specular(bgr: np.ndarray) -> np.ndarray:6 """ลด noise โดยรักษาขอบ แล้ว inpaint บริเวณที่สะท้อนแสงจนอิ่มตัว."""7 # 1) bilateral filter: d=9, sigmaColor/sigmaSpace ปรับตามขนาดภาพ8 smoothed = cv2.bilateralFilter(bgr, d=9, sigmaColor=75, sigmaSpace=75)910 # 2) หา specular highlight: ใน HSV คือ V สูงและ S ต่ำ (ขาวจ้า)11 hsv = cv2.cvtColor(smoothed, cv2.COLOR_BGR2HSV)12 h, s, v = cv2.split(hsv)13 spec_mask = ((v > 240) & (s < 30)).astype(np.uint8) * 2551415 # ขยาย mask เล็กน้อยให้ครอบขอบ highlight ที่ฟุ้ง16 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))17 spec_mask = cv2.dilate(spec_mask, kernel, iterations=1)1819 # 3) inpaint เติมเนื้อผิวบริเวณที่อิ่มตัวจาก pixel รอบข้าง20 restored = cv2.inpaint(smoothed, spec_mask, inpaintRadius=3,21 flags=cv2.INPAINT_TELEA)22 return restoredMobileSAM zero-shot กับข้อมูลน้อย
MobileSAM เป็น promptable segmentation ที่ pretrain มาแล้ว จึงคืน mask จาก prompt (จุด/กรอบ) ได้โดยไม่ต้อง fine-tune กับตำหนิเครื่องประดับ เหมาะกับสถานการณ์ที่ label น้อย เราหา candidate ตำหนิด้วยวิธีคลาสสิก (เช่น blob/contour) เพื่อสร้าง prompt อัตโนมัติ แล้วให้ MobileSAM ตัดขอบ mask ให้คมพอจะวัดได้
1import numpy as np2import torch3from mobile_sam import sam_model_registry, SamPredictor456class DefectSegmenter:7 def __init__(self, checkpoint: str, device: str | None = None):8 self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")9 sam = sam_model_registry["vit_t"](checkpoint=checkpoint)10 sam.to(self.device).eval()11 self.predictor = SamPredictor(sam)1213 def segment(14 self,15 image_rgb: np.ndarray,16 point_prompts: np.ndarray, # shape (N, 2) -> [x, y]17 box_prompt: np.ndarray | None = None, # [x0, y0, x1, y1]18 ) -> np.ndarray:19 """คืน boolean mask ของตำหนิจาก prompt (zero-shot ไม่ต้องเทรน)."""20 self.predictor.set_image(image_rgb)21 labels = np.ones(len(point_prompts), dtype=np.int32) # 1 = foreground22 masks, scores, _ = self.predictor.predict(23 point_coords=point_prompts,24 point_labels=labels,25 box=box_prompt,26 multimask_output=True, # คืนหลาย mask แล้วเลือกที่ดีที่สุด27 )28 best = int(np.argmax(scores))29 return masks[best].astype(bool)คลี่ผิวโค้งด้วย OpenCV polar transform
ตัวอักษร/เลขสลักรอบวงแหวนเรียงตามเส้นโค้ง OCR ที่คาดหวังข้อความเป็นเส้นตรงจะอ่านพลาด ใช้ cv2.warpPolar รอบจุดศูนย์กลางวงแหวนเพื่อ “คลี่” วงกลมออกเป็นแถบสี่เหลี่ยม ตัวอักษรจะเรียงเป็นแนวนอนตรงและพร้อมเข้า OCR
1import cv22import numpy as np345def unroll_ring(6 image: np.ndarray,7 center: tuple[float, float],8 max_radius: float,9 out_size: tuple[int, int] = (720, 120),10) -> np.ndarray:11 """คลี่ตัวอักษรที่สลักรอบวงแหวนให้เป็นแถบแนวนอนตรงด้วย polar transform."""12 width, height = out_size13 flags = cv2.INTER_CUBIC + cv2.WARP_POLAR_LINEAR1415 polar = cv2.warpPolar(16 image,17 dsize=(width, height), # (มุม, รัศมี) -> แถบแนวนอน18 center=center,19 maxRadius=max_radius,20 flags=flags,21 )22 # หมุนให้แกนมุมเป็นแนวนอน (อ่านซ้ายไปขวา)23 return cv2.rotate(polar, cv2.ROTATE_90_COUNTERCLOCKWISE)242526# inverse: ถ้าต้อง map ผลกลับไปยังภาพต้นฉบับ ใช้ WARP_INVERSE_MAP27def roll_back(polar: np.ndarray, center, max_radius, size) -> np.ndarray:28 flags = cv2.INTER_CUBIC + cv2.WARP_POLAR_LINEAR + cv2.WARP_INVERSE_MAP29 return cv2.warpPolar(polar, size, center, max_radius, flags)ตำหนิสังเคราะห์ด้วย Poisson blending
เมื่อตัวอย่างตำหนิจริงหายาก เราเพิ่มข้อมูลด้วยการแปะ patch ตำหนิจริงลงบนพื้นผิวสะอาด ปัญหาคือถ้าก็อปวางตรง ๆ จะเห็นขอบรอยต่อชัดจนโมเดลเรียนรู้ “ขอบรอยแปะ” แทนตำหนิ Poisson blending (seamless cloning) แก้ด้วยการ ผสานโดยรักษา gradient ของ source และปรับระดับสีให้กลมกลืนกับฉากหลัง รอยต่อจึงเนียน
1import cv22import numpy as np345def paste_defect(6 clean_bgr: np.ndarray,7 defect_patch: np.ndarray, # ภาพตำหนิจริงที่ตัดมา8 patch_mask: np.ndarray, # uint8 mask ของตำหนิใน patch (255 = ตำหนิ)9 center: tuple[int, int], # ตำแหน่งที่จะแปะบนชิ้นงานสะอาด10) -> np.ndarray:11 """แปะตำหนิแบบเนียนด้วย Poisson blending เพื่อสร้างข้อมูลเทรนเพิ่ม."""12 # MIXED_CLONE รักษา texture ของทั้ง source และ destination13 blended = cv2.seamlessClone(14 src=defect_patch,15 dst=clean_bgr,16 mask=patch_mask,17 p=center,18 flags=cv2.MIXED_CLONE,19 )20 return blended212223def build_label(shape, patch_mask, center) -> np.ndarray:24 """สร้าง ground-truth mask ให้ตรงกับตำแหน่งที่แปะ เพื่อใช้เทรน."""25 label = np.zeros(shape[:2], dtype=np.uint8)26 h, w = patch_mask.shape[:2]27 x, y = center28 y0, x0 = y - h // 2, x - w // 229 label[y0:y0 + h, x0:x0 + w][patch_mask > 0] = 25530 return labelเช็กลิสต์ก่อนสัมภาษณ์
- อธิบายได้ว่า bilateral filter ต่างจาก Gaussian อย่างไร (domain + range kernel รักษาขอบ)
- รู้ว่า specular highlight = pixel อิ่มตัว ต้องตรวจ mask แล้ว inpaint ไม่ใช่แค่ลด contrast
- อธิบายได้ว่าทำไม MobileSAM ทำ zero-shot ได้ และ prompt มาจากไหนเมื่อ label น้อย
- รู้ว่า warpPolar คลี่ตัวอักษรรอบวงแหวนเป็นแถบตรงก่อน OCR และ inverse map กลับได้
- อธิบาย Poisson blending ว่าผสานด้วย gradient ทำให้รอยต่อเนียน และได้ label มาฟรี
สรุปสำคัญ
- แสงสะท้อนแบบ specular บนโลหะมันวาวทำให้ pixel อิ่มตัว ใช้ bilateral filter รักษาขอบแล้ว inpaint บริเวณที่อิ่มตัวก่อนเข้าโมเดล
- MobileSAM เป็น promptable segmentation จึงทำ zero-shot ได้โดยไม่ต้องเทรน ใช้ point/box prompt แทนการมี label จำนวนมาก
- ผิวโค้งของแหวน/กำไลทำให้ตัวอักษรบิด ใช้ cv2.warpPolar คลี่เป็นแถบตรงก่อนส่งเข้า OCR
- เพิ่มข้อมูลตำหนิที่หายากด้วย Poisson blending (seamlessClone) เพื่อแปะตำหนิจริงลงบนพื้นผิวสะอาดอย่างเนียน
ควิซท้ายบท
01ทำไม bilateral filter จึงเหมาะกับการลด noise ก่อนจัดการแสงสะท้อนบนเครื่องประดับ มากกว่า Gaussian blur ธรรมดา
02ขั้นตอนที่ถูกต้องในการกำจัด specular highlight ก่อนเข้าโมเดลคือข้อใด
03ทำไม MobileSAM จึงทำงานแบบ zero-shot กับข้อมูลที่มี label น้อยได้
04ทำไมต้องใช้ cv2.warpPolar (polar transform) ก่อนทำ OCR บนตัวอักษรที่สลักรอบวงแหวน