ทั้งไฟล์อ่านเป็นประโยคเดียว: "อ่านค่าดิบ → ป้อนเข้าฟิลเตอร์ → วาดดิบเทียบกรอง → วัดว่าลด noise ได้กี่ %"
ตัวเลขบรรทัด (
:46,:99) ชี้จุดที่คุณต้องเติมโค้ดในไฟล์ฝึก จำโครงนี้ไว้ เดี๋ยวไล่ทีละส่วน
ช่องเติมที่ 1 และ 2: ฟังก์ชันนี้แปลงชื่อฟิลเตอร์เป็นอ็อบเจกต์จริง สังเกต keyword argument ให้ดี:
def make_filter(name):
if name == "Median":
# เติม: return dsp.Median(window=MED_WINDOW)
pass
if name == "Kalman1D":
# เติม: return dsp.Kalman1D(q=KAL_Q, r=KAL_R)
pass
return dsp.EMA(alpha=EMA_ALPHA) # ให้ไว้แล้ว (ค่าเริ่มต้น)
pass ด้วย return dsp.Median(window=MED_WINDOW)pass ด้วย return dsp.Kalman1D(q=KAL_Q, r=KAL_R)ทำไมต้องเป็น
window=q=r=ไม่ใช่dsp.Median(5)เฉย ๆ? เพราะฝั่ง C ประกาศพารามิเตอร์เป็น keyword-only ส่ง positional จะโยนTypeErrorทันที — อ่านข้อความ error แล้วแก้เป็น keyword
ช่องเติมที่ 3: อ่านความเร่งสามแกนแล้วยุบเป็น "ขนาด" ค่าเดียวเพื่อเอาไปกรอง:
def read_raw():
global last_raw
if source == 0: # IMU
ax, ay, az = sensors.bmi270.acceleration()
# เติม: mag = (ax*ax + ay*ay + az*az) ** 0.5
mag = 0.0
pass
last_raw = mag * 10.0 # x10 ให้เห็นชัดบนกราฟ
...
return last_raw
mag = (ax*ax + ay*ay + az*az) ** 0.5 — นี่คือความยาวเวกเตอร์ (Pythagoras 3 มิติ)mag ค้างที่ 0 → ทั้งสองกราฟแบนราบ ไม่มีอะไรให้กรองทำไมยุบสามแกนเป็นค่าเดียว? เพราะฟิลเตอร์ในชุดบทเรียนนี้เป็น 1 มิติ (
Kalman1D— เลข 1 บอกอยู่) เราจึงต้องแปลงเวกเตอร์เป็นสเกลาร์ก่อน (การ fuse หลายแกนจริง ๆ คือdsp.Madgwickเก็บไว้บทเรียนเรื่อง IMU เชิงลึก)
ช่องเติมที่ 4: บรรทัดเดียวที่เป็นหัวใจทั้งชุดบทเรียน ป้อนค่าดิบเข้าฟิลเตอร์ รับค่าที่กรองแล้วออกมา:
x = read_raw() # ค่าดิบ (มีหนาม)
# เติม: y = filt.update(x)
y = x
pass
y = filt.update(x) — filt คืออ็อบเจกต์ที่ make_filter() สร้างไว้y = x): เส้นล่างจะเท่าเส้นบนเป๊ะ — ไม่มีการกรองเลย (บททดสอบง่าย ๆ ว่าเติมถูกไหม: สองกราฟต้องต่างกัน)filt เปลี่ยนได้ตอน runtime เมื่อผู้ใช้เลือก dropdown — แต่โค้ดบรรทัดนี้ไม่ต้องรู้ว่าเป็นตัวไหน เพราะ API เหมือนกันหมดพลังของ API ร่วม: บรรทัด
filt.update(x)เขียนครั้งเดียว ใช้ได้กับทั้ง EMA/Median/Kalman โดยไม่ต้อง if แยก — นี่คือเหตุผลที่ทุกฟิลเตอร์ออกแบบให้หน้าตาเหมือนกัน
ช่องเติมที่ 5: เอาค่าดิบกับค่ากรองขึ้นสองกราฟ ให้ตาเทียบกันได้:
# เติม: raw_chart.value(clamp(x)) แล้ว filt_chart.value(clamp(y))
pass
raw_jit += abs(x - prev_x) # การกระตุกของเส้นดิบ
filt_jit += abs(y - prev_y) # การกระตุกของเส้นกรอง
...
red = (1.0 - filt_jit / raw_jit) * 100.0 # ลด jitter ได้กี่ %
pass ด้วยสองบรรทัด: raw_chart.value(clamp(x)) และ filt_chart.value(clamp(y))clamp() คุมค่าให้อยู่ในช่วงกราฟ 0..250 (กันเส้นทะลุขอบ)noise down % วัดจาก การกระตุกระหว่างเฟรม (ผลรวม |x - x_prev|) — ไม่ต้องมีสัญญาณจริงให้เทียบ ใช้กับเซนเซอร์จริงได้เลยเราวัด "ความเรียบ" ด้วย jitter เพราะกับเซนเซอร์จริงเราไม่มี "เฉลย" ว่าค่าจริงคือเท่าไร — วิธีนี้ตรงไปตรงมา: ฟิลเตอร์ดี = การกระตุกลดลง
ไม่มีบอร์ดก็เริ่มได้ (IMU จำลองมีให้ครบ):
s08_filters.py# เติม:แหล่ง Radar range บน Emulator เป็นระยะจำลองที่หมุนด้วยลูกบิด ไม่มี multipath จริง — ใช้ IMU accel เป็นหลักตอนซ้อม แล้วค่อยลอง radar จริงบนบอร์ด
นี่คือของจริงที่รันได้ในเบราว์เซอร์ ไม่ต้องมีบอร์ดก็เห็นสัญญาณ IMU ขยับตามมือ:

