ASUS ROG Zephyrus G15(GA503QR)には指紋センサーが搭載されている。

自分の環境ではUSBデバイスとしては認識されていた。

$ lsusb
Bus ... Device ...: ID 27c6:521d Shenzhen Goodix Technology Co.,Ltd. FingerPrint

Goodix製の27c6:521dである。

ところがUbuntuでは、USBデバイスとして見えていることと、指紋センサーとして利用できることはまったく別だった。

標準のlibfprintではこのデバイスを利用できず、fprintdから見ると指紋センサーとして正常に扱えない。

そこで、Goodix 52XD系をサポートするコミュニティ版libfprintをベースに、自分のZephyrus G15で動作するところまで修正してみることにした。

この記事は、その過程の記録である。

ソースコードの書き換えを行ったのは、現状、 driverディレクトリのgoodix.cのみである。

https://github.com/barsikus007/libfprint/blob/9fc5207500752d12c063e1dc69089ced4a25b140/libfprint/drivers/goodixtls/goodix.c

環境

今回使用した環境は以下。

PC:
ASUS ROG Zephyrus G15 GA503QR

OS:
Ubuntu 24.04.4 LTS

Fingerprint sensor:
Goodix 27c6:521d

fprintd:
1.94.3

Ubuntu libfprint:
1.94.7+tod1

検証したlibfprint:
barsikus007/libfprint
branch: merge/upstream-1.94.9

センサー自体は、

27c6:521d

としてUSB上に存在している。

しかしUbuntu標準のlibfprintから利用すると、サポート対象デバイスとして扱われなかった。

コミュニティ版libfprintを試す

Goodix 52XD向けドライバを含むlibfprintのforkを見つけたので、まずこれをビルドした。

git clone https://github.com/barsikus007/libfprint.git
cd libfprint
git checkout merge/upstream-1.94.9

Ninjaでビルドし、Ubuntu標準のlibfprintを直接置き換えず、検証用ディレクトリへ配置した。

/usr/local/lib/libfprint-goodix-test/

fprintd.service には LD_LIBRARY_PATH を設定し、検証版をロードさせた。

これでGoodix 521d自体は認識されるようになった。

しかし、次の問題が出た。

Invalid device PSK

ドライバがセンサーを認識したものの、初期化途中で、

Invalid device PSK

となった。

ここで初めて、単にUSB経由で画像を読み出せばよいデバイスではないことが分かった。

Goodix 521dはホストとセンサーの間で暗号化された通信を行っており、その通信にPSK(Pre-Shared Key)が使われるらしい。

センサー内部のPSKと、ドライバ側が期待するPSKが一致しないと、その先へ進めない。

調査にはGoodix 521d向けの解析ツールを利用した。

goodix-521d-explanation
goodix-fp-dump

最初の状態は、

Firmware: GFUSB_GM168SEC_APP_10019
Valid PSK: False

だった。

そこで一度IAP用ファームウェアへ移行してPSKを書き込み、通常ファームウェアへ戻した。

概略は、

通常Firmware
IAP Firmware
PSK書き込み
通常Firmware復元

という流れになる。

処理後は、

Firmware: GFUSB_GM168SEC_APP_10019
Valid PSK: True

となった。

ようやくセンサーと正常に通信できるようになった。

「通信できた。これで終わりだろう」

と思ったが、実際にはここからが長かった。

指紋を読み取ったはずなのに画像が真っ白

通信が成立すると、今度はセンサーから画像を取得できるようになった。

しかしデバッグ用に画像を書き出してみると、スキャン直後の画像が真っ白だった。

※pgmファイルを取得し、pngファイルに変換し、webp形式にしています。

実際のログでは、画像を正規化した後の白画素率を計算すると、

指なし:
white_ratio ≈ 0.93

指紋が正常に写った画像:
white_ratio ≈ 0.000〜0.002

という極端な差があった。

そこで暫定的に、

return white_ratio < 0.10;

として、白画像を指紋画像として採用しないようにした。

後の検証では、

white_ratio = 0.095

程度の中途半端な画像まで通ってしまうケースも見つかったため、現在は、

return white_ratio < 0.05;

程度まで厳しくしてもよさそうだと考えている。

これはGoodix公式の判定値ではなく、あくまで自分の521dで観測した値を使った暫定的な判定である。

センサーレジスタへの書き込みがおかしかった

