一丢丢的 FingerPrint Analysis

好久没有更新技术文章了,写写最近研究的小玩意。

image​​aa0f388fc1dd3ddb01864eb95513d4a

(下边就是我自己做的指纹膜,实操后是可以绕过一些签到机和手机指纹锁的)

我们的指纹为什么能用于解密手机?因为每个人的指纹都是独一无二、终生不变的,手机通过识别模块收集指纹信息,与之前存储在手机中的指纹信息进行对比,匹配成功即可解锁。

原理很简单,但是实现起来并不轻松。

指纹采集技术获取的指纹图像通常为二维灰度图像,其中脊线是暗的,而谷线是亮的。虽然指纹图像并不是深度图像,但是通过将灰度视为高度,可以将指纹显示为曲面(越黑越高),近似反映了实际手指皮肤上的高低起伏。成人脊线的宽度从0.1毫米到0.3毫米不等,脊线的周期约为0.5毫米。手指的轻微损伤,如表皮烧伤、擦伤或割伤,不会影响真皮层的脊线结构,新长出的皮肤还会恢复为原来的脊线结果,这就是指纹的终生不变性。

image

指纹增强

但是指纹图像常常会受到噪声、光照变化、模糊等影响,因此我们需要对目标指纹图片进行图片增强

image

CLAHE (Contrast Limited Adaptive Histogram Equalization), 一种增强图像对比度的方法,特别适用于局部图像区域的对比度调整,基于直方图均衡化,但对比度的增强是自适应的,并通过一个限制因子 (clipLimit) 来避免过度增强噪声。

自适应阈值化, 一种将图像转换为二值图像的方法,通过在每个局部区域内计算阈值来进行分割,而不是使用全局固定阈值,与传统的全局阈值化方法不同,自适应阈值化能够根据局部区域的不同亮度特征自动调整阈值,在图像中亮度不均匀或噪声较多的情况下,能够较好地分割出指纹区域。

def preprocess_fingerprint(image):
    # 检查图像格式并转换为灰度
    if len(image.shape) == 3:
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    else:
        gray = image

    # CLAHE 增强对比度
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
    enhanced = clahe.apply(gray)

    # 自适应阈值化
    binary = cv2.adaptiveThreshold(
        enhanced, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY,
        blockSize=11,
        C=2
    )
    return binary

指纹关键点提取

SIFT(Scale-Invariant Feature Transform,尺度不变特征变换)是一种计算机视觉算法,用于从图像中提取局部特征点,并对它们进行描述和匹配,具体分为五步:

1. 尺度空间构造

为了找到图像的特征点,SIFT 构造了一组尺度空间来检测关键点。

  • 高斯模糊 (Gaussian Blur) :通过逐渐增加高斯核的标准差 $\sigma$,对图像进行多次模糊,得到多尺度图像。
  • 高斯差分 (DoG) :用相邻的模糊图像相减构造差分图像(Difference of Gaussian, DoG),公式为:

    $D(x, y, \sigma) = L(x, y, k\sigma) - L(x, y, \sigma)$

  • 其中 $L(x, y, \sigma)$ 是高斯模糊图像。

2. 关键点检测

DoG 图像中,关键点通过极值检测找到:

  • 每个像素点在其当前尺度的 $3 \times 3$ 邻域,以及上下相邻尺度的 $3 \times 3$ 邻域中,寻找局部极值点。

3. 关键点过滤

为了确保关键点的稳定性和准确性,SIFT 对检测到的关键点进行了进一步优化:

  • 去掉低对比度点:如果关键点的 DoG 值低于某个阈值,丢弃。
  • 去掉边缘响应点:通过计算 Hessian 矩阵,去除对边缘敏感的关键点。

4. 关键点方向分配

为实现旋转不变性,SIFT 为每个关键点分配一个主方向:

  • 以关键点为中心,计算邻域内像素的梯度幅值和方向。
  • 构建方向直方图(36个bin,覆盖 $0^\circ$ 到 $360^\circ$)。
  • 主方向是直方图中幅值最大的方向,必要时添加次方向。

5. 生成特征描述符

根据关键点的尺度和方向,计算关键点周围区域的描述。

但是当我们尝试利用 SIFT 算法进行关键点信息提取识别时,就会发现本算法会对关键点进行全局匹配,这可能导致对一些局部区域的错配,尤其是在旋转、位移或纹理局部损坏的情况下。

因此为减小误差,我们还要限制一下特征匹配的范围,即:

  • 提取指纹图像的核心区域(ROI,Region of Interest),只在ROI内进行匹配。
  • ROI可以通过二值化后提取连通区域,定位指纹的主要部分。

我们还可以添加几何约束,比如:

  • 角度一致性:匹配点之间的相对角度。
  • 距离一致性:匹配点之间的距离。
# === 提取 SIFT 关键点与描述符 ===
def extract_keypoints_sift(image):
    sift = cv2.SIFT_create()
    keypoints, descriptors = sift.detectAndCompute(image, None)
    return keypoints, descriptors