จอ emulator ที่รันได้จริง — IMU dashboard บน BENTO Edge AI Emulator (ide.tesaiot.dev) ค่าดิบขยับตามที่เราลากปุ่ม Shake
x ตัวเดียวกับที่เราจะป้อนเข้า filt.update(x) ในแอป s08_filters.pyเห็นเลขเต้น ๆ ตรงนี้ไหม นั่นแหละหนามที่เราจะกรอง — จอนี้คือสนามซ้อมก่อนจะไปเจอสัญญาณจริงบนบอร์ดจริง
บนบอร์ดจริงได้ครบทั้ง IMU และ radar range profile ของจริง:
s08_filters.py ใน BENTO IDE กด Program to Deviceจุดที่ควรสังเกต: ในโหมด radar ลอง EMA ก่อน จะเห็น spike ถูกเกลี่ยเป็นโหนกนุ่ม ๆ แต่ยังอยู่ — พอสลับเป็น Median โหนกหายไปเลย นี่คือความต่างของ "เกลี่ย" กับ "โหวตตัด"
เปิด s08_filters.py มี pass/placeholder วางไว้ 5 จุด ตรงที่ต้องเติมของจริง:
| # | จุด | เติมด้วย | ถ้าลืม |
|---|---|---|---|
| 1 | make_filter (Median) | return dsp.Median(window=MED_WINDOW) |
เลือก Median แล้วพัง |
| 2 | make_filter (Kalman) | return dsp.Kalman1D(q=KAL_Q, r=KAL_R) |
เลือก Kalman แล้วพัง |
| 3 | read_raw | mag = (ax*ax+ay*ay+az*az) ** 0.5 |
สองกราฟแบนราบ |
| 4 | ในลูป | y = filt.update(x) |
เส้นล่าง = เส้นบน (ไม่กรอง) |
| 5 | ในลูป | raw_chart.value(clamp(x)) + filt_chart.value(clamp(y)) |
จอไม่ขึ้นกราฟ |
ขั้นตอน:
# เติม: ทีละจุด แทน pass/placeholder ด้วยคำสั่งตามคำใบ้ห้าช่องนี้ประกอบเป็นไปป์ไลน์ Analysis ที่สมบูรณ์: สร้างฟิลเตอร์ → อ่านค่า → กรอง → เห็นผล เติมครบเมื่อไร คุณทำสัญญาณจริงให้สะอาดได้ด้วยมือตัวเอง
ถ้าอยากเข้าใจฟิลเตอร์ให้ลึกกว่านี้ สามช่องนี้อธิบายด้วยภาพเคลื่อนไหวเข้าใจง่ายมาก:
วิดีโอ (อธิบายเชิงสัญชาตญาณ)
ภาพ/เอกสารอ้างอิง (เปิดอ่านต่อได้)
วิดีโอ/ภาพภายนอกเป็นของเจ้าของต้นฉบับ ใช้เพื่อการศึกษา อ้างอิงลิงก์ต้นทาง — เปิดดูเป็นการบ้านเสริม ไม่บังคับ แต่ช่วยให้ภาพสูตรที่เราแกะกันวันนี้ชัดขึ้นอีกมาก
MVP ของบทเรียน 4.1–4.2 (เกณฑ์ผ่านของชุดบทเรียน): คุณเปิดแอป s08_filters.py แล้วทำให้ฟิลเตอร์ ปรับสัญญาณเซนเซอร์ที่มี noise ให้ดีขึ้นอย่างเห็นได้ — เส้น Filtered เรียบกว่าเส้น Raw ชัดเจน พร้อมตัวเลข noise down % ยืนยัน
"กรองดีขึ้น" ไม่ใช่แค่ "เส้นดูสวย" — คุณต้องบอกได้ว่าทำไม Median ชนะ EMA ตอนเจอ spike และทำไม α เล็กลงถึงเรียบขึ้นแต่ตามช้าลง
ถ้าติด ให้ไต่บันไดนี้ทีละขั้น อย่าเพิ่งกระโดดไปดูเฉลย เพราะของจะเข้าหัวตอนที่คุณพยายามเองก่อน:
# เติม: ทั้ง 5 จุดในไฟล์ฝึก + ตารางหน้าที่แล้ว บอกว่าแต่ละช่องเติมอะไรs08_filters.py มีโครงครบทั้งไฟล์แล้ว เหลือ 5 จุดให้เติม (make_filter 2 จุด + ในลูป 3 จุด)s08_filters.py เติมครบพร้อมคอมเมนต์อธิบายทุกช่อง (อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์เอง)s08_filters_full.py เพิ่ม slider จูนพารามิเตอร์สด (α/window/r), แถบ noise down เปลี่ยนสีตามเกณฑ์, ปุ่ม Freeze จับภาพหนามนิ่ง ๆลองเขียนเองให้สุดก่อนนะ ถ้าติดจริง ๆ ค่อยเปิดเฉลยดูทีละช่อง แล้วกลับมาพิมพ์เอง — เดี๋ยวเราค่อย ๆ แกะไปด้วยกัน
การกรองสัญญาณตัวแรกซ่อนแนวคิด Analysis หลายชั้นที่จะใช้ต่อไปตลอดเสา 3–5:
ฝั่ง Analysis / สัญญาณ
.update(x) ทีละค่า (streaming) เหมาะกับ MCU ที่ RAM น้อยฝั่ง MicroPython / โครงโปรแกรม
.update/.value/.reset เหมือนกันทุกตัว → สลับฟิลเตอร์โดยไม่แก้ลูปradar_range() โยน OSError เมื่อ radar DSP ไม่ทำงาน เราจึงห่อไว้ (ซ้ำบทเรียน 1.1–1.3)ทั้งหมดนี้คือประตูสู่ บทเรียน 4.3–4.4 (FFT — มองสัญญาณในโดเมนความถี่) และ บทเรียน 4.5–4.6 (feature ที่โมเดลกินจริง) — กรองให้สะอาดก่อน แล้วค่อยสกัด feature เสมอ
ฟิลเตอร์สามตัวที่เล่นวันนี้ไม่ใช่ของสมมติ ทุกตัวอยู่ในสินค้าจริงที่คุณใช้ทุกวัน:
เห็นไหมว่าฟิลเตอร์ไม่ใช่ของเล่นในห้องเรียน — มันคือชั้นแรกที่ทำให้สัญญาณ "อ่านออก" ทั้งสำหรับคนและสำหรับโมเดล เราแค่กำลังเริ่มจับเครื่องมือชุดนี้
งานทำเอง (ท้ายบทเรียน):
s08_filters.py ให้ครบทั้ง 5 ช่อง รันได้จริง (Emulator หรือบอร์ด) — เส้น Filtered เรียบกว่า Rawnoise down % ของแต่ละตัวกับสัญญาณเดียวกันใบ้ข้อ 3 — spike คือค่าโดดสุดขั้วครั้งเดียว EMA จะเกลี่ยมันเป็นโหนกที่ยังเห็น ส่วน Median จะโหวตมันตกไปเลย
วันนี้เราได้: เข้าใจว่า noise คืออะไรและมาจากไหน · รู้จักฟิลเตอร์สามตระกูล (EMA/Median/Kalman1D) + API ร่วม · เห็น radar range profile ของจริง · ทำสัญญาณจริงให้สะอาดสด ๆ พร้อมวัดผลเป็น %
ชุดบทเรียนถัดไป (บทเรียน 4.3–4.4) เราจะมองสัญญาณเดียวกันนี้ใน โดเมนความถี่ ด้วย FFT — จากการดูว่า "ค่าขึ้นลงยังไงตามเวลา" ไปสู่ "มีความถี่อะไรซ่อนอยู่บ้าง" เจอกันครับ