画像取得処理を追っている途中で、センサーレジスタへの書き込みにも問題を見つけた。

元コードには、

guint16 payload = {0x05, 0x03};

という記述があった。

当然ながら guint16 はスカラーなので、この初期化は意図した2バイト配列にはならない。

コンパイラも、

excess elements in scalar initializer

相当の警告を出していた。

521dではワイヤ上で、

05 03

を送る必要があったため、

goodix_send_write_sensor_register(
    dev,
    0x022c,
    GUINT16_TO_LE(0x0305),
    write_sensor_complete,
    ssm
);

と修正した。

x86のlittle endian環境では、

0x0305
→ wire: 05 03

となる。

この修正後、画像取得の挙動が大きく変わった。

最初の数枚は白画像になるものの、その後は実際の指紋画像が得られるようになった。

「10フレームをつなぐ」という前提がおかしい

元のGoodix 52XDドライバは、1回のスキャンで複数フレームを取得し、それらを移動推定してつなぎ合わせる設計になっていた。

例えば、

#define GOODIX52XD_CAP_FRAMES 10

として10枚取得し、

fpi_do_movement_estimation(...)
fpi_assemble_frames(...)

で画像を構成している。

最初は自分も、このセンサーが小型のスワイプ式センサーなのだと思っていた。

しかし取得した各フレームを比較すると、ほぼ同じ画像だった。

さらにlibfprint側のmovement estimationが、

delta_x = -8
delta_y = -79

や、

delta_x = +8
delta_y = +79

のような明らかに異常な値を返し、最終画像が、

192x791

のようなサイズになることもあった。

独自にNumPyで画像間位置合わせを試すと、

dx = 0
dy = 0

で、そもそも取得画像がほぼ同一であることが分かった。

Goodix解析用Pythonコードも確認したところ、521dの取得方法は基本的に、

指を検出
1画像取得

だった。

libfprint側でもこのデバイスは、

FP_SCAN_TYPE_PRESS

として定義されている。

つまりスワイプ式ではなくpress sensorである。

そこで、

#define GOODIX52XD_CAP_FRAMES 1

へ変更し、

1回指を押すごとに1画像

として扱うよう変更した。

これでかなり構造が単純になった。

64x80では小さすぎる

521dから得られる画像は、

64px x 80px

しかない。

わずか、 5120px である。

この画像をそのままlibfprintへ渡すと、

Failed to detect minutiae: No minutiae found

となることが多かった。

そこで、

fpi_image_resize(img, 2, 2);

を使い、

64x80
128x160

へ2倍拡大してから指紋特徴点抽出へ渡すことにした。

もちろん、拡大したから新しい指紋情報が増えるわけではない。

ただしNBIS側の特徴点抽出処理に対しては、画像解像度をある程度確保することで処理しやすくなるらしく、No minutiae found は大幅に減った。

これで、

$ fprintd-enroll -f right-middle-finger "$USER"

Enroll result: enroll-stage-passed
...
Enroll result: enroll-completed

まで到達した。

ついに指紋登録できた。

しかし、また問題があった。

指を一度置いただけで登録が全部終わる

最初の実装では、一度指を置くだけで、

enroll-stage-passed
enroll-stage-passed
enroll-stage-passed
...
enroll-completed

と一気に登録が完了してしまった。

明らかにおかしい。

原因を追うと、画像取得直後に、

fpi_image_device_report_finger_status(img_dev, FALSE);

としていた。

つまり、

画像を取得した
実際にはまだ指が載っている
ドライバ「指が離れました」

と報告していた。

その結果、libfprint側は、

指を置いた
画像取得
指を離した
次の登録ステージへ

が完了したと誤認し、同じ指を載せたまま次々に登録していた。

この時点で、以前取得した画像同士の相関が異常に高かった理由も説明できた。

同じ指紋イメージを何度も登録していた可能性が高い。

libfprintのfinger state machineを読む

ここでlibfprint側の画像デバイス状態管理を詳しく読むことにした。

大まかな状態遷移は、

AWAIT_FINGER_ON
   finger ON
    CAPTURE
 image_captured()
AWAIT_FINGER_OFF
   finger OFF
      IDLE

となっている。

さらに登録時には、

minutiae抽出完了
かつ
finger OFF

の両方が成立して初めて次の AWAIT_FINGER_ONへ進む。

