ตัวอย่างโปรเจกต์ตรวจจับป้ายทะเบียนและนับจำนวนรถ
ระบบตรวจป้ายทะเบียนและนับรถเป็นหนึ่งในงาน AI ที่ใช้บ่อยที่สุดในกล้องวงจรปิด ระบบต้องหาตำแหน่งรถและป้ายในแต่ละเฟรม จับคู่ป้ายกับรถคันที่ถูกต้อง อ่านตัวอักษรบนป้าย และนับรถแต่ละคันเพียงครั้งเดียว บทความนี้อธิบายหลักการทั้งหมดด้วยโค้ด Node.js ที่รันได้จริงโดยใช้ข้อมูลจำลองแทนโมเดลตรวจจับ เพื่อให้เห็นตรรกะที่ใช้ได้กับระบบจริง
สิ่งที่ต้องเตรียม
ต้องมี Node.js 18 ขึ้นไป และพื้นฐาน JavaScript ตัวอย่างทุกไฟล์รันด้วยคำสั่ง node ชื่อไฟล์.js โดยไม่ต้องติดตั้งแพ็กเกจเพิ่ม ส่วนการใช้โมเดลตรวจจับและ OCR จริงต้องใช้ Python และ ultralytics ซึ่งยังไม่ได้รันในเครื่องทดสอบของบทความนี้ จึงแนะนำไว้ในตัวอย่างสุดท้าย หมายเหตุ: กติกาป้ายในตัวอย่างเป็นแบบง่ายเพื่อสาธิต ควรปรับตามรูปแบบป้ายที่ใช้จริงในพื้นที่ และการติดตั้งกล้องต้องคำนึงถึงกฎหมายความเป็นส่วนตัวของแต่ละประเทศด้วย
ภาพประกอบ
1. ภาพรวมของระบบ
ระบบแบ่งเป็นหกขั้นตอน ได้แก่ รับภาพจากกล้อง ตรวจจับรถและป้าย ติดตามรถข้ามเฟรม จับคู่ป้ายกับรถ อ่านและทำความสะอาดข้อความ แล้วนับและบันทึกผล
1. รับภาพจากกล้องทีละเฟรม
2. ตรวจจับกรอบรถและกรอบป้ายในเฟรม
3. ติดตามรถให้มี ID เดิมข้ามเฟรม
4. จับคู่ป้ายกับรถคันที่กรอบป้ายอยู่ภายใน
5. อ่านข้อความป้าย แก้ตัวอักษรที่สับสน และโหวตผลจากหลายเฟรม
6. นับรถที่ข้ามเส้นเสมือน แล้วบันทึกเป็นไฟล์
แผนผังด้านบนแสดงลำดับนี้ทั้งหมด
2. จับคู่ป้ายกับรถ
ป้ายทะเบียนต้องอยู่ภายในกรอบของรถ จึงจับคู่โดยตรวจว่ากรอบป้ายอยู่ภายในกรอบรถคันใด ถ้าไม่อยู่ในกรอบใดเลย ให้ถือว่าไม่พบรถที่ตรงกัน
**โค้ด: ex01_plate_in_car.js**
// จับคู่ป้ายทะเบียนกับรถ: ป้ายที่อยู่ในกรอบของรถคันใด ให้ถือว่าเป็นของรถคันนั้น
function contains(outer, inner) {
return inner[0] >= outer[0] && inner[1] >= outer[1] && inner[2] <= outer[2] && inner[3] <= outer[3];
}
const cars = [
{ id: "รถ A", box: [100, 200, 400, 380] },
{ id: "รถ B", box: [500, 210, 780, 390] },
];
const plates = [
{ text: "กข 1234", box: [200, 330, 300, 360] },
{ text: "ขค 5678", box: [600, 340, 690, 370] },
{ text: "งจ 9999", box: [850, 300, 900, 330] },
];
for (const p of plates) {
const owner = cars.find(c => contains(c.box, p.box));
console.log(p.text.padEnd(9), owner ? "อยู่ในกรอบ " + owner.id : "ไม่พบรถที่ตรงกัน");
}
**ผลลัพธ์ที่ได้จากการรันจริง**
กข 1234 อยู่ในกรอบ รถ A
ขค 5678 อยู่ในกรอบ รถ B
งจ 9999 ไม่พบรถที่ตรงกัน
3. นับรถด้วยเส้นเสมือน
วางเส้นแนวตั้งในภาพ แล้วนับเมื่อจุดกลางของรถเปลี่ยนข้างของเส้น การเปลี่ยนเครื่องหมายของผลต่างแสดงว่าข้ามเส้น
**โค้ด: ex02_line_crossing.js**
// นับรถด้วยเส้นเสมือน: นับเมื่อจุดกลางของรถเคลื่อนข้ามเส้นจากฝั่งหนึ่งไปอีกฝั่ง
const LINE_X = 400;
// ตำแหน่ง x ของจุดกลางรถในแต่ละเฟรม
const cars = {
"รถ A": [300, 340, 380, 420, 460],
"รถ B": [100, 120, 140, 160],
"รถ C": [500, 450, 390, 350],
};
let total = 0;
for (const [name, xs] of Object.entries(cars)) {
for (let i = 1; i < xs.length; i++) {
const before = xs[i - 1] - LINE_X;
const after = xs[i] - LINE_X;
// เครื่องหมายต่างกันแปลว่าข้ามเส้น
if (before * after < 0) {
total++;
const dir = after > 0 ? "ไปทางขวา" : "ไปทางซ้าย";
console.log(`${name} ข้ามเส้นที่เฟรม ${i} ${dir}`);
}
}
}
console.log("รวมทั้งหมด", total, "คัน");
**ผลลัพธ์ที่ได้จากการรันจริง**
รถ A ข้ามเส้นที่เฟรม 3 ไปทางขวา
รถ C ข้ามเส้นที่เฟรม 2 ไปทางซ้าย
รวมทั้งหมด 2 คัน
4. ติดตามรถข้ามเฟรมด้วย IoU
ในแต่ละเฟรม เทียบกรอบรถกับกรอบของรถที่ติดตามอยู่ด้วย IoU ถ้าทับกันมากพอ (เกิน 0.3) ให้ใช้ ID เดิม ถ้าไม่มีให้สร้าง ID ใหม่
**โค้ด: ex03_iou_tracker.js**
// ติดตามรถข้ามเฟรมด้วย IoU: กรอบที่ทับกับของเดิมมากพอให้ใช้ ID เดิม ถ้าไม่มีให้สร้าง ID ใหม่
function iou(a, b) {
const ix1 = Math.max(a[0], b[0]), iy1 = Math.max(a[1], b[1]);
const ix2 = Math.min(a[2], b[2]), iy2 = Math.min(a[3], b[3]);
const inter = Math.max(0, ix2 - ix1) * Math.max(0, iy2 - iy1);
return inter / ((a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter);
}
const frames = [
[[100, 200, 220, 300], [500, 210, 620, 310]],
[[140, 200, 260, 300], [470, 210, 590, 310]],
[[180, 200, 300, 300], [440, 210, 560, 310]],
[[220, 200, 340, 300]],
];
const tracks = [];
let nextId = 1;
frames.forEach((dets, f) => {
const used = new Set();
const ids = [];
for (const box of dets) {
let best = null, bestIou = 0.3;
for (const t of tracks) {
if (used.has(t.id)) continue;
const v = iou(t.box, box);
if (v >= bestIou) { bestIou = v; best = t; }
}
if (!best) {
best = { id: nextId++, box };
tracks.push(best);
}
best.box = box;
used.add(best.id);
ids.push(best.id);
}
console.log(`เฟรม ${f}: ID ${ids.join(", ")}`);
});
console.log("จำนวน ID ทั้งหมด:", nextId - 1);
**ผลลัพธ์ที่ได้จากการรันจริง**
เฟรม 0: ID 1, 2
เฟรม 1: ID 1, 2
เฟรม 2: ID 1, 2
เฟรม 3: ID 1
จำนวน ID ทั้งหมด: 2
5. นับรถแต่ละคันเพียงครั้งเดียว
รวมการติดตามกับเส้นเสมือน แล้วจำว่า ID ใดเคยข้ามเส้นแล้วหรือยัง เมื่อ ID ใดข้ามเส้นครั้งแรกจึงนับหนึ่งครั้ง การนับทุกเฟรมที่ข้ามจะทำให้รถคันเดียวถูกนับหลายครั้ง
**โค้ด: ex04_count_unique.js**
// นับรถที่ไม่ซ้ำกัน: ติดตามรถด้วย ID แล้วนับเฉพาะครั้งแรกที่แต่ละ ID ข้ามเส้น
// ถ้านับทุกเฟรมที่ข้ามเส้น รถคันเดียวจะถูกนับซ้ำ
const LINE_X = 400;
function iou(a, b) {
const ix1 = Math.max(a[0], b[0]), iy1 = Math.max(a[1], b[1]);
const ix2 = Math.min(a[2], b[2]), iy2 = Math.min(a[3], b[3]);
const inter = Math.max(0, ix2 - ix1) * Math.max(0, iy2 - iy1);
return inter / ((a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter);
}
// รถ A เคลื่อนไปทางขวา รถ B เคลื่อนไปทางซ้าย
const frames = [];
for (let f = 0; f < 8; f++) {
const xa = 60 + 50 * f;
const xb = 620 - 50 * f;
frames.push([[xa, 200, xa + 120, 300], [xb, 340, xb + 120, 440]]);
}
const tracks = [];
let nextId = 1;
let counted = 0;
frames.forEach((dets, f) => {
const used = new Set();
for (const box of dets) {
let best = null, bestIou = 0.3;
for (const t of tracks) {
if (used.has(t.id)) continue;
const v = iou(t.box, box);
if (v >= bestIou) { bestIou = v; best = t; }
}
if (!best) {
best = { id: nextId++, box, lastX: null, crossed: false };
tracks.push(best);
}
best.box = box;
used.add(best.id);
const cx = (box[0] + box[2]) / 2;
if (best.lastX !== null && !best.crossed && (best.lastX - LINE_X) * (cx - LINE_X) < 0) {
best.crossed = true;
counted++;
console.log(`เฟรม ${f}: ID ${best.id} ข้ามเส้น นับเป็นคันที่ ${counted}`);
}
best.lastX = cx;
}
});
console.log("รถที่นับได้ทั้งหมด:", counted, "คัน");
**ผลลัพธ์ที่ได้จากการรันจริง**
เฟรม 6: ID 1 ข้ามเส้น นับเป็นคันที่ 1
เฟรม 6: ID 2 ข้ามเส้น นับเป็นคันที่ 2
รถที่นับได้ทั้งหมด: 2 คัน
6. ทำความสะอาดข้อความจาก OCR
OCR มักมีช่องว่างเกินและสับสนตัวอักษร เช่น O กับ 0, l กับ 1 และ B กับ 8 ขั้นตอนนี้ตัดช่องว่าง แยกส่วนอักษรไทยกับตัวเลข แล้วแก้เฉพาะส่วนตัวเลข
**โค้ด: ex05_clean_text.js**
// OCR มักอ่านผิด: มีช่องว่างเกิน และสับสนตัวอักษรกับตัวเลข เช่น O กับ 0, l กับ 1
// ขั้นตอนนี้ตัดช่องว่าง แยกส่วนตัวอักษรไทยกับตัวเลข แล้วแก้ตัวที่สับสนเฉพาะส่วนตัวเลข
function cleanText(raw) {
const s = raw.replace(/\s+/g, "");
const m = s.match(/^([ก-ฮ]+)(.*)$/);
if (!m) return raw;
const digits = m[2]
.replace(/O/g, "0")
.replace(/[lI]/g, "1")
.replace(/B/g, "8")
.replace(/S/g, "5");
return m[1] + " " + digits;
}
const readings = ["กข 1234", "ก ข 1 2 3 4", "กข 1l34", "กข 12O4", "กข 1B34"];
for (const r of readings) {
console.log(JSON.stringify(r).padEnd(16), "->", cleanText(r));
}
**ผลลัพธ์ที่ได้จากการรันจริง**
"กข 1234" -> กข 1234
"ก ข 1 2 3 4" -> กข 1234
"กข 1l34" -> กข 1134
"กข 12O4" -> กข 1204
"กข 1B34" -> กข 1834
7. ตรวจรูปแบบป้ายก่อนยอมรับ
ตรวจด้วยนิพจน์ปกติว่าข้อความอยู่ในรูปแบบที่กำหนดหรือไม่ เพื่อกันผลอ่านที่ผิดรูปแบบชัดเจนออกไป
**โค้ด: ex06_validate.js**
// ตรวจรูปแบบป้ายก่อนยอมรับผล: ตัวอย่างนี้ใช้รูปแบบ อักษรไทย 1-3 ตัว เว้นวรรด หนึ่งช่อง แล้วตามด้วยตัวเลข 1-4 หลัก
// ตัวอย่างนี้เป็นกติกาอย่างง่ายเพื่อสาธิต ควรปรับตามรูปแบบป้ายที่ใช้จริงในพื้นที่
const PATTERN = /^[ก-ฮ]{1,3} \d{1,4}$/;
const samples = ["กข 1234", "กข1234", "กขค 56", "12 3456", "กข 12345", "งจ 9999"];
for (const s of samples) {
console.log(s.padEnd(10), PATTERN.test(s) ? "ผ่าน" : "ไม่ผ่าน");
}
**ผลลัพธ์ที่ได้จากการรันจริง**
กข 1234 ผ่าน
กข1234 ไม่ผ่าน
กขค 56 ผ่าน
12 3456 ไม่ผ่าน
กข 12345 ไม่ผ่าน
งจ 9999 ผ่าน
8. โหวตผลอ่านจากหลายเฟรม
รถคันเดียวถูกอ่านป้ายหลายครั้ง แต่ละครั้งอาจต่างกันเล็กน้อย ให้กรองผลที่ไม่ผ่านรูปแบบออก แล้วเลือกผลที่ปรากฏบ่อยที่สุด
**โค้ด: ex07_vote.js**
// อ่านป้ายจากหลายเฟรมแล้วเลือกผลที่ปรากฏบ่อยที่สุด (majority vote)
// อ่านผิดเฟรมเดียวจะไม่ทำให้ผลสุดท้ายผิด และตัดผลที่ไม่ผ่านรูปแบบออกก่อน
function isValid(t) {
return /^[ก-ฮ]{1,3} \d{1,4}$/.test(t);
}
function vote(readings) {
const count = {};
for (const r of readings.filter(isValid)) count[r] = (count[r] || 0) + 1;
const sorted = Object.entries(count).sort((a, b) => b[1] - a[1]);
return sorted.length ? sorted[0][0] : "ไม่พบ";
}
const trackA = ["กข 1234", "กข 1234", "กบ 1234", "กข 1Z34", "กข 1234"];
const trackB = ["ขค 5678", "ขค 5678", "ขค 5678"];
const trackC = ["xyz", "กข 12"];
console.log("รถ A:", vote(trackA));
console.log("รถ B:", vote(trackB));
console.log("รถ C:", vote(trackC));
**ผลลัพธ์ที่ได้จากการรันจริง**
รถ A: กข 1234
รถ B: ขค 5678
รถ C: กข 12
9. บันทึกผลเป็น CSV
ข้อความที่มีเครื่องหมายคอมมาต้องครอบด้วยเครื่องหมายคำพูด เพื่อไม่ให้คอลัมน์เลื่อน
**โค้ด: ex08_csv.js**
// บันทึกผลเป็นไฟล์ CSV เพื่อเปิดในโปรแกรมตารางคำนวณได้ทันที
// ต้องใส่เครื่องหมายคำพูดครอบค่าที่มีเครื่องหมายคอมมาหรือขึ้นบรรทัดใหม่
function toCsvLine(values) {
return values
.map(v => {
const s = String(v);
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
})
.join(",");
}
const header = ["id", "ป้าย", "เฟรมที่เห็นครั้งแรก", "ทิศทาง", "หมายเหตุ"];
const rows = [
[1, "กข 1234", 0, "ไปทางขวา", "ข้ามเส้นที่เฟรม 6"],
[2, "ขค 5678", 0, "ไปทางซ้าย", "ข้ามเส้น, ป้ายอ่านผิด 1 เฟรม"],
[3, "งจ 9999", 0, "-", "จอดอยู่, ไม่ข้ามเส้น"],
];
console.log(toCsvLine(header));
for (const r of rows) console.log(toCsvLine(r));
**ผลลัพธ์ที่ได้จากการรันจริง**
id,ป้าย,เฟรมที่เห็นครั้งแรก,ทิศทาง,หมายเหตุ
1,กข 1234,0,ไปทางขวา,ข้ามเส้นที่เฟรม 6
2,ขค 5678,0,ไปทางซ้าย,"ข้ามเส้น, ป้ายอ่านผิด 1 เฟรม"
3,งจ 9999,0,-,"จอดอยู่, ไม่ข้ามเส้น"
10. สรุปจำนวนรถรายชั่วโมง
แยกเวลาของแต่ละเหตุการณ์ที่นับได้ แล้วรวมตามชั่วโมง เพื่อดูช่วงเวลาที่มีรถมาก
**โค้ด: ex09_hourly.js**
// สรุปจำนวนรถรายชั่วโมง: แยกเวลาของแต่ละเหตุการณ์ที่นับได้ แล้วรวมตามชั่วโมง
const events = [
"08:05", "08:12", "08:47", "09:03", "09:10", "09:58", "10:02", "10:15", "10:16",
];
const perHour = {};
for (const t of events) {
const hour = t.slice(0, 2);
perHour[hour] = (perHour[hour] || 0) + 1;
}
for (const [hour, n] of Object.entries(perHour).sort()) {
console.log(`${hour}:00 - ${hour}:59 ${"#".repeat(n)} ${n} คัน`);
}
console.log("รวมทั้งหมด", events.length, "คัน");
**ผลลัพธ์ที่ได้จากการรันจริง**
08:00 - 08:59 ### 3 คัน
09:00 - 09:59 ### 3 คัน
10:00 - 10:59 ### 3 คัน
รวมทั้งหมด 9 คัน
11. ประกอบทั้งระบบ
ตัวอย่างนี้ใช้ข้อมูลจำลองแปดเฟรม รถ A และ B วิ่งในเลนของตัวเอง ส่วนรถ C จอดอยู่ ทำงานครบทุกขั้นตอน ตั้งแต่ติดตาม จับคู่ ทำความสะอาด โหวต ไปจนถึงนับ
**โค้ด: ex10_full_pipeline.js**
// ตัวอย่างรวม: ข้อมูลจำลองแต่ละเฟรม -> ติดตามรถ -> จับคู่ป้าย -> แก้ข้อความป้าย -> โหวตผล -> นับรถที่ข้ามเส้น
// ในระบบจริงข้อมูลแต่ละเฟรมจะมาจากโมเดลตรวจจับและ OCR แทนที่จะสร้างขึ้นเอง
const LINE_X = 400;
function iou(a, b) {
const ix1 = Math.max(a[0], b[0]), iy1 = Math.max(a[1], b[1]);
const ix2 = Math.min(a[2], b[2]), iy2 = Math.min(a[3], b[3]);
const inter = Math.max(0, ix2 - ix1) * Math.max(0, iy2 - iy1);
return inter / ((a[2] - a[0]) * (a[3] - a[1]) + (b[2] - b[0]) * (b[3] - b[1]) - inter);
}
function contains(outer, inner) {
return inner[0] >= outer[0] && inner[1] >= outer[1] && inner[2] <= outer[2] && inner[3] <= outer[3];
}
function cleanText(raw) {
const s = raw.replace(/\s+/g, "");
const m = s.match(/^([ก-ฮ]+)(.*)$/);
if (!m) return raw;
return m[1] + " " + m[2].replace(/O/g, "0").replace(/[lI]/g, "1").replace(/B/g, "8").replace(/S/g, "5");
}
function isValid(t) {
return /^[ก-ฮ]{1,3} \d{1,4}$/.test(t);
}
function vote(readings) {
const count = {};
for (const r of readings.filter(isValid)) count[r] = (count[r] || 0) + 1;
const sorted = Object.entries(count).sort((a, b) => b[1] - a[1]);
return sorted.length ? sorted[0][0] : "ไม่พบ";
}
// ข้อมูลจำลอง 8 เฟรม: รถ A ไปทางขวา รถ B ไปทางซ้าย รถ C จอดอยู่
const frames = [];
for (let f = 0; f < 8; f++) {
const xa = 60 + 50 * f;
const xb = 620 - 50 * f;
frames.push({
cars: [[xa, 200, xa + 120, 300], [xb, 340, xb + 120, 440], [250, 60, 370, 160]],
plates: [
{ box: [xa + 30, 270, xa + 90, 290], text: ["กข 1234", "กข 1Z34", "กข 1234", "กข 1234", "กบ 1234", "กข 1234", "กข 1234", "กข 1234"][f] },
{ box: [xb + 30, 390, xb + 90, 410], text: ["ขค 5678", "ขค 567B", "ขค 5678", "ขค 5678", "ขค 5678", "ขค 5678", "ขค 5678", "ขค 5678"][f] },
{ box: [280, 110, 340, 130], text: "งจ 9999" },
],
});
}
const tracks = [];
let nextId = 1;
for (const fr of frames) {
const used = new Set();
const carTracks = fr.cars.map(box => {
let best = null, bestIou = 0.3;
for (const t of tracks) {
if (used.has(t.id)) continue;
const v = iou(t.box, box);
if (v >= bestIou) { bestIou = v; best = t; }
}
if (!best) {
best = { id: nextId++, box, lastX: null, crossed: false, dir: "-", plates: [] };
tracks.push(best);
}
best.box = box;
used.add(best.id);
return best;
});
fr.cars.forEach((box, i) => {
const t = carTracks[i];
const cx = (box[0] + box[2]) / 2;
if (t.lastX !== null && !t.crossed && (t.lastX - LINE_X) * (cx - LINE_X) < 0) {
t.crossed = true;
t.dir = cx > LINE_X ? "ไปทางขวา" : "ไปทางซ้าย";
}
t.lastX = cx;
});
for (const p of fr.plates) {
fr.cars.forEach((box, i) => {
if (contains(box, p.box)) carTracks[i].plates.push(cleanText(p.text));
});
}
}
console.log("ID | ป้าย | ข้ามเส้น | ทิศทาง");
for (const t of tracks) {
console.log(`${t.id} | ${vote(t.plates)} | ${t.crossed ? "ใช่" : "ไม่"} | ${t.dir}`);
}
console.log("รถที่ข้ามเส้นทั้งหมด:", tracks.filter(t => t.crossed).length, "คัน");
**ผลลัพธ์ที่ได้จากการรันจริง**
ID | ป้าย | ข้ามเส้น | ทิศทาง
1 | กข 1234 | ใช่ | ไปทางขวา
2 | ขค 5678 | ใช่ | ไปทางซ้าย
3 | งจ 9999 | ไม่ | -
รถที่ข้ามเส้นทั้งหมด: 2 คัน
12. ขั้นตอนใช้โมเดลจริง
แทนที่ข้อมูลจำลองด้วยโมเดลจริง ใช้ Python และไลบรารีดังต่อไปนี้ ขั้นตอนเหล่านี้ยังไม่ได้รันในเครื่องทดสอบ
1. ติดตั้ง Python 3.12 แล้วรัน pip install ultralytics opencv-python
2. โหลดโมเดลตรวจจับรถและป้ายที่ฝึกไว้แล้ว หรือที่มีสิทธิ์ใช้งาน
3. เปิดวิดีโอด้วย OpenCV แล้วส่งทีละเฟรมเข้าโมเดล
4. ตัดภาพป้ายจากกรอบที่ได้ ส่งเข้าโมเดล OCR แล้วได้ข้อความ
5. นำผลเข้าตรรกะในตัวอย่างที่ 2 ถึง 10
ยังไม่ได้รันในบทความนี้ ผลจริงขึ้นอยู่กับโมเดล กล้อง และแสงในภาพ
ขั้นต่อไป
ขั้นต่อไปควรทดสอบกับวิดีโอจริงจากกล้องที่ติดตั้ง วัดความแม่นยำด้วยข้อมูลที่ติดฉลากแล้ว ปรับเกณฑ์ IoU และตำแหน่งเส้นตามมุมกล้อง และถ้าต้องการติดตามรถที่ทับกันมาก ให้ใช้ตัวติดตามที่ใช้ทั้งตำแหน่งและความเร็ว เช่น ByteTrack หรือ DeepSORT
สรุป
ระบบตรวจป้ายและนับรถประกอบจากการจับคู่ป้ายกับรถ การติดตามรถข้ามเฟรม การนับเมื่อข้ามเส้นครั้งแรกเท่านั้น และการทำความสะอาดข้อความจาก OCR แล้วโหวตจากหลายเฟรม ตรรกะทั้งหมดเป็นโค้ดที่รันได้จริงกับข้อมูลจำลอง