830 단어
4 분
Fingerprint Payment ep.3 - binarization isn't simple? There's no way I can do this! No way!
0 0

The board was rescued in the previous post, so now it’s firmware time.

The datasheet has no explanations#

I opened the HF302GD datasheet and the register table has only five-letter abbreviations register names, with no descriptions. What each register does, what its lifecycle is — none of that is there.

At first I wrote code inferring from the names alone, and naturally it didn’t go well. Seriously, please, just make it understandable, please

So I designed the firmware by inferring and reconstructing the protocol from the source for the manufacturer-provided test board carrying an AS608 chip. It was a matter of reading someone else’s reference implementation to reverse-reconstruct the spec, and since it was my first time doing that, it took quite a while.

Communication interface#

On the BLE side I layered a small custom binary protocol on top of NUS. It looks roughly like this.

     Host (iPad)                      Sensor (nRF54L15)
         │                                   │
         │◀──────── status / event ─────────│   finger detected, etc.
         │                                   │
         │──────── capture request ────────▶│
         │                                   │
         │◀──────── image header ───────────│   dimensions / bpp
         │◀──────── image data ─────────────│   (chunked)
         │◀──────── end flag ───────────────│
         │                                   │
         │◀──────── error ──────────────────│   (on failure)

A few things I made sure to handle while designing it:

First, any capture request arriving without an established secure connection is simply rejected.

I also split errors into several kinds: another request arriving while a capture is already in progress, an undefined command, the host aborting the capture, a corrupted image due to an unstable data line, and so on. Since the device sits out in the store, it had to be possible to narrow down causes on site. These error codes are also shown on the OLED.

For enrollment I added a separate capture mode. It reads three frames back to back from the sensor and returns an image averaged pixel-wise. This sensor is capacitive-discharge based and therefore noise-sensitive, but simply averaging attenuated the noise quite effectively.

From 8bpp to 1bpp#

The sensor gives an 8bpp image. But what actually goes out over BLE is 260×320 at 1bpp.

There’s one reason: transfer speed. Sending 8bpp as-is means 8× the data, and at BLE bandwidth that puts us right back in V1 territory.

The catch is that going from 8bpp to 1bpp is binarization, and fingerprint binarization was far trickier than expected. This pipeline didn’t come out in one shot — it’s the result of three rounds of refinement.

Gen 1 - fixed threshold#

The simplest thing possible: darker than some value → 1, brighter → 0.

It failed. Fingerprint ridges look different from person to person. For people with shallow, flat ridges, the ridges vanished entirely during packing. Nothing was left in the image.

Gen 2 - global dynamic threshold#

So why not set the threshold differently per person? I applied an algorithm that adjusts the threshold dynamically so ridges occupy 40% of the whole image.

Whether the print is overall dark or light, the ridge ratio in the output comes out similar. Much better than gen 1.

But it got stuck again. Some users don’t press their finger fully, so the print doesn’t fill the image frame; and fundamentally, when you press a finger down, the center contacts strongly while the edges contact weakly.

In other words, the appropriate threshold differs by region within a single image. No single global threshold, however well chosen, can solve this. Tune for the center and the edges vanish; tune for the edges and the center smears.

Gen 3 - local window standard deviation#

So I abandoned the global criterion.

I wrote an algorithm that builds a local window around each pixel, computes the standard deviation within that window, and classifies pixels deviating by more than 3× the standard deviation as ridges.

Now the question isn’t “is it absolutely dark?” but “does it stand out relative to its surroundings?” Even in edge regions that are faint overall due to weak contact, ridges are detected as long as relative contrast survives locally. Variation in contact pressure is absorbed automatically.

But this had a side effect too. In white regions the finger never touched there’s no signal, so the standard deviation itself is tiny — and then even minuscule noise sails past 3× that deviation. The white regions were plastered with noise.

The fix was simple. Any pixel brighter than the global mean plus a bias was just killed. Find ridges by a relative criterion, but cut out pixels that are absolutely too bright from the candidate set in the first place — a noise cut.

Final pipeline#

Sensor 8bpp capture (256×320)
        │
        ▼
Per-pixel local window std. deviation
        │
        ▼
Pixels beyond 3× std. deviation → ridge
        │
        ▼
Drop pixels brighter than global mean + bias
        │
        ▼
Pack to 1bpp, send over BLE

Find ridges by a relative criterion, filter noise by an absolute one. In the end both were needed together.

For reference, this image processing eats 630ms in the final performance figures. It’s the largest single slice of the 1.38 seconds total. Still, considering BLE transfer dropped to 150ms, it’s a net win. V1 spent a full second on BLE alone.

The next post covers infrastructure, DFU, and the results.

Did you find this post helpful?
Fingerprint Payment ep.3 - binarization isn't simple? There's no way I can do this! No way!
https://yeonfish.dev/en/posts/fingerprint_payment/3_firmware/
저자
yeonfish6040
게시일
2026-09-02
라이선스
CC BY-NC-SA 4.0