つまりドライバ側がやるべきなのは、

指を検出した
→ report TRUE

画像を取得
→ image_captured()

実際に指が離れた
→ report FALSE

である。

image_captured() の直後に勝手に FALSE を送ってはいけない。

本当に指を離すまで待つ

そこでfinger-off検出を追加した。

今回のセンサーでは、

指あり:
white_ratio ≈ 0.000〜0.002

指なし:
white_ratio ≈ 0.93

とかなり明確に分離できている。

そのため画像取得後も同じscan state machineを継続し、100msごとに画像を取得して、

white_ratio < threshold
→ まだ指がある

white_ratio >= threshold
→ 指が離れた

と判定するようにした。

実際のログでは、

frame white ratio: 0.001
finger-off check: finger still present

frame white ratio: 0.001
finger-off check: finger still present

frame white ratio: 0.924
finger-off check: finger removed

となった。

その後、

reporting real finger-off
Image device reported finger status: off

とlibfprintへ通知する。

これでようやく、

1. 指を置く
2. 画像取得
3. 指を離す
4. 次の登録ステージ

という、本来の指紋登録らしい動作になった。

finger-offを直したらfprintdが終了しなくなった

これで解決したと思ったら、今度は連続verify後に、

GDBus.Error:net.reactivated.Fprint.Error.AlreadyInUse:
Device was already claimed

が出るようになった。

さらに、

$ systemctl status fprintd.service

を見ると、

Active: deactivating (stop-sigterm)

のまま止まっていた。

systemctl restart fprintd すら完了しない。

原因は、finger-off監視のためにscan state machineを継続するようにした一方で、libfprintからdeactivate要求が来るとGoodix側のTLS shutdownを即座に開始していたことだった。

つまり、

scan SSM:
USB通信中

同時に

deactivate:
TLS shutdown

という競合が起きる可能性があった。

そこで現在動いているscan SSMをデバイス構造体で追跡するようにした。

FpiSsm *scan_ssm;
gboolean deactivating;

deactivate要求が来た場合は、

deactivating = TRUE

scan SSMが残っている
→ TLS shutdownを延期

scan SSM完了
→ goodix_shutdown_tls()
→ deactivate_complete()

という順序へ変更した。

また、元コードにはエラー時に、

if (error) {
    fp_err(...);
    return;
}

としている箇所があった。

これだとSSMが完了も失敗もしないまま取り残される可能性がある。

そこで、

if (error) {
    fp_err(...);
    fpi_ssm_mark_failed(user_data, error);
    return;
}

へ変更した。

修正後のログは、

finger-off check: finger removed
finished scan
reporting real finger-off

AWAIT_FINGER_OFF → IDLE
IDLE → DEACTIVATING

finishing deferred deactivation

DEACTIVATING → INACTIVE
released device 0

となった。

その後、連続して fprintd-verifyを実行しても AlreadyInUseは発生しなくなった。

ここまでで、

Goodix通信            OK
PSK                   OK
画像取得              OK
1押下1画像            OK
finger-on             OK
finger-off            OK
連続verify            OK
deactivate/release    OK

まで来た。

残る問題は認証精度

動作自体はかなり正常になったが、認証精度はまだ低い。

右中指を登録し、右中指で10回verifyしたところ、

verify-no-match
verify-match
verify-no-match
verify-no-match
verify-no-match
verify-no-match
verify-no-match
verify-match
verify-no-match
verify-match

となった。

10回中3回成功。

成功率は30%しかない。

ログを見ると、libfprint内部ではBozorth3のscoreが出ていた。

例えば、

score 28/24
→ verify-match

や、

score 21/24
→ verify-no-match

となる。

現在のドライバでは、

img_dev_class->bz3_threshold = 24;

である。

つまりBozorth3のscoreが24以上なら一致と判定する。

NBISとBozorth3

ここで初めて、生体認証の内部についてちゃんと調べることになった。

libfprintの指紋照合には、NISTが公開しているNBIS(NIST Biometric Image Software)の技術が使われている。

その中のBozorth3は、画像の画素を直接比較するアルゴリズムではない。

大まかには、

指紋画像
minutiae(特徴点)抽出
特徴点の位置・方向
Bozorth3
特徴点同士の幾何学的関係を比較
score

という流れになる。

minutiaeとは、例えば指紋の隆線が途切れる端点や、分岐する点などである。

