前腕8ch表面筋電位(sEMG)と手話非手指動作(NMM)のリアルタイム信号処理:
双二次IIRノッチフィルタと256点STFT実装
監修:加藤 明生(株式会社オープンナレッジ 創業者・代表取締役CTO / 上級医療情報技師 / 登録情報処理安全確保支援士(第007617号))
この記事の要点と4つの章のご案内
| 章構成 | 主なテーマ | 要点と確認ポイント |
|---|---|---|
| 1. 基本と全体像 | 身近なたとえで見る仕組み:「5本の指を動かす腕の中のピアノ線」と「雑音だけを取り除く落ち着いた聴診器」 | 災害時の避難所や救急医療の現場で、ろう者・難聴者の方がカメラの設置を気にせず手話で対話できるよう、前腕の8チャンネル表面筋電位(sEMG)を用いたウェアラブル支援技術(moshimo)を研究開発しています。 |
| 2. 現場での進め方と対策 | 現場で陥りやすい落とし穴と回避策:生体信号処理と手話情報アクセシビリティの3つの要点 | 皮膚表面で計測される微弱な筋電信号(20〜450Hz)から商用電源ハムノイズ(50Hz/60Hz)をリアルタイム除去するため、双二次(Biquad)直接形II転置IIRノッチフィルタを実装しています。 |
| 3. 関連する制度とルール | なぜ手話の認識に「カメラの映像」だけでなく「腕の筋電センサー(sEMG)」を組み合わせるのですか? | さらにハニング窓を用いた256点短時間フーリエ変換(STFT)と9軸IMU(姿勢角)を統合し、手話文法に不可欠な非手指動作(NMM)と指文字を遅延20ミリ秒以下で特徴量化します。 |
| 4. 詳しい技術仕様としくみ | 手話における「非手指動作(NMM:Non-Manual Markers)」とは何ですか?なぜそんなに大切なのですか? | カメラだけで手話を読み取ろうとすると、「自分の部屋や外出先で常にカメラを向けられ続けるプライバシーの負担」「暗い場所や逆光で見えなくなる問題」「手と手が重なったときに指の形が隠れてしまう問題(オクルージョン)」が起きます。リストバンド型の8チャンネル表面筋電位(sEMG)センサーなら、カメラの死角や照明に関係なく、指を動かす筋肉の微弱な電気信号から直接、繊細な指の動きや力の入れ具合を読み取ることができます。 |
-
01
災害時の避難所や救急医療の現場で、ろう者・難聴者の方がカメラの設置を気にせず手話で対話できるよう、前腕の8チャンネル表面筋電位(sEMG)を用いたウェアラブル支援技術(
moshimo)を研究開発しています。 - 02 皮膚表面で計測される微弱な筋電信号(20〜450Hz)から商用電源ハムノイズ(50Hz/60Hz)をリアルタイム除去するため、双二次(Biquad)直接形II転置IIRノッチフィルタを実装しています。
- 03 さらにハニング窓を用いた256点短時間フーリエ変換(STFT)と9軸IMU(姿勢角)を統合し、手話文法に不可欠な非手指動作(NMM)と指文字を遅延20ミリ秒以下で特徴量化します。
なぜ「単語の置き換え」だけでは手話の本当のニュアンス(疑問・否定・感情を表す非手指動作 NMM)が伝わらないのか、技術がどう寄り添うのかを視覚図解で解説します。
電極の接触インピーダンス変化(汗や乾燥)、関西(60Hz)と関東(50Hz)の電源周波数切替、オフライン環境での動作など、現場実装の工夫を確認できます。
後半の「第3章 Monograph」にて、Z領域伝達関数 H(z)、双一次変換係数導出、Direct Form II Transposed 差分方程式、Rust #![no_std] DSP コードを詳解しています。
身近なたとえで見る仕組み:「5本の指を動かす腕の中のピアノ線」と「雑音だけを取り除く落ち着いた聴診器」
手首から肘にかけての「前腕(ぜんわん)」に軽くもう片方の手を当てて、親指や小指をグッと動かしてみてください。腕の中の筋肉(浅指屈筋や総指伸筋など)が、まるで「5本の指を操るピアノ線」のように一本ずつ別々に動くのがわかります。
人間の脳が「指を動かそう」と命令を出すとき、その筋肉にはごくわずかな電気信号(筋電位:ミリボルト単位)が流れます。この電気信号を皮膚の上から8個のセンサーでぐるりと聞き取るのが「8チャンネル表面筋電位(sEMG)」です。これなら、カメラの前に立たなくても、ポケットの中や暗い夜道でも、どの指にどれくらい力を入れたかが正確にわかります。
ただし、この筋肉の電気信号はとても小さいため、そのまま測ると部屋の蛍光灯やコンセントから出ている電気の波(東日本では1秒間に50回、西日本では60回の「ブーン」というハムノイズ)にかき消されてしまいます。そこで私たちは、「双二次IIRノッチフィルタ」という数学のろ過器を使って50Hz/60Hzの雑音だけをピンポイントで消し去り、さらに「256点STFT(短時間フーリエ変換)」によって1秒間に数十回という速さで筋肉の音色(周波数スペクトル)を読み解く仕組みをRust言語で実装しています。
骨格筋の主要な活動帯域(20Hz〜450Hz)を歪みなく捉えるため、シャノン・ナイキストの標本化定理に基づき毎秒1,000回の高速計測を行います。
Q値(先鋭度)を高めた双二次IIRノッチフィルタにより、隣接する大切な筋電成分を傷つけずに50/60Hzの電源ノイズだけを100分の1以下に減衰させます。
動的メモリ確保を行わないRust固定長リングバッファにより、手話の自然な会話テンポを妨げないリアルタイム応答を実現します。
| 比較項目 | 一般的なカメラ映像のみの手話認識 | 当社研究(moshimo):前腕8ch sEMG + DSPエッジ処理 |
|---|---|---|
| プライバシーと心理的負担 | 常に顔や部屋の中をカメラで撮影し続ける必要がある | 腕のリストバンドのみで動作し、カメラ撮影が不要な場面でも使える |
| 指の重なり(オクルージョン)・暗所 | 両手が重なったり夜間・停電時になると認識精度が急落する | 筋肉の電気信号を直接測るため、照明ゼロや死角でも安定して計測可能 |
| 力の入れ具合(強意・緊張度)の検出 | 見た目の位置が同じだと「軽く握る」と「強く握る」を区別できない | 振幅(RMS)と周波数帯域から「痛みや緊急度の強さ」まで識別できる |
| 通信途絶時(災害避難所)の動作 | 大型クラウドGPUへ動画を送る方式は圏外になると使えない | Rust製の軽量DSPにより、端末内(エッジ)だけで高速・省電力に動作 |
現場で陥りやすい落とし穴と回避策:生体信号処理と手話情報アクセシビリティの3つの要点
回避策:独自の文法体系を持つ独立した言語として敬意を払い、非手指動作(NMM)と空間文法を設計の中心に据える
技術者が手話支援ツールを作る際に最も陥りやすい過ちが、「手の形=1文字・1単語」という単純な辞書変換だけで完結させようとすることです。日本手話では、同じ手の動きであっても、眉を上げるか下げるか、顎を引くか、あるいは動作のスピードと止め(ホールド)の強さによって、「行きますか?(疑問)」「行きません(否定)」「原則として行く!(強い意志)」が区別されます。当事者の方々の声に耳を傾け、この繊細なリズムと強弱(sEMGの振幅包絡と静止保持時間)を損なわない設計が不可欠です。
回避策:20Hzハイパスフィルタによる基線ドリフト除去と、50/60Hzノッチフィルタの直列カスケード構成
手話は腕全体を大きく動かすため、電極と皮膚がわずかにズレることで発生する「低周波の大きなうねり(0〜15Hzのモーションアーチファクト)」が必ず混入します。これを除去しないまま平均や二乗和(RMS)を計算すると、指を動かしていなくても「腕を振っただけ」で大きな信号が出たと誤判定してしまいます。前段で20Hz以下の揺れをカットし、続いて50Hz(東日本)または60Hz(西日本・高槻)の商用電源ノイズを双二次IIRフィルタで除去する2段構えが重要です。
回避策:256サンプル窓の両端を滑らかにゼロへ絞る「ハニング窓(Hanning Window)」と75%オーバーラップの適用
連続している筋肉の電気信号を256ミリ秒ごとにブツッと切り出して周波数解析にかけると、切り口の段差によって本来存在しないはずの高い周波数ノイズ(サイドローブ・スペクトル漏れ)が大量に発生します。これを防ぐため、窓の両端をコサイン曲線で滑らかに減衰させる「ハニング窓」を掛け合わせ、さらに64サンプル(64ミリ秒)ずつ窓をずらしながら重ねて解析(75%オーバーラップ)することで、一瞬の指の動きも取りこぼさずに捉えます。
今日から確認できる!ろう者・難聴者の方に配慮した職場・窓口のアクセシビリティ 3項目チェック
達成度: 0 / 3クリックしてチェックを入れると、職場やWebサイトの情報バリアフリー度に応じたアドバイスが表示されます。
8ch sEMG ノッチフィルタ&STFT リアルタイム処理検証マトリクス
西日本(60Hz:大阪府高槻市等)および東日本(50Hz)の商用電源ハムノイズ抑制と、256点STFTによる特徴量抽出の品質検証基準です。
| 検証項目 | 対象周波数・パラメータ | 減衰量・レイテンシ基準 |
|---|---|---|
| 商用電源ハム除去(西日本/東日本) | f0 = 60.0 Hz / 50.0 Hz (Q = 30.0) | 中心周波数において -40 dB 以上の減衰、隣接運動単位帯域の位相歪み最小化 |
| 基線動揺(モーションアーチファクト)除去 | High-Pass fc = 20.0 Hz (2nd-Order) | 腕の振りやケーブル揺れに起因する 0〜15 Hz 低周波ドリフトの確実に遮断 |
| エッジ推論・特徴量フレーム更新周期 | N = 256 pt Hanning / Hop R = 64 pt | 64 ms 周期(15.625 Hz)で8chスペクトル特徴量(RMS/MDF/MNF)をヒープ割り当てゼロで算出 |
筋肉が疲れてくると、電気信号の「音の高さ(中央周波数 MDF)」がだんだん低くなる?
実は、256点STFT(短時間フーリエ変換)で筋肉の電気信号の周波数を分析すると、「どの指を動かしたか」だけでなく「その人の腕がどれくらい疲れているか(筋疲労度)」までリアルタイムにわかります。筋肉が元気なときは100Hz〜150Hzあたりの高い周波数の信号が活発に出ますが、長時間手話を続けたり重い荷物を持ったりして筋肉に乳酸などの疲労物質が溜まってくると、筋繊維を伝わる電気のスピード(筋線維伝導速度)がゆっくりになり、周波数スペクトルの中心(MDF:Median Frequency)が60Hz〜80Hzの低い方へとスライドしていくのです。この性質を応用すると、「使いすぎによる腱鞘炎や疲労を事前に知らせてくれる配慮した見守り機能」も実現できます。
前腕8ch sEMG 用 双二次IIRノッチフィルタ伝達関数 H(z)・Direct Form II 転置差分方程式と 256点ハニング窓 STFT の Rust 2024 実装
標本化周波数 fs = 1000 Hz において、商用電源周波数 f0 ∈ {50 Hz, 60 Hz} を減衰させる2次IIRノッチフィルタ(双二次フィルタ:Biquad Filter)は、角周波数 ω0 = 2π f0 / fs と品質係数(先鋭度)Q = 30.0、減衰帯域幅パラメータ α = sin(ω0)/(2Q) を用いて、単位円上の零点 z = exp(±jω0) と単位円内側の近傍極を持つ以下の Z 領域伝達関数 H(z) として定式化されます:
単精度浮動小数点(f32)環境での内部状態オーバーフローと量子化雑音を抑えるため、実装には遅延レジスタ2個(w1, w2)のみで動作する直接形II転置構造(Direct Form II Transposed)を採用します。時刻 n における各チャンネル c ∈ {0,…,7} の差分方程式は以下で与えられます:
ここで窓長 N = 256(256 ms、周波数分解能 Δ f = 1000 / 256 = 3.90625 Hz)、ホップサイズ R = 64(64 ms 更新周期)とすることで、手指の素早い遷移相(Transient Phase)と非手指動作(NMM)に伴う等尺性収縮(Isometric Contraction)の双方をリアルタイムに捕捉します。
| Parameter / Stage | Symbol / Formula | Numerical Value (f_s = 1000Hz) | Bio-Signal Role |
|---|---|---|---|
| 60Hz Notch (West Japan) | (b0, b1, b2, a1, a2) at Q=30 | b0=0.9939, b1=-1.8476, a2=0.9879 | Suppresses Kansai 60Hz power-line hum > 40dB |
| 50Hz Notch (East Japan) | (b0, b1, b2, a1, a2) at Q=30 | b0=0.9949, b1=-1.8924, a2=0.9898 | Suppresses Kanto 50Hz power-line hum > 40dB |
| High-Pass Drift Filter | 2nd-Order Butterworth f_c = 20Hz | Attenuates 0..15Hz cable sway | Eliminates arm motion baseline wander |
| STFT Window Size | N = 256, R = 64 (75% overlap) | Delta_f = 3.90625 Hz / bin | Captures motor unit firing spectrum (Bins 5..115) |
| Hanning Sidelobe Roll-off | First sidelobe -31.5 dB | -18 dB/octave decay | Prevents spectral leakage across adjacent bins |
| NMM Tension Index | Co-contraction Ratio (Flexor/Extensor) | 8-Channel RMS + WL Vector | Distinguishes interrogative/emphatic JSL markers |
#![no_std]
#![forbid(unsafe_code)]
pub const SEMG_CHANNELS: usize = 8;
pub const STFT_WINDOW_LEN: usize = 256;
/// Normalized Biquad IIR filter coefficients (a0 normalized to 1.0).
#[derive(Clone, Copy, Debug)]
pub struct BiquadCoeffs {
pub b0: f32,
pub b1: f32,
pub b2: f32,
pub a1: f32,
pub a2: f32,
}
impl BiquadCoeffs {
/// Precomputed 60.0 Hz Notch Filter at f_s = 1000.0 Hz, Q = 30.0 (West Japan / Takatsuki)
pub const NOTCH_60HZ_FS1000_Q30: Self = Self {
b0: 0.993_912_4,
b1: -1.847_590_6,
b2: 0.993_912_4,
a1: -1.847_590_6,
a2: 0.987_824_8,
};
/// Precomputed 50.0 Hz Notch Filter at f_s = 1000.0 Hz, Q = 30.0 (East Japan)
pub const NOTCH_50HZ_FS1000_Q30: Self = Self {
b0: 0.994_885_1,
b1: -1.892_384_5,
b2: 0.994_885_1,
a1: -1.892_384_5,
a2: 0.989_770_2,
};
}
/// Direct Form II Transposed state registers for 8 synchronous sEMG channels.
#[derive(Clone, Copy, Debug)]
pub struct MultiChannelBiquad {
coeffs: BiquadCoeffs,
w1: [f32; SEMG_CHANNELS],
w2: [f32; SEMG_CHANNELS],
}
/// Real-time extracted feature frame for Sign Language Handshape & NMM tension inference.
#[derive(Clone, Copy, Debug)]
pub struct SemgFeatureFrame {
pub mean_abs_value: [f32; SEMG_CHANNELS],
pub waveform_length: [f32; SEMG_CHANNELS],
pub zero_crossings: [u16; SEMG_CHANNELS],
pub isometric_co_contraction: f32,
}
impl MultiChannelBiquad {
pub const fn new(coeffs: BiquadCoeffs) -> Self {
Self {
coeffs,
w1: [0.0; SEMG_CHANNELS],
w2: [0.0; SEMG_CHANNELS],
}
}
/// Processes a single 8-channel sample vector `x[0..8]` in O(1) time using DF-II Transposed.
#[inline]
pub fn process_sample_8ch(&mut self, input: &[f32; SEMG_CHANNELS]) -> [f32; SEMG_CHANNELS] {
let mut out = [0.0f32; SEMG_CHANNELS];
let mut ch = 0;
while ch < SEMG_CHANNELS {
let x = input[ch];
let y = self.coeffs.b0 * x + self.w1[ch];
self.w1[ch] = self.coeffs.b1 * x - self.coeffs.a1 * y + self.w2[ch];
self.w2[ch] = self.coeffs.b2 * x - self.coeffs.a2 * y;
out[ch] = y;
ch += 1;
}
out
}
}
/// Applies a 256-point window and extracts MAV, Waveform Length (WL), Zero Crossings (ZC),
/// and antagonist co-contraction index for NMM grammatical intensity estimation.
pub fn extract_window_features_256(
filtered_window: &[[f32; SEMG_CHANNELS]; STFT_WINDOW_LEN],
hanning_weights: &[f32; STFT_WINDOW_LEN],
zc_deadzone_mv: f32,
) -> SemgFeatureFrame {
let mut mav = [0.0f32; SEMG_CHANNELS];
let mut wl = [0.0f32; SEMG_CHANNELS];
let mut zc = [0u16; SEMG_CHANNELS];
for n in 0..STFT_WINDOW_LEN {
let w = hanning_weights[n];
for ch in 0..SEMG_CHANNELS {
let s_curr = filtered_window[n][ch] * w;
let abs_s = if s_curr < 0.0 { -s_curr } else { s_curr };
mav[ch] += abs_s;
if n > 0 {
let s_prev = filtered_window[n - 1][ch] * hanning_weights[n - 1];
let diff = s_curr - s_prev;
wl[ch] += if diff < 0.0 { -diff } else { diff };
let sign_change = (s_curr > 0.0 && s_prev < 0.0) || (s_curr < 0.0 && s_prev > 0.0);
let amp_diff = if diff < 0.0 { -diff } else { diff };
if sign_change && amp_diff >= zc_deadzone_mv {
zc[ch] += 1;
}
}
}
}
let inv_n = 1.0 / (STFT_WINDOW_LEN as f32);
let mut flexor_sum = 0.0f32;
let mut extensor_sum = 0.0f32;
for ch in 0..SEMG_CHANNELS {
mav[ch] *= inv_n;
if ch < 4 {
flexor_sum += mav[ch];
} else {
extensor_sum += mav[ch];
}
}
let denom = flexor_sum + extensor_sum + 1e-6;
let min_group = if flexor_sum < extensor_sum { flexor_sum } else { extensor_sum };
let isometric_co_contraction = (2.0 * min_group) / denom;
SemgFeatureFrame {
mean_abs_value: mav,
waveform_length: wl,
zero_crossings: zc,
isometric_co_contraction,
}
}