|
|
|
import numpy as np
|
|
import cv2
|
|
import itertools
|
|
from numba import njit, int64
|
|
from numba.typed import List
|
|
from numba.types import Tuple
|
|
|
|
|
|
tuple_type = Tuple((int64, int64))
|
|
|
|
|
|
def find_neighbours(fibers_map, point):
|
|
"""
|
|
Find the next point in the fiber starting from the given point.
|
|
The function returns None if the point is not in the fiber.
|
|
"""
|
|
|
|
neighbors = []
|
|
h, w = fibers_map.shape
|
|
for i in range(-1, 2):
|
|
for j in range(-1, 2):
|
|
|
|
if i == 0 and j == 0:
|
|
continue
|
|
|
|
nextpoint = (point[0] + i, point[1] + j)
|
|
|
|
if (
|
|
nextpoint[0] < 0
|
|
or nextpoint[0] >= h
|
|
or nextpoint[1] < 0
|
|
or nextpoint[1] >= w
|
|
):
|
|
continue
|
|
|
|
|
|
if fibers_map[nextpoint]:
|
|
neighbors.append(nextpoint)
|
|
return neighbors
|
|
|
|
|
|
def compute_points_angle(fibers_map, points, steps=25):
|
|
"""
|
|
For each endpoint, follow the fiber for a given number of steps and estimate the tangent line by
|
|
fitting a line to the visited points. The angle of the line is returned.
|
|
"""
|
|
points_angle = np.zeros((len(points),), dtype=np.float32)
|
|
for i, point in enumerate(points):
|
|
|
|
|
|
|
|
visited = trace_from_point(
|
|
fibers_map > 0, (point[0], point[1]), max_length=steps
|
|
)
|
|
visited = np.array(visited)
|
|
vx, vy, x, y = cv2.fitLine(visited[:, ::-1], cv2.DIST_L2, 0, 0.01, 0.01)
|
|
|
|
points_angle[i] = np.arctan(vy / vx)
|
|
|
|
return points_angle
|
|
|
|
|
|
def generate_nonadjacent_combination(input_list, take_n):
|
|
"""
|
|
It generates combinations of m taken n at a time where there is no adjacent n.
|
|
INPUT:
|
|
input_list = (iterable) List of elements you want to extract the combination
|
|
take_n = (integer) Number of elements that you are going to take at a time in
|
|
each combination
|
|
OUTPUT:
|
|
all_comb = (np.array) with all the combinations
|
|
"""
|
|
all_comb = []
|
|
for comb in itertools.combinations(input_list, take_n):
|
|
comb = np.array(comb)
|
|
d = np.diff(comb)
|
|
if len(d[d == 1]) == 0 and comb[-1] - comb[0] != 7:
|
|
all_comb.append(comb)
|
|
return all_comb
|
|
|
|
|
|
def populate_intersection_kernel(combinations):
|
|
"""
|
|
Maps the numbers from 0-7 into the 8 pixels surrounding the center pixel in
|
|
a 9 x 9 matrix clockwisely i.e. up_pixel = 0, right_pixel = 2, etc. And
|
|
generates a kernel that represents a line intersection, where the center
|
|
pixel is occupied and 3 or 4 pixels of the border are ocuppied too.
|
|
INPUT:
|
|
combinations = (np.array) matrix where every row is a vector of combinations
|
|
OUTPUT:
|
|
kernels = (List) list of 9 x 9 kernels/masks. each element is a mask.
|
|
"""
|
|
n = len(combinations[0])
|
|
template = np.array(([-1, -1, -1], [-1, 1, -1], [-1, -1, -1]), dtype="int")
|
|
match = [(0, 1), (0, 2), (1, 2), (2, 2), (2, 1), (2, 0), (1, 0), (0, 0)]
|
|
kernels = []
|
|
for n in combinations:
|
|
tmp = np.copy(template)
|
|
for m in n:
|
|
tmp[match[m][0], match[m][1]] = 1
|
|
kernels.append(tmp)
|
|
return kernels
|
|
|
|
|
|
def give_intersection_kernels():
|
|
"""
|
|
Generates all the intersection kernels in a 9x9 matrix.
|
|
INPUT:
|
|
None
|
|
OUTPUT:
|
|
kernels = (List) list of 9 x 9 kernels/masks. each element is a mask.
|
|
"""
|
|
input_list = np.arange(8)
|
|
taken_n = [4, 3]
|
|
kernels = []
|
|
for taken in taken_n:
|
|
comb = generate_nonadjacent_combination(input_list, taken)
|
|
tmp_ker = populate_intersection_kernel(comb)
|
|
kernels.extend(tmp_ker)
|
|
return kernels
|
|
|
|
|
|
def find_line_intersection(input_image, show=0):
|
|
"""
|
|
Applies morphologyEx with parameter HitsMiss to look for all the curve
|
|
intersection kernels generated with give_intersection_kernels() function.
|
|
INPUT:
|
|
input_image = (np.array dtype=np.uint8) binarized m x n image matrix
|
|
OUTPUT:
|
|
output_image = (np.array dtype=np.uint8) image where the nonzero pixels
|
|
are the line intersection.
|
|
"""
|
|
input_image = input_image.astype(np.uint8)
|
|
kernel = np.array(give_intersection_kernels())
|
|
output_image = np.zeros(input_image.shape)
|
|
for i in np.arange(len(kernel)):
|
|
out = cv2.morphologyEx(
|
|
input_image,
|
|
cv2.MORPH_HITMISS,
|
|
kernel[i, :, :],
|
|
borderValue=0,
|
|
borderType=cv2.BORDER_CONSTANT,
|
|
)
|
|
output_image = output_image + out
|
|
|
|
return output_image
|
|
|
|
|
|
@njit
|
|
def get_neighbors_8(y, x, shape):
|
|
neighbors = List.empty_list(tuple_type)
|
|
for dy in range(-1, 2):
|
|
for dx in range(-1, 2):
|
|
if dy == 0 and dx == 0:
|
|
continue
|
|
ny, nx = y + dy, x + dx
|
|
if 0 <= ny < shape[0] and 0 <= nx < shape[1]:
|
|
neighbors.append((ny, nx))
|
|
return neighbors
|
|
|
|
|
|
@njit
|
|
def find_endpoints(skel):
|
|
endpoints = List.empty_list(tuple_type)
|
|
for y in range(skel.shape[0]):
|
|
for x in range(skel.shape[1]):
|
|
if skel[y, x] == 1:
|
|
count = 0
|
|
neighbors = get_neighbors_8(y, x, skel.shape)
|
|
for ny, nx in neighbors:
|
|
if skel[ny, nx] == 1:
|
|
count += 1
|
|
if count == 1:
|
|
endpoints.append((y, x))
|
|
return endpoints
|
|
|
|
|
|
@njit
|
|
def trace_skeleton(skel):
|
|
endpoints = find_endpoints(skel)
|
|
if len(endpoints) < 1:
|
|
return List.empty_list(tuple_type)
|
|
|
|
return trace_from_point(skel, endpoints[0], max_length=skel.sum())
|
|
|
|
|
|
@njit
|
|
def trace_from_point(skel, point, max_length=25):
|
|
visited = np.zeros_like(skel, dtype=np.uint8)
|
|
path = List.empty_list(tuple_type)
|
|
|
|
|
|
y, x = point
|
|
if y < 0 or y >= skel.shape[0] or x < 0 or x >= skel.shape[1] or skel[y, x] != 1:
|
|
return path
|
|
|
|
stack = List.empty_list(tuple_type)
|
|
stack.append(point)
|
|
|
|
while len(stack) > 0 and len(path) < max_length:
|
|
y, x = stack.pop()
|
|
if visited[y, x]:
|
|
continue
|
|
visited[y, x] = 1
|
|
path.append((y, x))
|
|
neighbors = get_neighbors_8(y, x, skel.shape)
|
|
for ny, nx in neighbors:
|
|
if skel[ny, nx] == 1 and not visited[ny, nx]:
|
|
stack.append((ny, nx))
|
|
return path
|
|
|