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

added ipynb notebook

parents
No related branches found
No related tags found
No related merge requests found
samples/sample_1_1.png

60.3 KiB

File added
samples/sample_1_2.png

74.8 KiB

File added
samples/sample_2.png

66.1 KiB

samples/test01.jpg

56.5 KiB

samples/test02.jpg

87.8 KiB

samples/test03.jpg

197 KiB

samples/test04.jpg

37.7 KiB

%% 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))
```
utils.py 0 → 100644
import math
import numpy as np
import cv2 as cv
import urllib.request
import IPython
import base64
import html
# Utility function to show an image
def show(*images, enlarge_small_images = True, max_per_row = -1, font_size = 0):
if len(images) == 2 and type(images[1])==str:
images = [(images[0], images[1])]
def convert_for_display(img):
if img.dtype!=np.uint8:
a, b = img.min(), img.max()
if a==b:
offset, mult, d = 0, 0, 1
elif a<0:
offset, mult, d = 128, 127, max(abs(a), abs(b))
else:
offset, mult, d = 0, 255, b
img = np.clip(offset + mult*(img.astype(float))/d, 0, 255).astype(np.uint8)
return img
def convert(imgOrTuple):
try:
img, title = imgOrTuple
if type(title)!=str:
img, title = imgOrTuple, ''
except ValueError:
img, title = imgOrTuple, ''
if type(img)==str:
data = img
else:
img = convert_for_display(img)
if enlarge_small_images:
REF_SCALE = 100
h, w = img.shape[:2]
if h<REF_SCALE or w<REF_SCALE:
scale = max(1, min(REF_SCALE//h, REF_SCALE//w))
img = cv.resize(img,(w*scale,h*scale), interpolation=cv.INTER_NEAREST)
data = 'data:image/png;base64,' + base64.b64encode(cv.imencode('.png', img)[1]).decode('utf8')
return data, title
if max_per_row == -1:
max_per_row = len(images)
rows = [images[x:x+max_per_row] for x in range(0, len(images), max_per_row)]
font = f"font-size: {font_size}px;" if font_size else ""
html_content = ""
for r in rows:
l = [convert(t) for t in r]
html_content += "".join(["<table><tr>"]
+ [f"<td style='text-align:center;{font}'>{html.escape(t)}</td>" for _,t in l]
+ ["</tr><tr>"]
+ [f"<td style='text-align:center;'><img src='{d}'></td>" for d,_ in l]
+ ["</tr></table>"])
IPython.display.display(IPython.display.HTML(html_content))
# Utility function to load an image from an URL
def load_from_url(url):
resp = urllib.request.urlopen(url)
image = np.asarray(bytearray(resp.read()), dtype=np.uint8)
return cv.imdecode(image, cv.IMREAD_GRAYSCALE)
# Utility function to draw orientations over an image
def draw_orientations(fingerprint, orientations, strengths, mask, scale = 3, step = 8, border = 0):
if strengths is None:
strengths = np.ones_like(orientations)
h, w = fingerprint.shape
sf = cv.resize(fingerprint, (w*scale, h*scale), interpolation = cv.INTER_NEAREST)
res = cv.cvtColor(sf, cv.COLOR_GRAY2BGR)
d = (scale // 2) + 1
sd = (step+1)//2
c = np.round(np.cos(orientations) * strengths * d * sd).astype(int)
s = np.round(-np.sin(orientations) * strengths * d * sd).astype(int) # minus for the direction of the y axis
thickness = 1 + scale // 5
for y in range(border, h-border, step):
for x in range(border, w-border, step):
if mask is None or mask[y, x] != 0:
ox, oy = c[y, x], s[y, x]
cv.line(res, (d+x*scale-ox,d+y*scale-oy), (d+x*scale+ox,d+y*scale+oy), (255,0,0), thickness, cv.LINE_AA)
return res
# Utility function to draw a set of minutiae over an image
def draw_minutiae(fingerprint, minutiae, termination_color = (255,0,0), bifurcation_color = (0,0,255)):
res = cv.cvtColor(fingerprint, cv.COLOR_GRAY2BGR)
for x, y, t, *d in minutiae:
color = termination_color if t else bifurcation_color
if len(d)==0:
cv.drawMarker(res, (x,y), color, cv.MARKER_CROSS, 8)
else:
d = d[0]
ox = int(round(math.cos(d) * 7))
oy = int(round(math.sin(d) * 7))
cv.circle(res, (x,y), 3, color, 1, cv.LINE_AA)
cv.line(res, (x,y), (x+ox,y-oy), color, 1, cv.LINE_AA)
return res
# Utility function to generate gabor filter kernels
_sigma_conv = (3.0/2.0)/((6*math.log(10))**0.5)
# sigma is adjusted according to the ridge period, so that the filter does not contain more than three effective peaks
def _gabor_sigma(ridge_period):
return _sigma_conv * ridge_period
def _gabor_size(ridge_period):
p = int(round(ridge_period * 2 + 1))
if p % 2 == 0:
p += 1
return (p, p)
def gabor_kernel(period, orientation):
f = cv.getGaborKernel(_gabor_size(period), _gabor_sigma(period), np.pi/2 - orientation, period, gamma = 1, psi = 0)
f /= f.sum()
f -= f.mean()
return f
# Utility functions for minutiae
def angle_abs_difference(a, b):
return math.pi - abs(abs(a - b) - math.pi)
def angle_mean(a, b):
return math.atan2((math.sin(a)+math.sin(b))/2, ((math.cos(a)+math.cos(b))/2))
# Utility functions for MCC
def draw_minutiae_and_cylinder(fingerprint, origin_cell_coords, minutiae, values, i, show_cylinder = True):
def _compute_actual_cylinder_coordinates(x, y, t, d):
c, s = math.cos(d), math.sin(d)
rot = np.array([[c, s],[-s, c]])
return (rot@origin_cell_coords.T + np.array([x,y])[:,np.newaxis]).T
res = draw_minutiae(fingerprint, minutiae)
if show_cylinder:
for v, (cx, cy) in zip(values[i], _compute_actual_cylinder_coordinates(*minutiae[i])):
cv.circle(res, (int(round(cx)), int(round(cy))), 3, (0,int(round(v*255)),0), 1, cv.LINE_AA)
return res
def draw_match_pairs(f1, m1, v1, f2, m2, v2, cells_coords, pairs, i, show_cylinders = True):
#nd = _current_parameters.ND
h1, w1 = f1.shape
h2, w2 = f2.shape
p1, p2 = pairs
res = np.full((max(h1,h2), w1+w2, 3), 255, np.uint8)
res[:h1,:w1] = draw_minutiae_and_cylinder(f1, cells_coords, m1, v1, p1[i], show_cylinders)
res[:h2,w1:w1+w2] = draw_minutiae_and_cylinder(f2, cells_coords, m2, v2, p2[i], show_cylinders)
for k, (i1, i2) in enumerate(zip(p1, p2)):
(x1, y1, *_), (x2, y2, *_) = m1[i1], m2[i2]
cv.line(res, (int(x1), int(y1)), (w1+int(x2), int(y2)), (0,0,255) if k!=i else (0,255,255), 1, cv.LINE_AA)
return res
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment