// BLOG — 2026-08-27

P2.1 — Một cái 'máy nghe' báo nhầm cả đêm: lọc false positive của camera trông bé như thế nào

Case study lọc false positive cho camera nghe tiếng khóc bằng band-pass, debounce, cooldown và cross-check cảm biến, để giảm báo nhầm mà vẫn giữ tiếng khóc thật.

# Một cái 'máy nghe' báo nhầm cả đêm: lọc false positive của camera trông bé như thế nào

Bài này nằm trong chuỗi mổ xẻ kỹ thuật tại zeodavu.com/blog. Mình ghi lại một vấn đề thật, nhỏ và rất đời: một chiếc camera trông bé có tính năng nghe tiếng khóc, nhưng nó báo nhầm liên tục. Không có recipe để sao chép; chỉ có cấu trúc, con số đo được, và cái giá phải trả khi đánh đổi độ nhạy lấy độ đúng.


Vấn đề mở đầu nghe rất quen: trông bé qua một chiếc camera có tính năng "nghe tiếng khóc". Đặt ở phòng bé, nó hoạt động theo cảm biến âm thanh: nghe thấy âm thanh nào đó giống tiếng khóc là báo lên điện thoại. Lý tưởng, đúng chức năng. Thực tế, nó báo cả đêm — vì trong phòng có quạt, có máy lạnh, có gió.

Bài này mổ xẻ vì sao chuyện xảy ra, và mình đã phải "dạy" lại cái máy nghe ấy theo cách nào cho hết báo nhầm mà vẫn không lỡ tiếng khóc thật.


Vì sao một "máy nghe" lại báo nhầm

Cảm biến âm thanh của camera không "hiểu" tiếng khóc. Nó chỉ làm một việc đơn giản: đo độ lớn âm thanh trong một khoảng thời gian, rồi so với một ngưỡng. Vượt ngưỡng = báo.

# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai
# Cấp 1: đo năng lượng cửa sổ, so ngưỡng — bản "ngây thơ"
def detect_sound(frame_audio, sample_rate, threshold):
    # Năng lượng (RMS) trên một cửa sổ ngắn
    rms = energy(frame_audio)                 # độ lớn trung bình của đoạn âm
    return rms > threshold                    # vượt ngưỡng -> báo
python

Vấn đề nằm ngay dòng này. energy là một con số không phân biệt nguồn âm. Nó bằng nhau cho một tiếng khóc và cho một cơn gió mạnh thổi qua loa quạt — miễn là hai thứ đó cùng "độ lớn". Mà trong phòng bé, gió máy lạnh, tiếng quạt, tiếng máy lọc không khí chính là nguồn âm dồi dào nhất, đặc biệt về đêm.

Kết quả: một ngưỡng đặt "đủ nhạy để bắt tiếng khóc" gần như chắc chắn cũng đủ nhạy để bắt gió. Đặt ngưỡng cao hơn thì hết báo nhầm, nhưng tiếng khóc nhỏ giữa đêm cũng mất.

Một cảm biến "nghe" không cần hiểu âm thanh để báo đúng. Nó chỉ cần phân biệt được nguồn âm mình cần với nguồn âm mình không cần. Khó ở chỗ cả hai thường có cùng độ lớn.


Bản "ngây thơ" có một điểm mù: nó chỉ nhìn độ lớn, không nhìn bản chất

Chỗ thiếu sót cốt lõi: thiết bị chỉ nhìn biên độ trong khi vấn đề là tần số và độ ổn định.

  • Phổ tần số khác nhau. Tiếng khóc tập trung ở một dải tần khá hẹp, kèm sự dao động lên xuống theo từng nhịp thở. Gió / tiếng quạt thì rải đều và rất tĩnh — biên độ hầu như không đổi theo thời gian.
  • Tính ổn định khác nhau. Một cơn gió là một dạng năng lượng "phẳng", trơn tru. Tiếng khóc là một chuỗi "đỉnh - lặng - đỉnh" lặp lại. Chỗ khác biệt này là thứ có thể dùng để phân loại.

Vậy hướng đi là bỏ cách so ngưỡng đơn thuần, chuyển sang kết hợp nhiều tầng "lọc" trước khi quyết định báo. Mỗi tầng giết một loại nhiễu.

Đừng cố làm cảm biến "thông minh" để hiểu tiếng khóc. Hãy để nó loại trừ những thứ chắc chắn không phải tiếng khóc. Con đường loại trừ thường rẻ và chắc hơn con đường nhận diện.


Lọc nhiều tầng: từ độ lớn tới "chữ ký" của tiếng khóc

Tầng 1 — chỉ nghe ở dải tần của tiếng khóc

Thay vì đo năng lượng toàn dải, chỉ giữ lại một dải tần quan tâm rồi mới đo. Phần còn lại (tiếng quạt trầm, tiếng gió thấp, tiếng máy lọc trầm) phần lớn bị loại sẵn ở tầng này.

# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai
# Tầng 1: band-pass về dải tần quan tâm trước khi đo độ lớn
def detect_sound(frame_audio, sample_rate):
    x = bandpass(frame_audio, sample_rate, low, high)  # chỉ giữ dải của tiếng khóc
    rms = energy(x)
    return rms > threshold
python

Đây là bước làm sạch đầu tiên, loại bỏ phần lớn "tiếng ồn nền" mà không làm giảm độ nhạy với tiếng khóc.

Tầng 2 — đòi hỏi âm thanh phải "bền" một chút, không phải một chớp

Giải quyết vấn đề tiếng động ngắn, lẻ (một tiếng gõ, một cái tách, tiếng chạm cửa). Đặt một điều kiện: âm phải duy trì trên ngưỡng một khoảng thời gian liên tục thì mới tính. Một âm thanh kéo dài từng khúc mới đủ tư cách báo. Đây là kỹ thuật chống nhiễu khá phổ biến (debounce).

# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai
# Tầng 2: cần tồn tại đủ lâu trên ngưỡng thì mới báo (chống âm lẻ)
def detect_sound(frame_audio, sample_rate):
    rms = energy(bandpass(frame_audio, sample_rate, low, high))
    if rms > threshold:
        persist_count += 1
        if persist_count >= MIN_HITS:          # đủ dài liên tục
            return TRIGGER
    else:
        persist_count = 0                       # rớt ngưỡng -> đếm lại
    return IDLE
python

Tầng 3 — chống báo lặp: cần một khoảng "nghỉ" sau khi đã báo

Sau khi đã báo một lần, máy không được phép báo lại ngay nếu âm vẫn còn đó (hoặc có một âm tương tự ngay sau). Cho nó một khoảng cooldown — sau mỗi lần báo đúng, phải chờ qua một khoảng thời gian "yên" trước khi được báo lần kế tiếp.

# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai
# Tầng 3: hysteresis + cooldown chống spam báo lặp
if state == WAITING and rms > trigger_high:
    fire_alert()                 # báo 1 lần
    state = COOLDOWN
    last_fire = now

if state == COOLDOWN:
    if now - last_fire > COOLDOWN_MS and rms < trigger_low:
        state = WAITING          # hết cooldown + đã yên -> nghe tiếp
python

Ở đây dùng hai ngưỡng (một ngưỡng báo, một ngưỡng "thôi báo") để tránh hiện tượng bấp bênh — âm cứ lơ lửng quanh một mức là máy cứ báo/tắt liên tục.

Tầng 4 — chỉ nghe vào lúc cần nghe

Đây là tầng "thời gian". Ban đêm, thời điểm trông bé, mình muốn độ nhạy cao nhất. Ban ngày, khi có người lớn trong phòng và bé thức, độ nhạy này gây phiền. Một lịch (schedule) ấn định khi nào thì "tai" hoạt động ở mức nào là một bộ lọc rất rẻ mà cực hiệu quả.

# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai
# Tầng 4: windowed gating — chỉ nghe vào khung giờ cho phép
def should_listen(now):
    return now in night_window            # chỉ nghe trong khung giờ ngủ
python

Một bộ lọc không chỉ ở tầng âm thanh. Có thể đặt "tai" của máy nghe theo thời gian, theo vị trí, theo bối cảnh. Mỗi tầng như vậy là một lần giảm nhiễu mà gần như không tốn gì.


Tầng quan trọng nhất: xác nhận chéo bằng một thiết bị khác

Các tầng trên đều xử lý bên trong "tai" của một thiết bị. Nhưng có một giới hạn: một cảm biến câm chỉ phân biệt được nguồn âm dựa trên tín hiệu của chính nó. Nó không biết bé có đang ở trong phòng không.

Chỗ nâng cấp mạnh nhất là đưa một nguồn thông tin thứ hai vào để xác nhận. Mình có một thiết bị phát hiện chuyển động/không gian riêng (một "bộ cảm biến" đặt độc lập). Nó không nghe, nhưng nó biết khi nào trong phòng có hoạt động.

Khi camera báo "nghe thấy có gì đó giống tiếng khóc", mình không báo ngay. Mình đối chiếu chéo: trong cùng khoảng thời gian, thiết bị kia có thấy chuyển động / có gì đó bất thường trong phòng không? Nếu → mới báo là "nghi ngờ". Nếu không → coi như nhiễu, bỏ qua.

# pseudo-code minh hoạ — KHÔNG phải nguồn triển khai
# Tầng 5: cross-check — chỉ báo khi 2 nguồn khớp nhau trong cùng cửa sổ
def should_alert(audio_event, room_event):
    # audio_event: camera nghe thấy dấu hiệu giống tiếng khóc
    # room_event:  thiết bị khác thấy chuyển động trong phòng
    if audio_event and room_event:
        return HIGH_CONFIDENCE   # 2 nguồn khớp -> tin tưởng
    if audio_event:
        return LOW_CONFIDENCE    # chỉ 1 nguồn -> ghi nhớ, chưa vội báo
    return IDLE
python

Đây là cách dùng "bộ tai" kết hợp "bộ mắt + bộ cảm biến không gian". Cả hai cùng nói "có chuyện" thì xác suất đúng cao hơn nhiều so với một mình.


Kết quả: con số trước & sau

Sau khi dựng đủ các tầng, mình đo lại số lần báo trong một đêm. Đây là con số thật, đối chứng trước và sau khi áp dụng lọc:

| Chế độ | Số lần báo trong một đêm | Số lần đúng (be thật) |

|---|:---:|:---:|

| Không lọc (chỉ so ngưỡng) | ~20–30 lần | 1–2 lần |

| Lọc tần số + debounce + cooldown | ~4–6 lần | 1–2 lần |

| Thêm cross-check thiết bị khác | 1–2 lần | 1–2 lần |

Điểm đáng chú ý không phải là số lần báo giảm, mà là: cột phải (số lần đúng) không đổi. Trước khi lọc, cứ báo 20–30 lần thì mới có 1–2 lần đúng. Sau khi lọc xong, gần như cứ báo là đúng. Cái máy nghe chuyển từ "ồn ào mà mù" sang "ít nói mà chuẩn".

Và điều quan trọng hơn cả: không đánh mất cảnh báo thật. Tiếng khóc nhỏ giữa đêm vẫn lọt qua, vẫn báo.

Mục tiêu không phải "hết báo nhầm". Mục tiêu là "báo đúng tới mức mình còn tin cái báo." Giảm số lần báo xuống để người có thể tin từng cái một — đó mới là thắng lợi thật sự.


Cái giá phải trả

Lọc nhiều tầng không miễn phí — nó đổi một thứ này lấy một thứ khác.

Đánh đổi 1 — phản ứng chậm hơn. Vì đòi hỏi âm phải "bền" và phải đối chiếu chéo, hệ thống không báo tức thì. Một tiếng khóc rất ngắn, hoặc chuyển động của một thiết bị không bắt được, có thể bị bỏ qua. Độ trễ này (vài giây tới vài chục giây) là phí để đổi lấy độ đúng.

Đánh đổi 2 — phụ thuộc vào nhiều thiết bị. Cross-check chỉ mạnh khi thiết bị phụ vẫn hoạt động. Nếu nó hết pin, mất kết nối, hoặc nằm đúng nơi không phủ được, các tầng lọc vẫn xử lý bên trong camera, nhưng mức tin cậy tăng lên nhờ cross-check sẽ tụt xuống. Chỗ này nên có phương án dự phòng, hoặc chấp nhận nói chuyện lên "có thể nhiễu".

Đánh đổi 3 — cấu hình theo bối cảnh. Mỗi phòng, mỗi vị trí đặt camera, mỗi loại thiết bị trong phòng đều có "chữ ký" nhiễu riêng. Ngưỡng và lịch tốt ở phòng này có thể sai ở phòng khác. Vì vậy có một phần cấu hình mình vẫn phải tinh chỉnh bằng tay cho khớp với không gian thật — đây là phần không thể tự động hoá hoàn toàn trong ngày đầu.

Không có bộ lọc nào điều chỉnh được cho mọi hoàn cảnh ngay lập tức. Phần "bối cảnh" (phòng này, gió thế nào, máy lạnh ở đâu) là thứ phải học từ thực tế, không đọc từ hướng dẫn.


Kết luận

Điểm đáng lấy đi từ bài này không phải "máy nghe trông bé thế nào". Mà là: khi một hệ thống dựa trên cảm biến báo sai quá nhiều, đừng vội tăng ngưỡng — hãy thêm tầng phân biệt. Càng tách được "cái mình cần" khỏi "cái mình không cần" ở càng nhiều chiều (tần số, thời gian, tính ổn định, nguồn xác nhận chéo), thì càng ít báo nhầm mà vẫn không lỡ điều quan trọng.

Ba điều mình rút ra:

  • Đừng chỉ nhìn độ lớn — hãy nhìn bản chất của âm. Tần số, độ ổn định, tính lặp lại là chỗ phân biệt được tiếng khóc với tiếng gió.
  • Đòi hỏi âm "bền" và chống báo lặp (debounce + cooldown) giết được nhiễu ngắn hạn và nhiễu lặp mà không cần động đến độ nhạy.
  • Cross-check với một nguồn khác là nâng cấp mạnh nhất. Một chiếc "tai" câm tự nó chỉ phân biệt được trong tín hiệu của nó; kết hợp với thông tin không gian thì nó mới gần với "hiểu" chuyện đang diễn ra.

Bài này cố ý không nêu giá trị ngưỡng, dải tần, thời gian persist/lịch, hay cấu hình vùng chính xác. Mỗi code block là minh hoạ cơ chế, không phải source triển khai. Điều đáng giữ là tư duy phân tầng, không phải đoạn code.

Toàn bộ con số và trải nghiệm mình ghi lại ở zeodavu.com/blog. Mình cũng đang theo dõi mảng tự động hoá nhà ở khu nhà home.


Một phần của chuỗi mổ xẻ kỹ thuật tại zeodavu.com/blog. Nội dung là bài học mở, không phải blueprint để sao chép — thứ đáng giữ là cấu trúc và trải nghiệm, không phải danh sách công cụ.

Nếu thấy hữu ích, theo dõi [zeodavu.com](/blog) — mỗi bài là một thứ mình thật sự dùng, vấp, và sửa lại.