ラズベリーパイでインターホンの音を検知する
前回の記事で、オートロックマンション不在時荷物受け取りシステム(仮)の材料を揃えた。 westgate-lab.hatenablog.com
ここから実際にソフトウェアを開発していく。
やりたいこと
配達員の来訪を検知するには何はともあれインターホンが鳴ったことを検知する必要がある。原理的にはインターホンを分解してどこかからインターホンの信号を拾ってもいいが、賃貸マンションなのであまり原状復帰が難しくなるようなことはしないこととする。
なので、まずはウェブカメラについたマイクでインターホンの音を拾い、インターホンの音を識別、検知することを目標とする。使うウェブカメラは、logicoolのC270だ。
ロジクール ウェブカメラ C270 ブラック HD 720P ウェブカム ストリーミング 小型 シンプル設計 国内正規品 2年間メーカー保証
- 発売日: 2010/08/20
- メディア: Personal Computers
また、ラズベリーパイはModel 3B+である。言語はPythonで組んでいく。
リアルタイムで音声を取得する
ラズパイ+ウェブカメラ+Pythonで手軽に音声データを取得するにはPyAudioが便利である。 PyAudioの使い方は以下のNoteを参考にしたので、そちらを見てほしい。
リアルタイムで音声波形の取得【PyAudio】|もくいち|note
ここのコードをコピペすればとりあえずリアルタイムに音声の時系列データ(一定個数の配列)が得られるはずである。
如何にしてインターホンの音を識別するか
機械学習など使うほうがイマドキ感があるが、まずは目立つ周波数を抜き出して比較することとする。
家のインターホンの音を周波数解析してみる
家のインターホンの音がどういった周波数で構成されているか、スマホで録音したインターホンの音をAudacityのスペクトル解析機能を使って解析した。
うちのインターホンは鳴ると「ピロピロピロピロ・・・」という感じで、「ピ(↑)ロ(↓)」という高低の音の繰り返しである。
音声波形を表示するとこんな感じ。
最初が大きく、徐々に小さくなり、それが繰り返されることがわかる。
では、次にピ(↑)、すなわち高音部分を抜き出してスペクトル解析する。
いくつも波が立っているが、インターホンの音に対応するピークは基音から886Hz, 1778Hz, 2665Hz, 3550Hz・・・となっており、キレイに倍音が出ていることがわかる。
次にロ(↓)、すなわち低音部分を抜き出してスペクトル解析する。
同様にピークが立っており、基音から727Hz, 1450Hz, 2176Hz, 2905Hz・・・となっている。こちらもきれいな倍音である。うちのインターホンは、低音も高音も3倍音が最も大きい周波数のようである。
高速フーリエ変換(FFT)でインターホンの音を識別する
インターホンの音の素性がわかればあとはラズベリーパイでFFTしてその周波数を検知するのみである。
FFTはnumpyを使った。
手順としては、
- 常にPyAudioで音声を取得
- FFTで最大振幅の周波数を取得
- その周波数が低音の基音、2倍音、3倍音・・・の周辺であればインターホンの低音と識別
- その周波数が高音の基音、2倍音、3倍音・・・の周辺であればインターホンの高音と識別
- 高音・低音を検知したらインターホンが鳴ったものとする。
手順3,4でスペクトル解析で最も大きい音が出ていた3倍音以外も取ることにしたのは、録音環境やウェブカメラのマイクのスペックにより微妙に振幅が変化するためである。
コードはこんな感じ。
import pyaudio import numpy as np import time from scipy.signal import argrelmax # interphone setting CHUNK = 1024 RATE = 8000 # sampling rate dt = 1/RATE freq = np.linspace(0,1.0/dt,CHUNK) fn = 1/dt/2; # nyquist freq FREQ_HIGH_BASE = 886.0 # high tone frequency FREQ_LOW_BASE = 726.6 # low tone frequency FREQ_ERR = 0.02 # allowable freq error #variable detect_high = False detect_low = False # FFTで振幅最大の周波数を取得する関数 def getMaxFreqFFT(sound, chunk, freq): # FFT f = np.fft.fft(sound)/(chunk/2) f_abs = np.abs(f) # ピーク検出 peak_args = argrelmax(f_abs[:(int)(chunk/2)]) f_peak = f_abs[peak_args] f_peak_argsort = f_peak.argsort()[::-1] peak_args_sort = peak_args[0][f_peak_argsort] # 最大ピークをreturn return freq[peak_args_sort[0]] # 検知した周波数がインターホンの音の音か判定する関数 def detectDualToneInOctave(freq_in, freq_high_base, freq_low_base, freq_err): det_h = det_l = False # 検知した周波数が高音・低音のX倍音なのか調べる octave_h = freq_in / freq_high_base octave_l = freq_in / freq_low_base near_oct_h = round(octave_h) near_oct_l = round(octave_l) if near_oct_h == 0 or near_oct_l == 0: return False, False # X倍音のXが整数からどれだけ離れているか err_h = np.abs((octave_h-near_oct_h) / near_oct_h) err_l = np.abs((octave_l-near_oct_l) / near_oct_l) # 基音、2倍音、3倍音の付近であればインターホンの音とする if err_h < freq_err: det_h = True elif err_l < freq_err: det_l = True return det_h, det_l if __name__=='__main__': P = pyaudio.PyAudio() stream = P.open(format=pyaudio.paInt16, channels=1, rate=RATE, frames_per_buffer=CHUNK, input=True, output=False) while stream.is_active(): try: input = stream.read(CHUNK, exception_on_overflow=False) ndarray = np.frombuffer(input, dtype='int16') abs_array = np.abs(ndarray)/32768 if abs_array.max() > 0.5: # FFTで最大振幅の周波数を取得 freq_max = getMaxFreqFFT(ndarray, CHUNK, freq) print("振幅最大の周波数:", freq_max, "Hz") h,l = detectDualToneInOctave(freq_max, FREQ_HIGH_BASE, FREQ_LOW_BASE, FREQ_ERR) if h: detect_high = True print("高音検知!") if l: detect_low = True print("低音検知!") # dual tone detected if detect_high and detect_low: print("インターホンの音を検知!") time.sleep(30) print("フラグリセット") detect_high = detect_low = False except KeyboardInterrupt: break stream.stop_stream() stream.close() P.terminate()
実際にpython3で起動させて、インターホンの音をウェブカメラに聞かせると、
振幅最大の周波数: 2674.4868035190616 Hz 高音検知! 振幅最大の周波数: 2189.6383186705766 Hz 低音検知! インターホンの音を検知!
と出力され、正しくインターホンの音が検知できたことを確認できる。
ただし、最大振幅の周波数を見ているだけなので、日常生活のノイズをたまーにインターホンの音と誤検知する場合がある(週に2,3回)。これは将来的に機械学習で対応したいと思う。