好久没有更新技术文章了,写写最近研究的小玩意。
(下边就是我自己做的指纹膜,实操后是可以绕过一些签到机和手机指纹锁的)
我们的指纹为什么能用于解密手机?因为每个人的指纹都是独一无二、终生不变的,手机通过识别模块收集指纹信息,与之前存储在手机中的指纹信息进行对比,匹配成功即可解锁。
原理很简单,但是实现起来并不轻松。
指纹采集技术获取的指纹图像通常为二维灰度图像,其中脊线是暗的,而谷线是亮的。虽然指纹图像并不是深度图像,但是通过将灰度视为高度,可以将指纹显示为曲面(越黑越高),近似反映了实际手指皮肤上的高低起伏。成人脊线的宽度从0.1毫米到0.3毫米不等,脊线的周期约为0.5毫米。手指的轻微损伤,如表皮烧伤、擦伤或割伤,不会影响真皮层的脊线结构,新长出的皮肤还会恢复为原来的脊线结果,这就是指纹的终生不变性。
指纹增强
但是指纹图像常常会受到噪声、光照变化、模糊等影响,因此我们需要对目标指纹图片进行图片增强
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
处理结果
- 100.png:这个是原图,两个对比率均为100%
- 100_0.png:结果是对的,这两个确实来自同一个人,图片来源:Andrey_Kuzmin/Shutterstock
改进?
改进肯定是有的,例如在局部脊线方向和频率估计上,我们采用二维傅里叶变换检测局部区域的多个候选正弦波,然后利用相邻区域正弦波的连续性来确定正确的脊线方向和频率。
指纹识别中的傅里叶变换_matlab中如何计算指纹图像脊线的方向场与频率场-CSDN博客
还有背景纹理去除、指纹残缺等问题亟需解决,另外如何加入LLM,利用AI识别也是一个新的发展方向......指纹识别的道路也是任重道远,就算是上面写的这个我也是更迭了10个版本,不停地优化信息提取算法以及可视化分析,也只能算是小打小闹,上不得台面,希望大家还是多多批评指正🙏。