measure-fiber-diameter / line_fit.py
fcossio's picture
showing lines
b215ba8
raw
history blame
6.52 kB
import numpy as np
from scipy.optimize import curve_fit
from typing import Tuple, List
import cv2
point = Tuple[float,float]
line = Tuple[point, point]
measurements = List[line]
class LineFit:
def __init__(self, n: int, step_size:float) -> None:
"""Model that fits a line in a binary image and measures diameter of fibers
:param n: number of measurements to do along the fitted line.
:param step_size: step size of diameter measurement (in pixels). Can be fraction.
"""
self.n = n
self.step_size = step_size
def get_coordinates(self, im, value_for_mask):
#I = rgb2gray(I_orig) #we can delete this if we get binary images
mask = im > value_for_mask
fiber_coor = np.argwhere(mask)
x = fiber_coor[:, 1]
y = fiber_coor[:, 0]
return x, y
def func_line(self, x, a, b):
return a * x + b
def func_line_inv(self, y, a, b):
return (y - b)/a
def get_fited_line_x_y(self, im):
value_for_mask = (int(np.max(im))+int(np.min(im)))/2 # Pixels to mask in get_coordinate
x, y = self.get_coordinates(im, value_for_mask)
popt, pcov = curve_fit(self.func_line, x, y)
return x, y, popt, pcov
def get_fited_line_y_x(self, im):
value_for_mask = (int(np.max(im))+int(np.min(im)))/2 # Pixels to mask in get_coordinate
x, y = self.get_coordinates(im, value_for_mask)
popt, pcov = curve_fit(self.func_line, y, x)
return x, y, popt, pcov
def get_better_fit(self, x, y, popt, popt_inv, pcov, pcov_inv):
diagonal = np.diagonal(pcov)
diagonal_inv = np.diagonal(pcov_inv)
if np.less(diagonal, diagonal_inv).all() == True:
popt_fit = popt
x_line = np.arange(0, max(x), 1)
y_line = []
for i in x_line:
a = self.func_line(x_line[i], *popt)
y_line.append(a)
y_fit = y_line
x_fit = x_line
p1 = [x_fit[0],y_fit[0]]
p2 = [x_fit[-1],y_fit[-1]]
elif np.less(diagonal, diagonal_inv).all() == False:
popt_fit = [1/popt_inv[0], (-popt_inv[1])/popt_inv[0]]
y_line = np.arange(0, max(y), 1)
x_line = []
for i in y_line:
a = self.func_line(y_line[i], *popt_inv)
x_line.append(a)
y_fit = y_line
x_fit = x_line
p1 = [x_fit[0],y_fit[0]]
p2 = [x_fit[-1],y_fit[-1]]
else:
print("One of the pcov values is True and the rest are False")
return popt_fit, x_fit, y_fit, p1, p2
def get_point(self, t, p1, p2):
dx = p2[0]-p1[0]
dy = p2[1]-p1[1]
p = [(dx * t + p1[0]), (dy * t + p1[1])]
return p, dx, dy
def get_normal_vector(self, t, dx, dy, p3):
n_pos = [-dy, dx]
mag_pos = np.linalg.norm(n_pos)
nu_pos = n_pos/mag_pos
u_pos = [(nu_pos[0] * t + p3[0]), (nu_pos[1] * t + p3[1])]
return u_pos
def is_inside(self, im, pos):
if not (0 <= pos[0] < im.shape[0]):
return False
if not (0 <= pos[1] < im.shape[1]):
return False
return True
def get_pixels_half (self, pos_or_neg, im, dx, dy, p3):
color_threshold = (int(np.max(im))+int(np.min(im)))/2
for ts in (range(len(im[0]))):
u = self.get_normal_vector((pos_or_neg*(ts+(self.step_size))), dx, dy, p3)
test_point = round(u[1]),round(u[0])
if not self.is_inside(im, test_point):
return None, None
test = im[test_point[0], test_point[1]] > color_threshold
if test == False:
pixels = ts - 1
break
# plt.plot(u[0], u[1], 'c.', markersize=12)
return pixels, (u[0], u[1])
def get_calculated_diameter(self, im, p1, p2):
color_threshold = (int(np.max(im))+int(np.min(im)))/2
diameters = []
lines = []
#mask_meas_lines = np.zeros_like(im)
for n in range(1, self.n+1):
t = 1/(self.n+1)
p3, dx, dy = self.get_point((t * n), p1, p2)
test_point = round(p3[1]),round(p3[0])
if not self.is_inside(im, test_point):
continue
true_point = im[test_point[0], test_point[1]] > color_threshold
if true_point == False:
continue
if true_point == True:
radius_p, cp1 = self.get_pixels_half(1, im, dx, dy, p3)
radius_n, cp2 = self.get_pixels_half(-1, im, dx, dy, p3)
if (radius_p != None) and (radius_n != None):
max_val = max(radius_p, radius_n)
min_val = min(radius_p, radius_n)
equal = abs((max_val - min_val)/(max_val + 1e-5))
if equal < 0.1:
lines.append((cp1,cp2))
diameters.append(radius_p+radius_n)
# mask_meas_lines = self.mask_measured_lines(im, lines)
# plt.plot(p3[0], p3[1], 'r.', markersize=12)
calculated_diameter = np.array(diameters).mean()
return calculated_diameter, lines
def line_to_arrays(self, line):
return [line[0][0], line[1][0]], [line[0][1], line[1][1]]
def mask_measured_lines(self, im, lines):
mask = np.zeros_like(im)
for p1, p2 in lines:
if not (p1 == None or p2 == None):
cv2.line(mask, np.array(p1).astype(np.int32), np.array(p2).astype(np.int32), 1, 1)
return mask
def predict(self, im: np.ndarray):
x, y, popt, pcov = self.get_fited_line_x_y(im)
_, _, popt_inv, pcov_inv = self.get_fited_line_y_x(im)
popt_fit, x_fit, y_fit, p1, p2 = self.get_better_fit(x, y, popt, popt_inv, pcov, pcov_inv)
calculated_diameter, lines = self.get_calculated_diameter(im, p1, p2)
mask_meas_lines = self.mask_measured_lines(im, lines)
#for line in lines:
# plt.plot(*self.line_to_arrays(line), 'c-')
return calculated_diameter, lines
if __name__ == "__main__":
import os
model = LineFit(10, 0.5)
dataset_path = "/Users/carmenlopez/dev/diameterY/scratch/dataset_files"
example_path = os.path.join(dataset_path, "test_0005.npz")
example = np.load(example_path)
diameter_pred, mask_meas_lines = model.predict(example["x"])
print(diameter_pred, example["d"])