Skip to content
Snippets Groups Projects
Commit 94c91f2b authored by iliya.saroukha's avatar iliya.saroukha :first_quarter_moon:
Browse files

rendu

parent 2da595cd
No related branches found
No related tags found
No related merge requests found
%% Cell type:markdown id: tags:
# Installation
- Install conda or mini-conda: https://docs.conda.io/en/latest/miniconda.html
- Install dependencies: conda install -c conda-forge -y opencv notebook ipywidgets matplotlib
- Place the file "utils.py" in the same location or add it to PYTHONPATH
%% Cell type:code id: tags:
``` python
from google.colab import drive
drive.mount('/content/drive/')
# Adapter ce chemin à votre hiérarchie
%cd /content/drive/MyDrive/uni-ete
%ls
```
%% Cell type:code id: tags:
``` python
import math
import numpy as np
import cv2 as cv
import matplotlib.pyplot as plt
from utils import *
from ipywidgets import interact
```
%% Cell type:markdown id: tags:
# Step 1: Fingerprint segmentation
%% Cell type:code id: tags:
``` python
fingerprint = cv.imread('samples/sample_1_1.png', cv.IMREAD_GRAYSCALE)
show(fingerprint, f'Fingerprint with size (w,h): {fingerprint.shape[::-1]}')
```
%% Cell type:code id: tags:
``` python
# Calculate the local gradient (using Sobel filters)
"""
On applique le filtre de sobel pour mettre en évidence les contours de l'empreinte digitale.
"""
gx, gy = cv.Sobel(fingerprint, cv.CV_64F, 1, 0), cv.Sobel(fingerprint, cv.CV_64F, 0, 1)
show((gx, 'Gx'), (gy, 'Gy'))
```
%% Cell type:code id: tags:
``` python
# Calculate the magnitude of the gradient for each pixel
gx2, gy2 = gx**2, gy**2
gm = np.sqrt(gx2 + gy2)
show((gx2, 'Gx**2'), (gy2, 'Gy**2'), (gm, 'Gradient magnitude'))
```
%% Cell type:code id: tags:
``` python
# Integral over a square window
window = (25,25)
sum_gm = cv.boxFilter(gm, -1, window)
show(sum_gm, 'Sum of the gradient magnitude')
```
%% Cell type:code id: tags:
``` python
# Use a simple threshold for segmenting the fingerprint pattern
"""
On calcule un masque qui permettra de déterminer les contours extérieurs de l'empreinte
"""
thr = np.max(sum_gm) * 0.2
mask = cv.threshold(sum_gm, thr, 255, cv.THRESH_BINARY)[1].astype(np.uint8)
show(fingerprint, mask, cv.merge((mask, fingerprint, fingerprint)))
```
%% Cell type:markdown id: tags:
# Step 2: Estimation of local ridge orientation
%% Cell type:markdown id: tags:
The ridge orientation is estimated as ortoghonal to the gradient orientation, averaged over a window $W$.
$G_{xx}=\sum_W{G_x^2}$
$G_{yy}=\sum_W{G_y^2}$
$G_{xy}=\sum_W{G_xG_y}$
$\theta=\frac{\pi}{2} + \frac{phase(G_{xx}-G_{yy}, 2G_{xy})}{2}$
For each orientation, we will also calculate a confidence value (strength), which measures how much all gradients in $W$ share the same orientation.
$strength = \frac{\sqrt{(G_{xx}-G_{yy})^2+(2G_{xy})^2}}{G_{xx}+G_{yy}}$
%% Cell type:code id: tags:
``` python
W = (23, 23)
# gxx = cv.GaussianBlur(gx2, W, 0)
# gyy = cv.GaussianBlur(gy2, W, 0)
# gxy = cv.GaussianBlur(gx * gy, W, 0)
gxx = cv.blur(gx2, W)
gyy = cv.blur(gy2, W)
gxy = cv.blur(gx * gy, W)
orientations = (-1) * 0.5 * np.arctan2(2 * gxy, gxx - gyy) + np.pi / 2
strengths = np.sqrt((gxx - gyy) ** 2 + (2 * gxy) ** 2) / (gxx + gyy + 1e-5)
show(draw_orientations(fingerprint, orientations, strengths, mask, 1, 16), 'Orientation image')
```
%% Cell type:markdown id: tags:
# Step 3: Estimation of local ridge frequency
%% Cell type:code id: tags:
``` python
# region = cv.bitwise_and(fingerprint, fingerprint, mask=mask
region = fingerprint[80:200, 100:220]
show(region)
```
%% Cell type:code id: tags:
``` python
# smoothed = cv.medianBlur(region, 5)
smoothed = cv.GaussianBlur(region, (5, 5), 0)
xs = np.sum(smoothed, axis=1)
print(xs)
```
%% Cell type:code id: tags:
``` python
x = np.arange(region.shape[0])
f, axarr = plt.subplots(1,2, sharey = True)
axarr[0].imshow(region,cmap='gray')
axarr[1].plot(xs, x)
# axarr[1].plot(x, xs)
axarr[1].set_ylim(region.shape[0]-1,0)
plt.show()
```
%% Cell type:code id: tags:
``` python
# local_maxima = (np.diff(np.sign(np.diff(xs))) < 0).nonzero()[0] + 1
local_maxima = np.where((xs[:-2] < xs[1:-1]) & (xs[1:-1] > xs[2:]))[0] + 1
print(local_maxima)
```
%% Cell type:code id: tags:
``` python
x = np.arange(region.shape[0])
plt.plot(x, xs)
plt.xticks(local_maxima)
plt.grid(True, axis='x')
plt.show()
```
%% Cell type:code id: tags:
``` python
# sum = 0
# deltas = []
# for i in range(len(local_maxima) - 1):
# deltas.append(local_maxima[i + 1] - local_maxima[i])
# sum += local_maxima[i + 1] - local_maxima[i]
# distances = sum / len(deltas)
# print(distances)
distances = local_maxima[1:] - local_maxima[:-1]
print(distances)
```
%% Cell type:markdown id: tags:
<!-- -->
%% Cell type:code id: tags:
``` python
ridge_period = distances.mean()
print(ridge_period)
```
%% Cell type:markdown id: tags:
# Step 4: Fingerprint enhancement
%% Cell type:code id: tags:
``` python
# Create the filter bank
or_count = 8
gabor_bank = [gabor_kernel(ridge_period, o) for o in np.arange(0, np.pi, np.pi/or_count)]
```
%% Cell type:code id: tags:
``` python
show(*gabor_bank)
```
%% Cell type:code id: tags:
``` python
nf = 255-fingerprint
all_filtered = np.array([cv.filter2D(nf, cv.CV_32F, f) for f in gabor_bank])
show(nf, *all_filtered)
```
%% Cell type:code id: tags:
``` python
y_coords, x_coords = np.indices(fingerprint.shape)
orientation_idx = np.round(((orientations % np.pi) / np.pi) * or_count).astype(np.int32) % or_count
filtered = all_filtered[orientation_idx, y_coords, x_coords]
enhanced = mask & np.clip(filtered, 0, 255).astype(np.uint8)
show(fingerprint, filtered, enhanced)
```
%% Cell type:markdown id: tags:
# Step 5: Detection of minutiae positions
%% Cell type:code id: tags:
``` python
# Binarization
_, ridge_lines = cv.threshold(enhanced, 32, 255, cv.THRESH_BINARY)
show(enhanced, ridge_lines)
```
%% Cell type:code id: tags:
``` python
# Thinning
skeleton = cv.ximgproc.thinning(ridge_lines, thinningType = cv.ximgproc.THINNING_GUOHALL)
show(ridge_lines, skeleton)
```
%% Cell type:code id: tags:
``` python
def compute_crossing_number(values):
return np.count_nonzero(values < np.roll(values, -1))
```
%% Cell type:code id: tags:
``` python
cn_filter = np.array([[ 1, 2, 4],
[128, 0, 8],
[ 64, 32, 16]
])
```
%% Cell type:code id: tags:
``` python
all_8_neighborhoods = [np.array([int(d) for d in f'{x:08b}'])[::-1] for x in range(256)]
cn_lut = np.array([compute_crossing_number(x) for x in all_8_neighborhoods]).astype(np.uint8)
```
%% Cell type:code id: tags:
``` python
# Skeleton: from 0/255 to 0/1 values
skeleton01 = np.where(skeleton!=0, 1, 0).astype(np.uint8)
# Apply the filter to encode the 8-neighborhood of each pixel into a byte [0,255]
neighborhood_values = cv.filter2D(skeleton01, -1, cn_filter, borderType = cv.BORDER_CONSTANT)
# Apply the lookup table to obtain the crossing number of each pixel from the byte value of its neighborhood
cn = cv.LUT(neighborhood_values, cn_lut)
# Keep only crossing numbers on the skeleton
cn[skeleton==0] = 0
```
%% Cell type:code id: tags:
``` python
# Find minutiae.
# List format: [(x: int, y: int, type_terminaison: bool), ...]
minutiae = [(x,y,cn[y,x]==1) for y, x in zip(*np.where(np.isin(cn, [1,3])))]
```
%% Cell type:code id: tags:
``` python
show(draw_minutiae(fingerprint, minutiae), skeleton, draw_minutiae(skeleton, minutiae))
```
%% Cell type:code id: tags:
``` python
# Create mask.
mask_edges = cv.distanceTransform(cv.copyMakeBorder(mask, 1, 1, 1, 1, cv.BORDER_CONSTANT), cv.DIST_C, 3)[1:-1,1:-1]
show(mask, mask_edges)
```
%% Cell type:code id: tags:
``` python
# Filter minutiae.
filtered_minutiae = list(filter(lambda m: mask_edges[m[1], m[0]]>10, minutiae))
```
%% Cell type:code id: tags:
``` python
show(draw_minutiae(fingerprint, filtered_minutiae), skeleton, draw_minutiae(skeleton, filtered_minutiae))
```
%% Cell type:markdown id: tags:
# Step 6: Estimation of minutiae directions
%% Cell type:code id: tags:
``` python
def compute_next_ridge_following_directions(previous_direction, values):
next_positions = np.argwhere(values!=0).ravel().tolist()
if len(next_positions) > 0 and previous_direction != 8:
# There is a previous direction: return all the next directions, sorted according to the distance from it,
# except the direction, if any, that corresponds to the previous position
next_positions.sort(key = lambda d: 4 - abs(abs(d - previous_direction) - 4))
if next_positions[-1] == (previous_direction + 4) % 8: # the direction of the previous position is the opposite one
next_positions = next_positions[:-1] # removes it
return next_positions
```
%% Cell type:code id: tags:
``` python
r2 = 2**0.5 # sqrt(2)
"""
Si nous sommes en p et que nous voulons aller en 7, nous bougeons que d'un pixel,
donc la valeur sera (-1, 0, 1).
Si nous sommes en p et que nous voulons aller en 4, nous bougeons de 2 pixel via diagonale,
donc la valeur sera (1,1, sqrt(2)).
"""
# The eight possible (x, y) offsets with each corresponding Euclidean distance
xy_steps = [(-1,-1,r2),( 0,-1,1),( 1,-1,r2),( 1, 0,1),( 1, 1,r2),( 0, 1,1),(-1, 1,r2),(-1, 0,1)]
# TODO
# LUT: for each 8-neighborhood and each previous direction [0,8],
# where 8 means "none", provides the list of possible directions
nd_lut = [[compute_next_ridge_following_directions(pd, x) for pd in range(9)] for x in all_8_neighborhoods]
```
%% Cell type:code id: tags:
``` python
def follow_ridge_and_compute_angle(x, y, d = 8):
px, py = x, y
length = 0.0
while length < 20: # max length followed
next_directions = nd_lut[neighborhood_values[py,px]][d]
if len(next_directions) == 0:
break
# Need to check ALL possible next directions
if (any(cn[py + xy_steps[nd][1], px + xy_steps[nd][0]] != 2 for nd in next_directions)):
break # another minutia found: we stop here
# Only the first direction has to be followed
d = next_directions[0]
ox, oy, l = xy_steps[d]
px += ox ; py += oy ; length += l
# check if the minimum length for a valid direction has been reached
return math.atan2(-py+y, px-x) if length >= 10 else None
```
%% Cell type:code id: tags:
``` python
# List format: [(x: int, y: int, type_terminaison: bool, direction: int), ...]
valid_minutiae = []
for x, y, term in filtered_minutiae:
d = None
if term: # termination: simply follow and compute the direction
d = follow_ridge_and_compute_angle(x, y)
else: # bifurcation: follow each of the three branches
dirs = nd_lut[neighborhood_values[y,x]][8] # 8 means: no previous direction
if len(dirs)==3: # only if there are exactly three branches
angles = [follow_ridge_and_compute_angle(x+xy_steps[d][0], y+xy_steps[d][1], d) for d in dirs]
if all(a is not None for a in angles):
a1, a2 = min(((angles[i], angles[(i+1)%3]) for i in range(3)), key=lambda t: angle_abs_difference(t[0], t[1]))
d = angle_mean(a1, a2)
if d is not None:
valid_minutiae.append( (x, y, term, d) )
```
%% Cell type:code id: tags:
``` python
show(draw_minutiae(fingerprint, valid_minutiae))
```
%% Cell type:markdown id: tags:
# Step 7: Creation of local structures
%% Cell type:code id: tags:
``` python
# Compute the cell coordinates of a generic local structure
mcc_radius = 70
mcc_size = 16
g = 2 * mcc_radius / mcc_size
x = np.arange(mcc_size)*g - (mcc_size/2)*g + g/2
y = x[..., np.newaxis]
iy, ix = np.nonzero(x**2 + y**2 <= mcc_radius**2)
ref_cell_coords = np.column_stack((x[ix], x[iy]))
```
%% Cell type:code id: tags:
``` python
mcc_sigma_s = 7.0
mcc_tau_psi = 400.0
mcc_mu_psi = 1e-2
def Gs(t_sqr):
"""Gaussian function with zero mean and mcc_sigma_s standard deviation, see eq. (7) in MCC paper"""
return np.exp(-0.5 * t_sqr / (mcc_sigma_s**2)) / (math.tau**0.5 * mcc_sigma_s)
def Psi(v):
"""Sigmoid function that limits the contribution of dense minutiae clusters, see eq. (4)-(5) in MCC paper"""
return 1. / (1. + np.exp(-mcc_tau_psi * (v - mcc_mu_psi)))
```
%% Cell type:code id: tags:
``` python
# n: number of minutiae
# c: number of cells in a local structure
xyd = np.array([(x,y,d) for x,y,_,d in valid_minutiae]) # matrix with all minutiae coordinates and directions (n x 3)
# rot: n x 2 x 2 (rotation matrix for each minutia)
d_cos, d_sin = np.cos(xyd[:,2]).reshape((-1,1,1)), np.sin(xyd[:,2]).reshape((-1,1,1))
rot = np.block([[d_cos, d_sin], [-d_sin, d_cos]])
# rot@ref_cell_coords.T : n x 2 x c
# xy : n x 2
xy = xyd[:,:2]
# cell_coords: n x c x 2 (cell coordinates for each local structure)
cell_coords = np.transpose(rot@ref_cell_coords.T + xy[:,:,np.newaxis],[0,2,1])
# cell_coords[:,:,np.newaxis,:] : n x c x 1 x 2
# xy : (1 x 1) x n x 2
# cell_coords[:,:,np.newaxis,:] - xy : n x c x n x 2
# dists: n x c x n (for each cell of each local structure, the distance from all minutiae)
dists = np.sum((cell_coords[:,:,np.newaxis,:] - xy)**2, -1)
# cs : n x c x n (the spatial contribution of each minutia to each cell of each local structure)
cs = Gs(dists)
diag_indices = np.arange(cs.shape[0])
cs[diag_indices,:,diag_indices] = 0 # remove the contribution of each minutia to its own cells
# local_structures : n x c (cell values for each local structure)
local_structures = Psi(np.sum(cs, -1))
```
%% Cell type:code id: tags:
``` python
@interact(i=(0,len(valid_minutiae)-1))
def test(i=0):
show(draw_minutiae_and_cylinder(fingerprint, ref_cell_coords, valid_minutiae, local_structures, i))
```
%% Cell type:markdown id: tags:
# Step 8: Fingerprint comparison
%% Cell type:code id: tags:
``` python
print(f"""Fingerprint image: {fingerprint.shape[1]}x{fingerprint.shape[0]} pixels
Minutiae: {len(valid_minutiae)}
Local structures: {local_structures.shape}""")
```
%% Cell type:code id: tags:
``` python
f1, m1, ls1 = fingerprint, valid_minutiae, local_structures
```
%% Cell type:code id: tags:
``` python
ofn = 'samples/sample_1_2' # Fingerprint of the same finger
#ofn = 'samples/sample_2' # Fingerprint of a different finger
# f2 = cv.imread(f'{ofn}.png', cv.IMREAD_GRAYSCALE)
# m2 = ???
# ls2 = ???
f2, (m2, ls2) = cv.imread(f'{ofn}.png', cv.IMREAD_GRAYSCALE), np.load(f'{ofn}.npz', allow_pickle=True).values()
```
%% Cell type:code id: tags:
``` python
# Compute
# Compute all pairwise normalized Euclidean distances between local structures in v1 and v2
# ls1 : n1 x c
# ls1[:,np.newaxis,:] : n1 x 1 x c
# ls2 : (1 x) n2 x c
# ls1[:,np.newaxis,:] - ls2 : n1 x n2 x c
# dists : n1 x n2
dists = np.linalg.norm(ls1[:,np.newaxis,:] - ls2, axis = -1)
dists /= np.linalg.norm(ls1, axis = 1)[:,np.newaxis] + np.linalg.norm(ls2, axis = 1) # Normalize as in eq. (17) of MCC paper
```
%% Cell type:code id: tags:
``` python
num_p = 5 # For simplicity: a fixed number of pairs
pairs = np.unravel_index(np.argpartition(dists, num_p, None)[:num_p], dists.shape)
score = 1 - np.mean(dists[pairs[0], pairs[1]]) # See eq. (23) in MCC paper
print(f'Comparison score: {score:.2f}')
```
%% Cell type:code id: tags:
``` python
@interact(i = (0,len(pairs[0])-1), show_local_structures = False)
def show_pairs(i=0, show_local_structures = False):
show(draw_match_pairs(f1, m1, ls1, f2, m2, ls2, ref_cell_coords, pairs, i, show_local_structures))
```
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment