จบชุดบทเรียนนี้เราจะเดินครบ แล้วปิดท้ายด้วยการทำสัญญาณจริงให้สะอาดสด ๆ:
dsp.EMA (ถัวเฉลี่ยถ่วงน้ำหนัก) · dsp.Median (ตัดหนามโดด) · dsp.Kalman1D (ชั่งน้ำหนัก predict/measure).update(x) / .value() / .reset() — สร้างครั้งเดียว ป้อนทีละค่าs08_filters.py เขย่าบอร์ด ดูเส้นดิบเทียบเส้นกรอง + เปอร์เซ็นต์ลดสัญญาณรบกวนปลายทางของวันนี้: เลือกฟิลเตอร์ใน dropdown ขยับบอร์ด แล้วเห็นเส้นล่างสะอาดกว่าเส้นบนอย่างชัดเจน พร้อมตัวเลข "noise down %"
วันนี้เราเน้น "กรองเป็น + อธิบายข้อแลกเปลี่ยนได้" ส่วน FFT กับ feature extraction เก็บไว้เป็น บทเรียน 4.3–4.6 ต่อจากนี้
เราเข้าเสาที่ 3 แล้ว — Analysis ต่อจาก Processing (โมดูล 3) ที่เราแปลงสัญญาณดิบเป็นปริมาณที่มีความหมาย (tilt, พลังงาน, dBFS)
Processing แปลง "หน่วย" (accel → tilt), Analysis จัดการ "รูปร่างของสัญญาณตามเวลา/ความถี่" ฟิลเตอร์วันนี้คือก้าวแรก ก่อนจะไป FFT (บทเรียน 4.3–4.4) และ feature ที่โมเดลกินจริง (บทเรียน 4.5–4.6)
บทเรียน 3.1–3.2 เราเอาสัญญาณดิบมาแปลงเป็นปริมาณที่มีความหมาย เช่น dsp.tilt(ax,ay,az) คืน (roll,pitch) แล้วโชว์เป็นเกจ
dsp — บทเรียน 3.1–3.2 ใช้ฟังก์ชันฟิสิกส์ (tilt/altitude) วันนี้เราใช้คลาสฟิลเตอร์ (EMA/Median/Kalman1D)ถ้าบทเรียน 3.1–3.2 คุณเคยรำคาญว่า "ทำไมเกจกระพริบจัง" — ชุดบทเรียนนี้แหละคือคำตอบ เราจะจับความกระพริบนั้นมากดให้นิ่ง
ไม่มีเซนเซอร์ตัวไหนในโลกให้ค่าเรียบ ๆ ทุกอย่างมี หนาม (noise) ปนมาด้วย บางทีก็หนามเล็ก ๆ กระจาย บางทีก็ค่าโดดสุดขั้ว (outlier)
นี่คือเหตุผลที่ Analysis มาก่อน Training เสมอ — ถ้าป้อนสัญญาณสกปรกให้โมเดล มันจะเรียน "หนาม" ไปด้วย แล้วพังตอนใช้จริง
เปิด s08_filters_full.py (หรือ s08_filters.py ที่เติมครบแล้ว) จะเห็นสองกราฟซ้อนกัน:
ลองเลย: เขย่าบอร์ดเบา ๆ → เส้นบนเต้นแรง เส้นล่างตามช้ากว่าและเรียบกว่า แล้วสลับฟิลเตอร์ใน dropdown ดูว่าเส้นล่างเปลี่ยนนิสัยยังไง
อย่าเพิ่งอ่านโค้ด รันก่อน เล่นก่อน ให้ตาเห็นความต่างของสามฟิลเตอร์ด้วยมือตัวเอง แล้วค่อยกลับมาถามว่า "มันต่างกันเพราะอะไร"
ก่อนจะกรอง เราต้องรู้ว่ากำลังสู้กับอะไร noise มีหลายหน้า และแต่ละหน้าเรียกฟิลเตอร์คนละตัว
| ชนิด | หน้าตา | มาจาก | ใครจัดการเก่ง |
|---|---|---|---|
| white noise | หนามเล็กกระจายรอบค่าจริง | ความร้อน · วงจร · ADC | EMA / SMA (ถัวเฉลี่ย) |
| spike / outlier | ค่าโดดสุดขั้วเป็นครั้งคราว | multipath · ชนสัญญาณ · บิตพลิก | Median (โหวตค่ากลาง) |
| drift | ค่าค่อย ๆ เลื่อนช้า ๆ | อุณหภูมิ · แบตหมด | HPF (ตัดของช้า) |
| สั่นเร็วปนช้า | จริงบ้างรบกวนบ้าง | ทุกอย่างรวมกัน | Kalman (ชั่งน้ำหนัก) |
การถามว่า "สัญญาณของฉันสกปรกแบบไหน" คือทักษะวิศวกรที่แท้จริง — ก่อนหยิบเครื่องมือ ให้มองศัตรูก่อน
หัวใจของฟิลเตอร์ตามเวลา (temporal filter): มันไม่ได้มองแค่ค่าปัจจุบันค่าเดียว แต่ จำอดีต แล้วเอามาถ่วงกับค่าใหม่
dsp ทำงานแบบ streaming: ป้อนทีละค่าด้วย .update(x) มันเก็บ state ไว้ในตัวแล้วคืนค่าที่กรองแล้ว"ความเรียบ" มาจากการที่ฟิลเตอร์ไม่ยอมกระโดดตามค่าใหม่ทันที มันถ่วงด้วยอดีต — ยิ่งถ่วงหนัก ยิ่งเรียบ แต่ก็ยิ่งตอบสนองช้า นี่คือข้อแลกเปลี่ยนหลักของทั้งชุดบทเรียน
โมดูล dsp (ฝั่ง C บน CM33) มีคลาสฟิลเตอร์พร้อมใช้หลายตัว ทุกตัวหน้าตา API เหมือนกันเป๊ะ วันนี้เราโฟกัสสามตัว:
| คลาส | สร้างด้วย | เก่งเรื่อง | ความจำ |
|---|---|---|---|
dsp.EMA |
EMA(alpha=0.15) |
หนามเล็กกระจาย | ค่าเดียว (ถูกสุด) |
dsp.SMA |
SMA(window=10) |
หนามเล็ก + ค่าเฉลี่ยตรง | buffer N ค่า |
dsp.Median |
Median(window=5) |
spike / outlier โดด ๆ | buffer N ค่า |
dsp.LPF |
LPF(cutoff=5, fs=100) |
ตัดของเร็ว (ความถี่สูง) | ค่าเดียว |
dsp.HPF |
HPF(cutoff=0.5, fs=100) |
ตัด drift (ความถี่ต่ำ) | ค่าเดียว |
dsp.Kalman1D |
Kalman1D(q=0.02, r=0.6) |
ผสม จริง+รบกวน | ค่าเดียว + ความมั่นใจ |
alpha=, window=, q=, r=) ส่งแบบ positional เช่น dsp.EMA(0.15) จะโยน TypeError ทันทีEMA / Median / Kalman1D (อีกสามตัวเป็นการบ้านต่อยอด)ฝั่ง C เขียนฟิลเตอร์ให้เสร็จ เรากลายเป็นคนเลือกและจูน — นี่คือ 70% ที่ได้ฟรี เราลงมือกับ 30% ที่เหลือคือ "เลือกให้ถูก + ตั้งค่าให้พอดี"
จำสามเมท็อดนี้ตัวเดียว ใช้ได้กับฟิลเตอร์ทุกตัวในโมดูล dsp:
import dsp
f = dsp.EMA(alpha=0.15) # 1) สร้างครั้งเดียว (นอกลูป!)
while True:
x = read_sensor() # ค่าดิบหนึ่งตัว
y = f.update(x) # 2) ป้อนเข้า -> คืนค่าที่กรองแล้ว
last = f.value() # ดูค่าล่าสุดโดยไม่ป้อนใหม่
f.reset() # 3) ล้าง state (เช่นตอนสลับแหล่งสัญญาณ)
.update(x) — หัวใจ: ป้อนตัวอย่างใหม่หนึ่งค่า คืนค่าที่กรองแล้ว (เรียกทุกวนรอบ).value() — คืนค่าล่าสุดที่กรองไว้ ไม่เปลี่ยน state (เอาไว้อ่านซ้ำ).reset() — ล้างความจำกลับเป็นศูนย์ (เริ่มนับใหม่)สร้างครั้งเดียวนอกลูป สำคัญมาก เพราะ state (ความจำ) อยู่ในอ็อบเจกต์ ถ้าสร้างใหม่ทุกวนรอบ = ลบความจำทุกครั้ง ฟิลเตอร์จะไม่มีวันเรียบ (นี่คือบั๊กยอดฮิต)
EMA (Exponential Moving Average) ฟิลเตอร์ที่ถูกและใช้บ่อยสุด สูตรมีแค่บรรทัดเดียว:
คำถามวิศวกร: "ฉันยอมช้าแค่ไหนเพื่อแลกความเรียบ?" — งานตรวจการล้มต้องไว เลือก α ใหญ่หน่อย · งานวัดอุณหภูมิห้องยอมช้าได้ เลือก α เล็ก ในไฟล์เราตั้ง
EMA_ALPHA = 0.15เป็นจุดตั้งต้นที่สมดุล
Median filter เก่งคนละเรื่องกับ EMA มันไม่ถัวเฉลี่ย แต่ เรียงค่าล่าสุด N ตัวแล้วเอาค่ากลาง — ค่าโดดสุดขั้วจึงไม่มีสิทธิ์ชนะ
dsp.Median(window=5) — เฟิร์มแวร์บังคับ window เป็นเลขคี่ (3..15) เพื่อให้มีค่ากลางชัดนี่คือฟิลเตอร์ที่ตัวอย่างเรดาร์ (
05_radar_distance.py) ใช้จริง เพราะระยะจากเรดาร์ชอบกระโดดเพราะ multipath — median กันได้ตรงจุด เดี๋ยวเราจะเห็นมันในหน้า radar range profile
Kalman filter ฉลาดกว่าเพื่อน มันเก็บทั้งค่าประมาณ และ ความไม่มั่นใจ แล้วทุกก้าวจะชั่งน้ำหนักว่าจะเชื่อการวัดใหม่แค่ไหน
dsp.Kalman1D(q=0.02, r=0.6) — สองปุ่มปรับ:
q (process noise) ต่ำ = เชื่อว่าสัญญาณจริง "นิ่ง" → เรียบมาก · สูง = ยอมให้ค่าจริงขยับเร็วr (measurement noise) สูง = "ไม่ค่อยเชื่อเซนเซอร์" (มองว่า noisy) → กรองหนัก · ต่ำ = เชื่อการวัดเริ่มจูนง่าย ๆ: ล็อก
qไว้เล็ก ๆ แล้วเล่นกับrอย่างเดียว —rสูงขึ้น = เรียบขึ้นแต่ตามช้าลง (คุ้น ๆ ไหม มันคือข้อแลกเปลี่ยนเดิม แค่คนละหน้าตา)
เส้นเขียว (เรียบ) ค่อย ๆ วาดทับเส้นแดง (ดิบ) — เห็นแนวโน้มจริงโดยไม่ถูกหนามหลอก
สูตร EMA มีบรรทัดเดียว ท่องได้เลย แล้วเราจะแกะทีละสัญลักษณ์ว่ามันแปลว่าอะไร:
อ่านเป็นประโยคเดียว: ค่าใหม่หนึ่งส่วน ผสมกับความทรงจำเดิมอีก ส่วน
ทำไมสำคัญกับชุดบทเรียนนี้: ค่า
EMA_ALPHA = 0.15ในไฟล์คือการเลือกว่า "เชื่อของใหม่ 15% เชื่อความจำเดิม 85%" นั่นแหละคือปุ่มเดียวที่ตัดสินว่าเส้นล่างจะเรียบแค่ไหน ตามช้าแค่ไหน — เข้าใจสูตรนี้ตัวเดียว จูน EMA เป็นทันที
หัวใจของ Kalman1D คือค่าเดียวชื่อ Kalman gain ที่คำนวณใหม่ทุกก้าว:
ลองแทนค่าให้เห็นภาพ:
ทำไมสำคัญกับชุดบทเรียนนี้: EMA ใช้ คงที่ ทั้งชีวิต แต่ Kalman คำนวณ ใหม่ทุกก้าวตามความมั่นใจ — นี่คือเหตุผลที่มันฉลาดกว่า แต่ก็มีสองปุ่ม (
q,r) ให้จูน ไม่ใช่ปุ่มเดียวเหมือน EMA เทียบสองสูตรนี้ข้าง ๆ กัน คุณจะเห็นว่า Kalman ก็คือ EMA ที่ปรับ ของตัวเองได้
ไม่มีตัวชนะขาด เลือกให้ตรงกับหน้าของ noise และงบประมาณ (CPU/RAM) ที่มี
| EMA | Median | Kalman1D | |
|---|---|---|---|
| เก่งเรื่อง | หนามเล็กกระจาย | spike / outlier | ผสม จริง+รบกวน |
| ราคา (CPU/RAM) | ถูกสุด (ค่าเดียว) | กลาง (buffer N) | กลาง (คณิตนิดหน่อย) |
| ปุ่มปรับ | alpha |
window |
q, r |
| จุดอ่อน | spike ยังเล็ดลอด | ตอบขอบคมช้า | ต้องจูน 2 ค่า |
| เหมาะกับ | เกจอุณหภูมิ/แสง | ระยะเรดาร์/GPS | IMU fusion ละเอียด |
ในงานจริงเรามัก ต่อฟิลเตอร์กัน เช่น Median กัน spike ก่อน แล้ว EMA เกลี่ยให้เนียนต่อ — วันนี้เราลองทีละตัวก่อน ให้เข้าใจนิสัยของแต่ละตัวชัด ๆ
ตัวอย่างอ้างอิงของชุดบทเรียนนี้คือ 05_radar_distance.py — ไม้วัดระยะด้วยเรดาร์ 60 GHz ที่โชว์ว่าการกรองสำคัญแค่ไหนกับสัญญาณจริง
sensors.radar_range() คืน dict: {'distance_m','peak_db','resolution_m','target','seq'} — ฝั่ง C ทำ HPF→FFT→dB→peak ให้หมดแล้วresolution_m) — บอกโซนได้ แต่ระยะชอบ กระโดด เพราะการสะท้อนหลายทาง (multipath)
sensors.radar_range()ให้ระยะจริงบน บอร์ดจริง เท่านั้น (บน Emulator เป็นระยะจำลองที่หมุนด้วยลูกบิด) และบนบอร์ดจะโยนOSErrorถ้า radar DSP ยังไม่ทำงาน — โค้ดเราจึงห่อtry/except OSErrorไว้ คงเส้นนิ่งแทนที่จะพัง (บทเรียนเดียวกับselect()ใน บทเรียน 1.1–1.3)