# === 匹配 SIFT 关键点 ===
def match_sift_keypoints(desc1, desc2, kp1, kp2):
    bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=True)
    matches = bf.match(desc1, desc2)

    filtered_matches = []
    for match in matches:
        pt1 = kp1[match.queryIdx].pt
        pt2 = kp2[match.trainIdx].pt
        distance = np.linalg.norm(np.array(pt1) - np.array(pt2))
        if distance < 100:  # 距离阈值
            filtered_matches.append(match)

    return sorted(filtered_matches, key=lambda x: x.distance)
# === ROI 提取函数 ===
def extract_roi(binary_image):
    contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    if contours:
        largest_contour = max(contours, key=cv2.contourArea)
        x, y, w, h = cv2.boundingRect(largest_contour)
        roi = binary_image[y:y + h, x:x + w]
        return roi, (x, y, w, h)
    return binary_image, None

骨架化(脊线提取)

骨架化(脊线提取)是指在指纹图像中提取脊线(即指纹的主要纹理结构)的一种过程,在骨架化中腐蚀操作可以逐步去除图像边缘,膨胀则恢复图像的区域,结合腐蚀和膨胀,逐渐提取出指纹的细节;我们用到了 skimage 库中的 skeletonize​ 函数来进行骨架化。

# === 骨架化(脊线提取) ===
def extract_ridges(image):
    inverted = cv2.bitwise_not(image)
    skeleton = skeletonize(inverted // 255)
    skeleton = (skeleton * 255).astype(np.uint8)
    return skeleton

而在指纹形成脊线的同时,也产生了重要的关键点,也是指纹的独特标识符:

在脊线分裂成两条脊线的地方,我们称为分叉点,即指纹的脊线在某一点发生了分叉,形成两个方向。

当脊线到达某个点后终止的位置,我们称为端点,即脊线的尽头,没有继续延伸下去。

# === 提取分叉点和端点 ===
def extract_minutiae(skeleton):
    minutiae = []
    for y in range(1, skeleton.shape[0] - 1):
        for x in range(1, skeleton.shape[1] - 1):
            if skeleton[y, x] == 255:
                neighbors = skeleton[y - 1:y + 2, x - 1:x + 2].sum() // 255
                if neighbors == 2:  # 端点
                    minutiae.append((x, y, 'ending'))
                elif neighbors > 3:  # 分叉点
                    minutiae.append((x, y, 'bifurcation'))
    return minutiae
# 如果一个脊线像素周围只有一个相邻的脊线像素,那么该像素是一个端点。
# 如果一个脊线像素周围有三个或更多的相邻脊线像素,那么该像素是一个分叉点。

统计与计算

这个地方我们综合分析,两种算法的结果都要尊重,因此最后我们进行加权:

# === 计算匹配率 ===
def calculate_match_ratio(kp1, kp2, matches):
    return len(matches) / max(len(kp1), len(kp2))

# === 综合特征匹配函数 ===
def compare_fingerprints(img1, img2):
    binary1 = preprocess_fingerprint(img1)
    binary2 = preprocess_fingerprint(img2)

    roi1, _ = extract_roi(binary1)
    roi2, _ = extract_roi(binary2)

    # SIFT 匹配
    kp1, desc1 = extract_keypoints_sift(roi1)
    kp2, desc2 = extract_keypoints_sift(roi2)
    matches = match_sift_keypoints(desc1, desc2, kp1, kp2)
    sift_ratio = calculate_match_ratio(kp1, kp2, matches)

    # 脊线特征匹配
    ridge1 = extract_ridges(roi1)
    ridge2 = extract_ridges(roi2)
    minutiae1 = extract_minutiae(ridge1)
    minutiae2 = extract_minutiae(ridge2)
    minutiae_matches = len(set(minutiae1) & set(minutiae2))
    ridge_ratio = minutiae_matches / max(len(minutiae1), len(minutiae2))

    # 加权融合得分
    final_score = 0.7 * sift_ratio + 0.3 * ridge_ratio

    return final_score, matches, sift_ratio, ridge_ratio

处理结果

  1. 100.png:这个是原图,两个对比率均为100%

image

  1. 100_0.png:结果是对的,这两个确实来自同一个人,图片来源:Andrey_Kuzmin/Shutterstock

image

image

改进?

改进肯定是有的,例如在局部脊线方向和频率估计上,我们采用二维傅里叶变换检测局部区域的多个候选正弦波,然后利用相邻区域正弦波的连续性来确定正确的脊线方向和频率。

指纹识别中的傅里叶变换_matlab中如何计算指纹图像脊线的方向场与频率场-CSDN博客

还有背景纹理去除、指纹残缺等问题亟需解决,另外如何加入LLM,利用AI识别也是一个新的发展方向......指纹识别的道路也是任重道远,就算是上面写的这个我也是更迭了10个版本,不停地优化信息提取算法以及可视化分析,也只能算是小打小闹,上不得台面,希望大家还是多多批评指正🙏。

发布者

AndyNoel

一杯未尽,离怀多少。