指をセンサーに置く位置は毎回少しずれるので、単純な座標一致ではなく、特徴点同士の距離や方向といった幾何学的関係を比較している。

ログに出ている、

score 29/24

は、

Bozorth3 score = 29
threshold = 24
29 >= 24
→ match

という意味になる。

29%一致という意味ではない。

生体認証は今まであまり勉強したことがなかったので、NISTがこういう特徴点抽出や指紋照合アルゴリズムまで実装として公開していることを知って少し感動した。

さすがNIST。

しきい値を下げればいいわけではない

本人でのverifyでは、

0
28
16
17
21
19
20
26
16
29

程度の最大scoreが出ていた。

それなら、

threshold 24
threshold 20

などに下げれば認証率が上がるのではないか、と考えたくなる。

しかし別の指で試す必要がある。

登録は右中指のまま、右人差し指で10回verifyした。

各verifyの最大scoreは、

21
7
13
13
13
14
11
7
16
8

だった。

最大21。

したがってthresholdを21まで下げると、今回の実測だけでも別の指がmatchする可能性が出る。

本人と別指のscore分布はかなり重なっている。

正しい指:
0 ── 16 17 19 20 21 ───── 26 28 29

別指:
7 8 11 13 14 16 ─── 21

つまり、単純にBozorth3のthresholdを下げるだけでは解決できない。

問題はもっと前段にありそうだ。

64x80という小ささ

今回のGoodix 521dの生画像は、

64x80

である。

非常に小さい。

2倍resizeして、

128x160

にしてはいるものの、元の情報量は増えない。

指を1〜2mmずらすだけでも、登録時とverify時に写る領域がかなり変わる可能性がある。

広い指紋画像なら多数のminutiaeを使えるが、64x80程度の小さな画像では特徴点数が少なくなりやすい。

その結果、

同じ指なのにscore 16

別の指なのにscore 21

のようなことが起きている可能性がある。

また、現在は画像ごとに、

squash_frame_linear()

でmin/maxを0〜255へ正規化している。

この処理によってノイズや固定パターンまで強調され、偽のminutiaeが生成されている可能性も考えられる。

次はNCCで画像そのものを比較する

そこで次に試そうとしているのがNCC(Normalized Cross-Correlation)である。

Bozorth3が、

特徴点の幾何学的配置が似ているか

を見るのに対し、NCCでは、

画像パターンそのものが似ているか

を見る。

概念的には、

Bozorth3:
画像
→ minutiae
→ 特徴点比較

NCC:
画像
→ 位置を少しずらしながら相関計算
→ 画像そのものを比較

となる。

以前、一度NCCを試したときには同じ指で、

0.9382
0.9531
0.3035
0.9945
0.9998

とかなり高い値が出た。

ただし、このときはfinger-off処理にバグがあり、同じラッチ画像を繰り返し比較していた可能性がある。

そのため、この値は信用できない。

finger on/offが正常になった現在の状態で、

右中指 vs 右中指

右中指 vs 右人差し指

の画像を独立して大量に取得し、NCC分布を比較する予定である。

もし、

same finger:
0.7〜0.95

different finger:
0.1〜0.4

のように綺麗に分離できれば、521dのような小型press sensorではBozorth3より画像ベース照合の方が適している可能性がある。

逆にNCCでも本人と別指が大きく重なるなら、matcher以前に、

背景補正
固定パターン除去
画像正規化

を改善する必要がある。

現在の状態

最初は、

lsusbでは見える
でも指紋センサーとして使えない

というところから始まった。

そこから、

USBデバイス認識
PSK不整合修復
Goodixとの暗号化通信確立
画像取得
白画像除外
センサーレジスタ書き込み修正
10フレーム方式を廃止
1押下1画像化
64x80 → 128x160
minutiae抽出成功
finger-on/off状態管理修正
deactivateハング修正
連続verify可能

まで進んだ。

最初は「Linuxで指紋センサーを使えるようにしたい」程度の話だったが、気付けば、

USBプロトコル
TLS/PSK
センサー制御
画像処理
状態機械
NBIS
minutiae
Bozorth3

まで追うことになった。

こういう、ハードウェアは認識しているのにソフトウェアスタックのどこか一段が欠けていて動かないものを追いかけるのは面白い。

まだ認証精度は実用レベルではないので、次は画像処理とmatcher側を調べていく予定である。