package-style architecture
This commit is contained in:
368
package/lib/convex_hull.py
Executable file
368
package/lib/convex_hull.py
Executable file
@@ -0,0 +1,368 @@
|
||||
"""
|
||||
Library functions for graham algorithm implementation (find the convex hull of a given list of points).
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
import numpy as np
|
||||
|
||||
|
||||
def clean_ROI(image):
|
||||
"""
|
||||
Remove instruments borders from an observation.
|
||||
"""
|
||||
H, J = [], []
|
||||
|
||||
shape = np.array(image.shape)
|
||||
row, col = np.indices(shape)
|
||||
|
||||
for i in range(0, shape[0]):
|
||||
r = row[i, :][image[i, :] > 0.]
|
||||
c = col[i, :][image[i, :] > 0.]
|
||||
if len(r) > 1 and len(c) > 1:
|
||||
H.append((r[0], c[0]))
|
||||
H.append((r[-1], c[-1]))
|
||||
H = np.array(H)
|
||||
for j in range(0, shape[1]):
|
||||
r = row[:, j][image[:, j] > 0.]
|
||||
c = col[:, j][image[:, j] > 0.]
|
||||
if len(r) > 1 and len(c) > 1:
|
||||
J.append((r[0], c[0]))
|
||||
J.append((r[-1], c[-1]))
|
||||
J = np.array(J)
|
||||
xmin = np.min([H[:, 1].min(), J[:, 1].min()])
|
||||
xmax = np.max([H[:, 1].max(), J[:, 1].max()])+1
|
||||
ymin = np.min([H[:, 0].min(), J[:, 0].min()])
|
||||
ymax = np.max([H[:, 0].max(), J[:, 0].max()])+1
|
||||
return np.array([xmin, xmax, ymin, ymax])
|
||||
|
||||
|
||||
# Define angle and vectors operations
|
||||
def vector(A, B):
|
||||
"""
|
||||
Define a vector from the inputed points A, B.
|
||||
"""
|
||||
return (B[0] - A[0], B[1] - A[1])
|
||||
|
||||
|
||||
def determinant(u, v):
|
||||
"""
|
||||
Compute the determinant of 2 vectors.
|
||||
--------
|
||||
Inputs:
|
||||
u, v : vectors (list of length 2)
|
||||
Vectors for which the determinant must be computed.
|
||||
----------
|
||||
Returns:
|
||||
determinant : real
|
||||
Determinant of the input vectors.
|
||||
"""
|
||||
return u[0] * v[1] - u[1] * v[0]
|
||||
|
||||
|
||||
def pseudo_angle(A, B, C):
|
||||
"""
|
||||
Define a pseudo-angle from the inputed points A, B, C.
|
||||
"""
|
||||
u = vector(A, B)
|
||||
v = vector(A, C)
|
||||
return determinant(u, v)
|
||||
|
||||
|
||||
def distance(A, B):
|
||||
"""
|
||||
Compute the euclidian distance betwwen 2 points of the plan.
|
||||
----------
|
||||
Inputs:
|
||||
A, B : points (list of length 2)
|
||||
Points between which the distance must be computed.
|
||||
----------
|
||||
Returns:
|
||||
distance : real
|
||||
Euclidian distance between A, B.
|
||||
"""
|
||||
x, y = vector(A, B)
|
||||
return np.sqrt(x ** 2 + y ** 2)
|
||||
|
||||
|
||||
# Define lexicographic and composition order
|
||||
def lexico(A, B):
|
||||
"""
|
||||
Define the lexicographic order between points A, B.
|
||||
A <= B if:
|
||||
- y_a < y_b
|
||||
or - y_a = y_b and x_a < x_b
|
||||
----------
|
||||
Inputs:
|
||||
A, B : points (list of length 2)
|
||||
Points compared for the lexicographic order
|
||||
----------
|
||||
Returns:
|
||||
lexico : boolean
|
||||
True if A<=B, False otherwise.
|
||||
"""
|
||||
return (A[1] < B[1]) or (A[1] == B[1] and A[0] <= B[0])
|
||||
|
||||
|
||||
def min_lexico(s):
|
||||
"""
|
||||
Return the minimum of a list of points for the lexicographic order.
|
||||
----------
|
||||
Inputs:
|
||||
s : list of points
|
||||
List of points in which we look for the minimum for the lexicographic
|
||||
order
|
||||
----------
|
||||
Returns:
|
||||
m : point (list of length 2)
|
||||
Minimum for the lexicographic order in the input list s.
|
||||
"""
|
||||
m = s[0]
|
||||
for x in s:
|
||||
if lexico(x, m):
|
||||
m = x
|
||||
return m
|
||||
|
||||
|
||||
def comp(Omega, A, B):
|
||||
"""
|
||||
Test the order relation between points such that A <= B :
|
||||
This is True if :
|
||||
- (Omega-A,Omega-B) is a right-hand basis of the plan.
|
||||
or - (Omega-A,Omega-B) are linearly dependent.
|
||||
----------
|
||||
Inputs:
|
||||
Omega, A, B : points (list of length 2)
|
||||
Points compared for the composition order
|
||||
----------
|
||||
Returns:
|
||||
comp : boolean
|
||||
True if A <= B, False otherwise.
|
||||
"""
|
||||
t = pseudo_angle(Omega, A, B)
|
||||
if t > 0:
|
||||
return True
|
||||
elif t < 0:
|
||||
return False
|
||||
else:
|
||||
return distance(Omega, A) <= distance(Omega, B)
|
||||
|
||||
|
||||
# Implement quicksort
|
||||
def partition(s, left, right, order):
|
||||
"""
|
||||
Take a random element of a list 's' between indexes 'left', 'right' and place it
|
||||
at its right spot using relation order 'order'. Return the index at which
|
||||
it was placed.
|
||||
----------
|
||||
Inputs:
|
||||
s : list
|
||||
List of elements to be ordered.
|
||||
left, right : int
|
||||
Index of the first and last elements to be considered.
|
||||
order : func: A, B -> bool
|
||||
Relation order between 2 elements A, B that returns True if A<=B,
|
||||
False otherwise.
|
||||
----------
|
||||
Returns:
|
||||
index : int
|
||||
Index at which have been placed the element chosen by the function.
|
||||
"""
|
||||
i = left - 1
|
||||
for j in range(left, right):
|
||||
if order(s[j], s[right]):
|
||||
i = i + 1
|
||||
temp = deepcopy(s[i])
|
||||
s[i] = deepcopy(s[j])
|
||||
s[j] = deepcopy(temp)
|
||||
temp = deepcopy(s[i+1])
|
||||
s[i+1] = deepcopy(s[right])
|
||||
s[right] = deepcopy(temp)
|
||||
return i + 1
|
||||
|
||||
|
||||
def sort_aux(s, left, right, order):
|
||||
"""
|
||||
Sort a list 's' between indexes 'left', 'right' using relation order 'order' by
|
||||
dividing it in 2 sub-lists and sorting these.
|
||||
"""
|
||||
if left <= right:
|
||||
# Call partition function that gives an index on which the list will be divided
|
||||
q = partition(s, left, right, order)
|
||||
sort_aux(s, left, q - 1, order)
|
||||
sort_aux(s, q + 1, right, order)
|
||||
|
||||
|
||||
def quicksort(s, order):
|
||||
"""
|
||||
Implement the quicksort algorithm on the list 's' using the relation order
|
||||
'order'.
|
||||
"""
|
||||
# Call auxiliary sort on whole list.
|
||||
sort_aux(s, 0, len(s) - 1, order)
|
||||
|
||||
|
||||
def sort_angles_distances(Omega, s):
|
||||
"""
|
||||
Sort the list of points 's' for the composition order given reference point
|
||||
Omega.
|
||||
"""
|
||||
def order(A, B): return comp(Omega, A, B)
|
||||
quicksort(s, order)
|
||||
|
||||
|
||||
# Define fuction for stacks (use here python lists with stack operations).
|
||||
def empty_stack(): return []
|
||||
def stack(S, A): S.append(A)
|
||||
def unstack(S): S.pop()
|
||||
def stack_top(S): return S[-1]
|
||||
def stack_sub_top(S): return S[-2]
|
||||
|
||||
|
||||
# Alignement handling
|
||||
def del_aux(s, k, Omega):
|
||||
"""
|
||||
Returne the index of the first element in 's' after the index 'k' that is
|
||||
not linearly dependent of (Omega-s[k]), first element not aligned to Omega
|
||||
and s[k].
|
||||
----------
|
||||
Inputs:
|
||||
s : list
|
||||
List of points
|
||||
k : int
|
||||
Index from which to look for aligned points in s.
|
||||
Omega : point (list of length 2)
|
||||
Reference point in 's' to test alignement.
|
||||
----------
|
||||
Returns:
|
||||
index : int
|
||||
Index of the first element not aligned to Omega and s[k].
|
||||
"""
|
||||
p = s[k]
|
||||
j = k + 1
|
||||
while (j < len(s)) and (pseudo_angle(Omega, p, s[j]) == 0):
|
||||
j = j + 1
|
||||
return j
|
||||
|
||||
|
||||
def del_aligned(s, Omega):
|
||||
"""
|
||||
Deprive a list of points 's' from all intermediary points that are aligned
|
||||
using Omega as a reference point.
|
||||
----------
|
||||
Inputs:
|
||||
s : list
|
||||
List of points
|
||||
Omega : point (list of length 2)
|
||||
Point of reference to test alignement.
|
||||
-----------
|
||||
Returns:
|
||||
s1 : list
|
||||
List of points where no points are aligned using reference point Omega.
|
||||
"""
|
||||
s1 = [s[0]]
|
||||
n = len(s)
|
||||
k = 1
|
||||
while k < n:
|
||||
# Get the index of the last point aligned with (Omega-s[k])
|
||||
k = del_aux(s, k, Omega)
|
||||
s1.append(s[k - 1])
|
||||
return s1
|
||||
|
||||
|
||||
# Implement Graham algorithm
|
||||
def convex_hull(H):
|
||||
"""
|
||||
Implement Graham algorithm to find the convex hull of a given list of
|
||||
points, handling aligned points.
|
||||
----------
|
||||
Inputs:
|
||||
A : list
|
||||
List of points for which the the convex hull must be returned.
|
||||
----------
|
||||
S : list
|
||||
List of points defining the convex hull of the input list A.
|
||||
"""
|
||||
S = empty_stack()
|
||||
Omega = min_lexico(H)
|
||||
sort_angles_distances(Omega, H)
|
||||
A = del_aligned(H, Omega)
|
||||
if len(A) < 3:
|
||||
return H
|
||||
else:
|
||||
stack(S, A[0])
|
||||
stack(S, A[1])
|
||||
stack(S, A[2])
|
||||
for i in range(3, len(A)):
|
||||
while pseudo_angle(stack_sub_top(S), stack_top(S), A[i]) <= 0:
|
||||
unstack(S)
|
||||
stack(S, A[i])
|
||||
return S
|
||||
|
||||
|
||||
def image_hull(image, step=5, null_val=0., inside=True):
|
||||
"""
|
||||
Compute the convex hull of a 2D image and return the 4 relevant coordinates
|
||||
of the maximum included rectangle (ie. crop image to maximum rectangle).
|
||||
----------
|
||||
Inputs:
|
||||
image : numpy.ndarray
|
||||
2D array of floats corresponding to an image in intensity that need to
|
||||
be cropped down.
|
||||
step : int, optional
|
||||
For images with straight edges, not all lines and columns need to be
|
||||
browsed in order to have a good convex hull. Step value determine
|
||||
how many row/columns can be jumped. With step=2 every other line will
|
||||
be browsed.
|
||||
Defaults to 5.
|
||||
null_val : float, optional
|
||||
Pixel value determining the threshold for what is considered 'outside'
|
||||
the image. All border pixels below this value will be taken out.
|
||||
Defaults to 0.
|
||||
inside : boolean, optional
|
||||
If True, the cropped image will be the maximum rectangle included
|
||||
inside the image. If False, the cropped image will be the minimum
|
||||
rectangle in which the whole image is included.
|
||||
Defaults to True.
|
||||
----------
|
||||
Returns:
|
||||
vertex : numpy.ndarray
|
||||
Array containing the vertex of the maximum included rectangle.
|
||||
"""
|
||||
H = []
|
||||
shape = np.array(image.shape)
|
||||
row, col = np.indices(shape)
|
||||
for i in range(0, int(min(shape)/2), step):
|
||||
r1, r2 = row[i, :][image[i, :] > null_val], row[-i, :][image[-i, :] > null_val]
|
||||
c1, c2 = col[i, :][image[i, :] > null_val], col[-i, :][image[-i, :] > null_val]
|
||||
if r1.shape[0] > 1:
|
||||
H.append((r1[0], c1[0]))
|
||||
H.append((r1[-1], c1[-1]))
|
||||
if r2.shape[0] > 1:
|
||||
H.append((r2[0], c2[0]))
|
||||
H.append((r2[-1], c2[-1]))
|
||||
S = np.array(convex_hull(H))
|
||||
|
||||
x_min, y_min = S[:, 0] < S[:, 0].mean(), S[:, 1] < S[:, 1].mean()
|
||||
x_max, y_max = S[:, 0] > S[:, 0].mean(), S[:, 1] > S[:, 1].mean()
|
||||
# Get the 4 extrema
|
||||
# S0 = S[x_min*y_min][np.argmin(S[x_min*y_min][:, 0])]
|
||||
# S1 = S[x_min*y_max][np.argmax(S[x_min*y_max][:, 1])]
|
||||
# S2 = S[x_max*y_min][np.argmin(S[x_max*y_min][:, 1])]
|
||||
# S3 = S[x_max*y_max][np.argmax(S[x_max*y_max][:, 0])]
|
||||
S0 = S[x_min*y_min][np.abs(0-S[x_min*y_min].sum(axis=1)).min() == np.abs(0-S[x_min*y_min].sum(axis=1))][0]
|
||||
S1 = S[x_min*y_max][np.abs(shape[1]-S[x_min*y_max].sum(axis=1)).min() == np.abs(shape[1]-S[x_min*y_max].sum(axis=1))][0]
|
||||
S2 = S[x_max*y_min][np.abs(shape[0]-S[x_max*y_min].sum(axis=1)).min() == np.abs(shape[0]-S[x_max*y_min].sum(axis=1))][0]
|
||||
S3 = S[x_max*y_max][np.abs(shape.sum()-S[x_max*y_max].sum(axis=1)).min() == np.abs(shape.sum()-S[x_max*y_max].sum(axis=1))][0]
|
||||
# Get the vertex of the biggest included rectangle
|
||||
if inside:
|
||||
f0 = np.max([S0[0], S1[0]])
|
||||
f1 = np.min([S2[0], S3[0]])
|
||||
f2 = np.max([S0[1], S2[1]])
|
||||
f3 = np.min([S1[1], S3[1]])
|
||||
else:
|
||||
f0 = np.min([S0[0], S1[0]])
|
||||
f1 = np.max([S2[0], S3[0]])
|
||||
f2 = np.min([S0[1], S2[1]])
|
||||
f3 = np.max([S1[1], S3[1]])
|
||||
|
||||
return np.array([f0, f1, f2, f3]).astype(int)
|
||||
Reference in New Issue
Block a user