I'm using Scipy for rendering planes from 3D data (vector 200x200x200).
I can specify the wanted plane by 2 vectors or vector and an angle.
I want to extract such an arbitrary slice from this 3D volume.
I found how to do it in Matlab:
http://www.mathworks.com/help/techdoc/ref/slice.html
How do I do it in Scipy?
You can use scipy.ndimage.interpolation.rotate to rotate your 3d array to whatever angle you want (it uses spline interpolation) then you can take a slice out of it.
def extract_slice(data, triplet):
"""
Algorithm:
1. find intersections of the plane with the data box edges
2. for these pts, find axis-oriented b-box
3. find the "back" trans (A,T) from R2 to R3, like X' = AX + T
use (0,0), (0,h), (w,0) which are easy to calculate
4. use the trans (with trilinear-interpolation) for every value in the 2D (w,h) image
"""
I will release the code properly in a few months I believe as a part of this project.
I was addressing a similar task as the OP so I came up with this code based on numpy (not scipy) to extract any given slice from a volume given a position vector of any point of the plane and three orthogonal orientation vectors.
I apologise for the length of my answer, but given the complexity of the problem at hand i thought it would be better to give this amount of detail.
For my particular problem these vectors were defined in mm instead of pixels so the spacing (i.e. distance between two consecutive volume pixels at each direction) was also used as input. I have used a nearest neighbour approach to interpolate the subpixel points of the slice.
reslice_volume (volume, spacing, o1, o2, n, pos)
The main steps behind this algorithm are as follow. Note that i use plane and slice interchangeably:
1. Get intersection lines between the desired plane and the bounds of the volume.
def PlaneBoundsIntersectionsLines (n, pos):
"""Outputs points and vectors defining the lines that the view creates by intersecting the volume's bounds.
Input:
Normal vector of the given plane and the coords of a point belonging to the plane.
Output:
normals_line, points_line
"""
def intersectionPlanePlane(n1,p1,n2,p2):
# Get direction of line
nout = np.cross(n1.reshape((1,3)),n2.reshape((1,3))).reshape(3,1)
nout = normalizeLength(nout)
M = np.concatenate((n1.reshape(1,3),n2.reshape(1,3)), axis=0)
b = np.zeros((2,1))
# print(n1.shape, p1.shape)
b[0,0]=np.dot(n1,p1)
b[1,0]=np.dot(n2,p2)
pout,resid,rank,s = np.linalg.lstsq(M,b, rcond=None)
return pout, nout
# ... For each face
normalFaces = np.concatenate((np.eye(3,3),np.eye(3,3)), axis = 1)
pointsFaces = np.array([[0,0,0],[0,0,0],[0,0,0], [379.9872, 379.9872, 169.5], [379.9872, 379.9872, 169.5], [379.9872, 379.9872, 169.5]]).transpose()
points_line = np.zeros((3,6))
normals_line = np.zeros((3,6))
for face in range(6):
n1 = normalFaces[:,face].reshape(3,)
p1 = pointsFaces[:,face].reshape(3,)
pout, nout = intersectionPlanePlane(n1,p1,n,pos)
points_line[:,face] = pout.reshape((3,))
normals_line[:,face] = nout.reshape((3,))
return normals_line, points_line
2. Get intersection points between these lines that are close enough to the borders of the volume to be considered corners of the intersection between the plane and the volume.
def FindPlaneCorners(normals_line, points_line):
"""Outputs the points defined by the intersection of the input lines that
are close enough to the borders of the volume to be considered corners of the view plane.
Input:
Points and vectors defining lines
Output:
p_intersection, intersecting_lines
"""
def intersectionLineLine(Up,P0,Uq,Q0):
# Computes the closest point between two lines
# Must be column points
b = np.zeros((2,1))
b[0,0] = -np.dot((P0-Q0),Up)
b[1,0] = -np.dot((P0-Q0),Uq)
A = np.zeros((2,2))
A[0,0] = np.dot(Up,Up)
A[0,1] = np.dot(-Uq,Up)
A[1,0] = np.dot(Up,Uq)
A[1,1] = np.dot(-Uq,Uq)
if ( np.abs(np.linalg.det(A)) < 10^(-10) ):
point = np.array([np.nan, np.nan, np.nan]).reshape(3,1)
else:
lbd ,resid,rank,s = np.linalg.lstsq(A,b, rcond=None)
# print('\n')
# print(lbd)
P1 = P0 + lbd[0]*Up;
Q1 = Q0 + lbd[1]*Uq;
point = (P1+Q1)/2;
return point
# ... ... Get closest point for every possible pair of lines and select only the ones inside the box
npts = 0
p_intersection = []
intersecting_lines = []
# ... Get all possible pairs of lines
possible_pairs = np.array(list(itertools.combinations(np.linspace(0,5,6), 2)))
for pair in possible_pairs:
k = int(pair[0])
j = int(pair[1])
Up = normals_line[:,k]
P0 = points_line[:,k]
Uq = normals_line[:,j]
Q0 = points_line[:,j]
closest_point = intersectionLineLine(Up,P0,Uq,Q0)
epsilon = 2.2204e-10
# ... ... Is point inside volume? Is it close to the border?
if closest_point[0] <= 379.9872 + epsilon and closest_point[0] >= 0 - epsilon and \
closest_point[1] <= 379.9872 + epsilon and closest_point[1] >= 0 - epsilon and \
closest_point[2] <= 169.5 + epsilon and closest_point[2] >= 0 - epsilon:
# ... ... Is it close to the border? 25 mm?
th = 25
if 379.9872 - closest_point[0] <= th or closest_point[0] - 0 <= th or \
379.9872 - closest_point[1] <= th or closest_point[1] - 0 <= th or \
169.5 - closest_point[2] <= th or closest_point[2] - 0 <= th:
# print('It is close to teh border')
npts += 1
p_intersection.append(closest_point)
intersecting_lines.append([k,j])
p_intersection = np.array(p_intersection).transpose()
return p_intersection, intersecting_lines
3. Transform the points found into the slice's reference frame (sRF) (we can center the RF arbitrarily within the slice plane).
dim = volume.shape
# ... Get intersection lines between plane and volume bounds
normals_line, points_line = PlaneBoundsIntersectionsLines (n, pos)
# ... Get intersections between generated lines to get corners of view plane
p_intersection, intersecting_lines = FindPlaneCorners (normals_line, points_line)
# ... Calculate parameters of the 2D slice
# ... ... Get corners of slice from volume RF (vrf) to slice RF (srf) - in this case centered in the middle of teh slice
# ... ... ... Define T_vrf2srf
Pose_slice_vrf = M_creater(o1,o2,n,pos)
# ... ... ... Apply transform
p_intersection_slicerf = np.zeros(p_intersection.shape)
for corner in range(p_intersection.shape[1]):
pt_arr = np.concatenate((p_intersection[:,corner],np.ones((1,))) ,axis = 0).reshape((4,1))
p_intersection_slicerf[:,corner] = np.matmul(np.linalg.inv(Pose_slice_vrf), pt_arr)[:-1].reshape((3,))
4. Get minimum x and y coordinates across these points and define a corner point that will be used as origin of the plane/slice. Transform this origin point back to the volume's RF (vRF) and define a new transform matrix that shifts RF from vRF to a sRF but now centered on said origin point.
5. From these inslice points coordinates we can determine the size of the slice and then use it to generate all possible inslice indexes of the target slice.
# ... ... Get slice size based on corners and spacing
spacing_slice = [1,1,8]
min_bounds_slice_xy = np.min(p_intersection_slicerf,axis=1)
max_bounds_slice_xy = np.max(p_intersection_slicerf,axis=1)
size_slice_x = int(np.ceil((max_bounds_slice_xy[0] - min_bounds_slice_xy[0] - 1e-6) / spacing_slice[0]))
size_slice_y = int(np.ceil((max_bounds_slice_xy[1] - min_bounds_slice_xy[1] - 1e-6) / spacing_slice[1]))
slice_size = [size_slice_x, size_slice_y, 1]
print('slice_size')
print(slice_size)
# ... ... Get corner in slice coords and redefine transform mat - make corner origin of the slice
origin_corner_slice = np.array([min_bounds_slice_xy[0],min_bounds_slice_xy[1],0])
pt_arr = np.concatenate((origin_corner_slice,np.ones((1,))) ,axis = 0).reshape((4,1))
origin_corner_slice_vrf = np.matmul(Pose_slice_vrf, pt_arr)[:-1].reshape((3,))
Pose_slice_origin_corner_vrf = M_creater(o1,o2,n,origin_corner_slice_vrf)
# ... ... Get every possible inslice coordinates
xvalues = np.linspace(0,size_slice_x-1,size_slice_x)
yvalues = np.linspace(0,size_slice_y-1,size_slice_y)
zvalues = np.linspace(0,0,1)
xx, yy = np.meshgrid(xvalues, yvalues)
xx = xx.transpose()
yy = yy.transpose()
zz = np.zeros(xx.shape)
inslice_coords = np.concatenate((xx.reshape(-1,1), yy.reshape(-1,1), zz.reshape(-1,1)), axis = 1)
6. Next step is to use the newly defined transform matrix (step 4) to map every possible inslice index to the volume's reference frame.
# ... ... Map every point of slice into volume's RF
inslice_coords_vrf = np.zeros(inslice_coords.shape)
for coord_set in range(inslice_coords.shape[0]):
pt_arr = np.concatenate((inslice_coords[coord_set,:],np.ones((1,))) ,axis = 0).reshape((4,1))
inslice_coords_vrf[coord_set,:] = np.matmul(Pose_slice_origin_corner_vrf, pt_arr)[:-1].reshape((3,))
7. We now have all the vRF coordinates that the slice encompasses that should be promptly converted into pixel values by dividing them by the respective spacing. At this step we find that we end up with non-integer pixel values as the slices passes through subpixel locations of the volume. We round the pixel value to its nearest integer - nearest neighbour interpolation.
# ... ... ... Convert to pixel coord - here we used teh resampled spacing
inslice_coords_vrf_px = inslice_coords_vrf.copy()
inslice_coords_vrf_px[:,0] = inslice_coords_vrf[:,0] / spacing[0]
inslice_coords_vrf_px[:,1] = inslice_coords_vrf[:,1] / spacing[1]
inslice_coords_vrf_px[:,2] = inslice_coords_vrf[:,2] / spacing[2]
# ... ... Interpolate pixel value at each mapped point - nearest neighbour int
# ... ... ... Convert pixel value to its closest existing value in the volume
inslice_coords_vrf_px = np.round(inslice_coords_vrf_px, 0).astype(int)
8. Next, we determine which pixels of the slice are actually within the bounds of the volume and get their values. Pixels outside volume are padded to 0.
# ... ... Find slice voxels within volume bounds
in_mask = np.zeros((inslice_coords_vrf_px.shape[0], 1))
idx_in = []
for vox in range(in_mask.shape[0]):
if not np.any(inslice_coords_vrf_px[vox,:]<0) and \
inslice_coords_vrf_px[vox,0]<dim[0] and \
inslice_coords_vrf_px[vox,1]<dim[1] and \
inslice_coords_vrf_px[vox,2]<dim[2]:
in_mask[vox] = 1
idx_in.append(vox)
idx_in = np.array(idx_in)
# ... ... Get pixel value from volume based on interpolated pixel indexes
extracted_slice = np.zeros((inslice_coords_vrf_px.shape[0], 1))
for point in range(inslice_coords_vrf_px.shape[0]):
if point in idx_in:
vol_idx = inslice_coords_vrf_px[point,:]
extracted_slice[point] = volume[vol_idx[0], vol_idx[1], vol_idx[2]]
# ... ... Reshape to slice shape
extracted_slice = extracted_slice.reshape((slice_size[0], slice_size[1]))
I added a plot for extra clarity. Here the volume is defined by the bounding box in black. Line intersections of the slice with the planes defined by the faces of the box/volume in dotted orange. In blue the intersection points between the previous lines. Points in pink belong to the slice and the orange ones belong to the slice and are within the volume.
In my case i was dealing with MRI volumes, so as example I added my resulting slice from the volume.
Related
Basically, we have N=10000 particles in a two-dimensional system, and for clarity, only N=1000 particles are shown in the left panel. For each particle, there's a so-called polar vector corrosponding to it. The positions and corrosponding polar vectors of particles are known, and what I need to do it to pixelate the vector fields using 2D Gaussian weight function. In formula, the procedure is as below:
Left: Polar vector fields in simulation data, subject to uncertainty. Right: Polar vector fields smoothed with Gaussian weight function, with standard deviation , truncated at .
My pseudocode works like this, and it is low-efficient for the huge for loops:
weighted_vector_sum_on_lattice = np.zeros((nPixelsInOneDim, nPixelsInOneDim, 2))
sum_weight = np.zeros((nPixelsInOneDim, nPixelsInOneDim))
sum_x = np.zeros((nPixelsInOneDim, nPixelsInOneDim))
sum_y = np.zeros((nPixelsInOneDim, nPixelsInOneDim))
# scan for all atoms
for x_pixel_index in range(nPixelsInOneDim):
for y_pixel_index in range(nPixelsInOneDim):
# calculate the coordinate of the pixel
coordinate_of_pixel = get_coordinates_from_pixel_indices([x_pixel_index, y_pixel_index])
# calculate a scalar cell index
serial_cell_index_ij = get_cell_index_of_point(coordinate_of_pixel)
# get vector cell index
i_th_cell, j_th_cell = get_vector_cell_index(serial_cell_index_ij)
# scan the neighbor cells (including itself) of cell c
for a in [-1, 0, 1]:
for b in [-1, 0, 1]:
k_th_cell = i_th_cell + a
l_th_cell = j_th_cell + b
# periodic boundary condition by shifting coordinates
k_th_cell %= nCellsInOneDim
l_th_cell %= nCellsInOneDim
# calculate the scalar cell index of the neighbor cell
serial_cell_index_kl = get_serial_cell_index(k_th_cell, l_th_cell)
# get atoms in the neighbor cells
atoms_in_kl_subcell = get_atoms_in_subcell(serial_cell_index_kl, head_of_chain, linked_list)
# scan atoms in neighbor cells
for atom_index_j in atoms_in_kl_subcell:
dist = get_distance(coordinate_of_pixel, coordinates[atom_index_j])
if dist <= filter_radius:
weight = get_gaussian(dist, standard_deviation)
sum_weight[x_pixel_index, y_pixel_index] += weight
sum_x[x_pixel_index, y_pixel_index] += weight * eigenvector[atom_index_j][0]
sum_y[x_pixel_index, y_pixel_index] += weight * eigenvector[atom_index_j][1]
weighted_vector_sum_on_lattice[:, :, 0] = sum_x/sum_weight
weighted_vector_sum_on_lattice[:, :, 1] = sum_y/sum_weight
I wonder if there is a faster way to get the vector fields smoothed with Gaussian weight function on lattice?
Problem
I have a list of coordinates that are meant to form a grid. Each coordinate has a random error component and some of the coordinates are missing. Grid could be rotated (update). I want to fit a orthogonal grid to the data points and return a list of the grid's vertices. For example:
Application
The purpose is to find a grid in a scanned image. The data points come from the results of contour or edge detection in OpenCV. An example is image with a grid of photos.
Goal
I wrote some Python code that works, but would like to find a linear algebra algorithm using SciPy, statsmodels or other modules that would be more robust and handle a small rotation of the grid (less than 10°).
Python Code Using Lists Only
# Noisy [x, y] coordinates (origin is upper-left corner)
pts = [[103,101],
[198,103],
[300, 99],
[ 97,205],
[304,202],
[102,295],
[200,303],
[104,405],
[205,394],
[298,401]]
def row_col_avgs(num_list, ratio):
# Finds the average of each row and column. Coordinates are
# assigned to a row and column by specifying an error ratio.
last_num, sum_nums, count_nums, avgs = 0, 0, 0, []
num_list.sort()
for num in num_list:
# Calculate average for last row or column and begin new row or column
if num > (1+ratio)*last_num and count_nums != 0:
avgs.append(int(round(sum_nums/count_nums,0)))
sum_nums = num
count_nums = 1
# Or continue with current row or column
else:
sum_nums += num
count_nums += 1
last_num = num
avgs.append(int(round(sum_nums/count_nums,0)))
return avgs
# Split coordinates into two lists of x's and y's
xs, ys = map(list, zip(*pts))
# Find averages of each row and column of the grid
x_avgs = row_col_avgs(xs, 0.1)
y_avgs = row_col_avgs(ys, 0.1)
# Return vertices of completed averaged grid
avg_grid = []
for y_avg in y_avgs:
avg_row = []
for x_avg in x_avgs:
avg_row.append([int(x_avg), int(y_avg)])
avg_grid.append(avg_row)
print(avg_grid)
Output
[[[102, 101], [201, 101], [301, 101]],
[[102, 204], [201, 204], [301, 204]],
[[102, 299], [201, 299], [301, 299]],
[[102, 400], [201, 400], [301, 400]]]
Parallel Slopes Ordinary Least Squares (OLS) Model:
y = mx + grp + b where m=slope, b=y-intercept, & grp=categorical variable.
This is an alternative algorithm that can handle a rotated grid.
The OLS model includes both the data points in the original orientation
and a 90° rotation of the same data points. This is necessary so all gridlines are parallel and have the same slope.
Algorithm:
Find a reference gridline to compare with remaining points by choosing two neighboring points in the first or last row with a slope closest to zero.
Calculate the distances between this reference line and the remaining points.
Segment points into groups w.r.t. the calculated distances (one group per gridline).
Repeat steps 1 to 3 for the 90 degree rotated grid and combine results.
Create a parallel slopes OLS model to determine linear equations for the gridlines.
Rotate the rotated gridlines back to their original orientation.
Calculate the intersection points.
Note: Fails if noise, angle and/or missing data are too much.
Example:
Python Code to Create Example
def create_random_example():
# Requires import of numpy and random packages
# Creates grid with random noise and missing points
# Example will fail if std_dev, rotation, pct_removed too large
# Parameters
first_row, last_row = 100, 900
first_col, last_col = 100, 600
num_rows = 6
num_cols = 4
rotation = 3 # degrees that grid is rotated
sd = 3 # percent std dev of avg x and avg y coordinates
pct_remove = 30 # percent of points to randomly remove from data
# Create grid
x = np.linspace(first_col, last_col, num_cols)
y = np.linspace(first_row, last_row, num_rows)
xx, yy = np.meshgrid(x, y)
# Add noise
x = xx.flatten() + sd * np.mean(xx) * np.random.randn(xx.size) / 100
y = yy.flatten() + sd * np.mean(yy) * np.random.randn(yy.size) / 100
# Randomly remove points
random_list = random.sample(range(0, num_cols*num_rows),
int(pct_remove*num_cols*num_rows/100))
x, y = np.delete(x, random_list), np.delete(y, random_list)
pts = np.column_stack((x, y))
# Rotate points
radians = np.radians(rotation)
rot_mat = np.array([[np.cos(radians),-np.sin(radians)],
[np.sin(radians), np.cos(radians)]])
einsum = np.einsum('ji, mni -> jmn', rot_mat, [pts])
pts = np.squeeze(einsum).T
return np.rint(pts)
Python Code to Fit Gridlines
import numpy as np
import pandas as pd
import itertools
import math
import random
from statsmodels.formula.api import ols
from scipy.spatial import KDTree
import matplotlib.pyplot as plt
def pt_line_dist(pt, ref_line):
pt1, pt2 = [ref_line[:2], ref_line[2:]]
# Distance from point to line defined by two other points
return np.linalg.norm(np.cross(pt1 - pt2, [pt[0],pt[1]])) \
/ np.linalg.norm(pt1 - pt2)
def segment_pts(amts, grp_var, grp_label):
# Segment on amounts (distances here) in last column of array
# Note: need to label groups with string for OLS model
amts = amts[amts[:, -1].argsort()]
first_amt_in_grp = amts[0][-1]
group, groups, grp = [], [], 0
for amt in amts:
if amt[-1] - first_amt_in_grp > grp_var:
groups.append(group)
first_amt_in_grp = amt[-1]
group = []; grp += 1
group.append(np.append(amt[:-1],[[grp_label + str(grp)]]))
groups.append(group)
return groups
def find_reference_line(pts):
# Find point with minimum absolute slope relative both min y and max y
y = np.hsplit(pts, 2)[1] # y column of array
m = []
for i, y_pt in enumerate([ pts[np.argmin(y)], pts[np.argmax(y)] ]):
m.append(np.zeros((pts.shape[0]-1, 5))) # dtype default is float64
m[i][:,2:4] = np.delete(pts, np.where((pts==y_pt).all(axis=1))[0], axis=0)
m[i][:,4] = abs( (m[i][:,3]-y_pt[1]) / (m[i][:,2]-y_pt[0]) )
m[i][:,:2] = y_pt
m = np.vstack((m[0], m[1]))
return m[np.argmin(m[:,4]), :4]
# Ignore division by zero (slopes of vertical lines)
np.seterr(divide='ignore')
# Create dataset and plot
pts = create_random_example()
plt.scatter(pts[:,0], pts[:,1], c='r') # plot now because pts array changes
# Average distance to the nearest neighbor of each point
tree = KDTree(pts)
nn_avg_dist = np.mean(tree.query(pts, 2)[0][:, 1])
# Find groups of points representing each gridline
groups = []
for orientation in ['o', 'r']: # original and rotated orientations
# Rotate points 90 degrees (note: this moves pts to 2nd quadrant)
if orientation == 'r':
pts[:,1] = -1 * pts[:,1]
pts[:, [1, 0]] = pts[:, [0, 1]]
# Find reference line to compare remaining points for grouping
ref_line = find_reference_line(pts) # line is defined by two points
# Distances between points and reference line
pt_dists = np.zeros((pts.shape[0], 3))
pt_dists[:,:2] = pts
pt_dists[:,2] = np.apply_along_axis(pt_line_dist, 1, pts, ref_line).T
# Segment pts into groups w.r.t. distances (one group per gridline)
# Groups have range less than nn_avg_dist.
groups += segment_pts(pt_dists, 0.7*nn_avg_dist, orientation)
# Create dataframe of groups (OLS model requires a dataframe)
df = pd.DataFrame(np.row_stack(groups), columns=['x', 'y', 'grp'])
df['x'] = pd.to_numeric(df['x'])
df['y'] = pd.to_numeric(df['y'])
# Parallel slopes OLS model
ols_model = ols("y ~ x + grp + 0", data=df).fit()
# OLS parameters
grid_lines = ols_model.params[:-1].to_frame() # panda series to dataframe
grid_lines = grid_lines.rename(columns = {0:'b'})
grid_lines['grp'] = grid_lines.index.str[4:6]
grid_lines['m'] = ols_model.params[-1] # slope
# Rotate the rotated lines back to their original orientation
grid_lines.loc[grid_lines['grp'].str[0] == 'r', 'b'] = grid_lines['b'] / grid_lines['m']
grid_lines.loc[grid_lines['grp'].str[0] == 'r', 'm'] = -1 / grid_lines['m']
# Find grid intersection points by combinations of gridlines
comb = list(itertools.combinations(grid_lines['grp'], 2))
comb = [i for i in comb if i[0][0] != 'r']
comb = [i for i in comb if i[1][0] != 'o']
df_comb = pd.DataFrame(comb, columns=['grp', 'r_grp'])
# Merge gridline parameters with grid points
grid_pts = df_comb.merge(grid_lines.drop_duplicates('grp'),how='left',on='grp')
grid_lines.rename(columns={'grp': 'r_grp'}, inplace=True)
grid_pts.rename(columns={'b':'o_b', 'm': 'o_m', 'grp':'o_grp'}, inplace=True)
grid_pts = grid_pts.merge(grid_lines.drop_duplicates('r_grp'),how='left',on='r_grp')
grid_pts.rename(columns={'b':'r_b', 'm': 'r_m'}, inplace=True)
# Calculate x, y coordinates of gridline interception points
grid_pts['x'] = (grid_pts['r_b']-grid_pts['o_b']) \
/ (grid_pts['o_m']-grid_pts['r_m'])
grid_pts['y'] = grid_pts['o_m'] * grid_pts['x'] + grid_pts['o_b']
# Results output
print(grid_lines)
print(grid_pts)
plt.scatter(grid_pts['x'], grid_pts['y'], s=8, c='b') # for setting axes
axes = plt.gca()
axes.invert_yaxis()
axes.xaxis.tick_top()
axes.set_aspect('equal')
axes.set_xlim(axes.get_xlim())
axes.set_ylim(axes.get_ylim())
x_vals = np.array(axes.get_xlim())
for idx in grid_lines.index:
y_vals = grid_lines['b'][idx] + grid_lines['m'][idx] * x_vals
plt.plot(x_vals, y_vals, c='gray')
plt.show()
A numpy implementation of your code can be found below. As the size AvgGrid is known, I pre-allocate the required memory (rather than append). This should have speed advantages, especially if the number of output vertices is large.
import numpy as np
# Input of [x, y] coordinates of a sparse grid with errors
xys = np.array([[103,101],
[198,103],
[300, 99],
[ 97,205],
[304,202],
[102,295],
[200,303],
[104,405],
[205,394],
[298,401]])
# Function to average
def ColAvgs(CoordinateList, CutoffRatio = 1.1):
# Length of CoordinateList
L = len(CoordinateList)
# Sort input
SortedList = np.sort(CoordinateList)
# Determine indices to average
RelativeIncrease = SortedList[-(L-1):]/SortedList[:(L-1)]
CriticalIndices = np.flatnonzero(RelativeIncrease > CutoffRatio) + 1
Indices = np.hstack((0,CriticalIndices))
if (Indices[-1] != L):
Indices = np.hstack((Indices,L))
#print(Indices) # Uncomment to show index construction
# Compute averages
Avgs = np.empty((len(Indices)-1)); Avgs[:] = np.NaN
for iter in range(len(Avgs)):
Avgs[iter] = int( round(np.mean(SortedList[Indices[iter]:Indices[(iter+1)]]) ) )
# Return output
return Avgs
# Compute x- and y-coordinates of vertices
AvgsXcoord = ColAvgs(xys[:,0])
AvgsYcoord = ColAvgs(xys[:,1])
# Return all vertices
AvgGrid = np.empty((len(AvgsXcoord)*len(AvgsYcoord),2)); AvgGrid[:] = np.NaN
iter = 0
for y in AvgsYcoord:
for x in AvgsXcoord:
AvgGrid[iter, :] = np.hstack((x,y))
iter = iter+1
print(AvgGrid)
If you project all points on a vertical or horizontal axis, the problem turns to one of clustering with equally spaced clusters.
To perform these clusterings, you can consider the distances between the successive (sorted) points. They will form two clusters: short distances corresponding to noise, and longer ones for the grid size. You can solve the two-way clustering using the Otsu method.
I'm currently attempting to implement this algorithm for volume rendering in Python, and am conceptually confused about their method of generating the LH histogram (see section 3.1, page 4).
I have a 3D stack of DICOM images, and calculated its gradient magnitude and the 2 corresponding azimuth and elevation angles with it (which I found out about here), as well as finding the second derivative.
Now, the algorithm is asking me to iterate through a set of voxels, and "track a path by integrating the gradient field in both directions...using the second order Runge-Kutta method with an integration step of one voxel".
What I don't understand is how to use the 2 angles I calculated to integrate the gradient field in said direction. I understand that you can use trilinear interpolation to get intermediate voxel values, but I don't understand how to get the voxel coordinates I want using the angles I have.
In other words, I start at a given voxel position, and want to take a 1 voxel step in the direction of the 2 angles calculated for that voxel (one in the x-y direction, the other in the z-direction). How would I take this step at these 2 angles and retrieve the new (x, y, z) voxel coordinates?
Apologies in advance, as I have a very basic background in Calc II/III, so vector fields/visualization of 3D spaces is still a little rough for me.
Creating 3D stack of DICOM images:
def collect_data(data_path):
print "collecting data"
files = [] # create an empty list
for dirName, subdirList, fileList in os.walk(data_path):
for filename in fileList:
if ".dcm" in filename:
files.append(os.path.join(dirName,filename))
# Get reference file
ref = dicom.read_file(files[0])
# Load dimensions based on the number of rows, columns, and slices (along the Z axis)
pixel_dims = (int(ref.Rows), int(ref.Columns), len(files))
# Load spacing values (in mm)
pixel_spacings = (float(ref.PixelSpacing[0]), float(ref.PixelSpacing[1]), float(ref.SliceThickness))
x = np.arange(0.0, (pixel_dims[0]+1)*pixel_spacings[0], pixel_spacings[0])
y = np.arange(0.0, (pixel_dims[1]+1)*pixel_spacings[1], pixel_spacings[1])
z = np.arange(0.0, (pixel_dims[2]+1)*pixel_spacings[2], pixel_spacings[2])
# Row and column directional cosines
orientation = ref.ImageOrientationPatient
# This will become the intensity values
dcm = np.zeros(pixel_dims, dtype=ref.pixel_array.dtype)
origins = []
# loop through all the DICOM files
for filename in files:
# read the file
ds = dicom.read_file(filename)
#get pixel spacing and origin information
origins.append(ds.ImagePositionPatient) #[0,0,0] coordinates in real 3D space (in mm)
# store the raw image data
dcm[:, :, files.index(filename)] = ds.pixel_array
return dcm, origins, pixel_spacings, orientation
Calculating gradient magnitude:
def calculate_gradient_magnitude(dcm):
print "calculating gradient magnitude"
gradient_magnitude = []
gradient_direction = []
gradx = np.zeros(dcm.shape)
sobel(dcm,0,gradx)
grady = np.zeros(dcm.shape)
sobel(dcm,1,grady)
gradz = np.zeros(dcm.shape)
sobel(dcm,2,gradz)
gradient = np.sqrt(gradx**2 + grady**2 + gradz**2)
azimuthal = np.arctan2(grady, gradx)
elevation = np.arctan(gradz,gradient)
azimuthal = np.degrees(azimuthal)
elevation = np.degrees(elevation)
return gradient, azimuthal, elevation
Converting to patient coordinate system to get actual voxel position:
def get_patient_position(dcm, origins, pixel_spacing, orientation):
"""
Image Space --> Anatomical (Patient) Space is an affine transformation
using the Image Orientation (Patient), Image Position (Patient), and
Pixel Spacing properties from the DICOM header
"""
print "getting patient coordinates"
world_coordinates = np.empty((dcm.shape[0], dcm.shape[1],dcm.shape[2], 3))
affine_matrix = np.zeros((4,4), dtype=np.float32)
rows = dcm.shape[0]
cols = dcm.shape[1]
num_slices = dcm.shape[2]
image_orientation_x = np.array([ orientation[0], orientation[1], orientation[2] ]).reshape(3,1)
image_orientation_y = np.array([ orientation[3], orientation[4], orientation[5] ]).reshape(3,1)
pixel_spacing_x = pixel_spacing[0]
# Construct affine matrix
# Method from:
# http://nipy.org/nibabel/dicom/dicom_orientation.html
T_1 = origins[0]
T_n = origins[num_slices-1]
affine_matrix[0,0] = image_orientation_y[0] * pixel_spacing[0]
affine_matrix[0,1] = image_orientation_x[0] * pixel_spacing[1]
affine_matrix[0,3] = T_1[0]
affine_matrix[1,0] = image_orientation_y[1] * pixel_spacing[0]
affine_matrix[1,1] = image_orientation_x[1] * pixel_spacing[1]
affine_matrix[1,3] = T_1[1]
affine_matrix[2,0] = image_orientation_y[2] * pixel_spacing[0]
affine_matrix[2,1] = image_orientation_x[2] * pixel_spacing[1]
affine_matrix[2,3] = T_1[2]
affine_matrix[3,3] = 1
k1 = (T_1[0] - T_n[0])/ (1 - num_slices)
k2 = (T_1[1] - T_n[1])/ (1 - num_slices)
k3 = (T_1[2] - T_n[2])/ (1 - num_slices)
affine_matrix[:3, 2] = np.array([k1,k2,k3])
for z in range(num_slices):
for r in range(rows):
for c in range(cols):
vector = np.array([r, c, 0, 1]).reshape((4,1))
result = np.matmul(affine_matrix, vector)
result = np.delete(result, 3, axis=0)
result = np.transpose(result)
world_coordinates[r,c,z] = result
# print "Finished slice ", str(z)
# np.save('./data/saved/world_coordinates_3d.npy', str(world_coordinates))
return world_coordinates
Now I'm at the point where I want to write this function:
def create_lh_histogram(patient_positions, dcm, magnitude, azimuthal, elevation):
print "constructing LH histogram"
# Get 2nd derivative
second_derivative = gaussian_filter(magnitude, sigma=1, order=1)
# Determine if voxels lie on boundary or not (thresholding)
# Still have to code out: let's say the thresholded voxels are in
# a numpy array called voxels
#Iterate through all thresholded voxels and integrate gradient field in
# both directions using 2nd-order Runge-Kutta
vox_it = voxels.nditer(voxels, flags=['multi_index'])
while not vox_it.finished:
# ???
I'm running quite a complex code so I won't bother with details as I've had it working before but now im getting this error.
Particle is a 3D tuple filled with 0 or 255, and I am using the scipy centre of mass function and then trying to turn the value into its closest integer (as I'm dealing with arrays). The error is found with on the last line... can anyone explain why this might be??
2nd line fills Particle
3rd line deletes any surrounding particles with a different label (This is in a for loop for all labels)
Particle = []
Particle = big_labelled_stack[x_start+20:x_stop+20,y_start+20:y_stop+20,z_start+20:z_stop+20]
Particle = np.where(Particle == i ,255,0)
CoM = scipy.ndimage.measurements.center_of_mass(Particle)
CoM = [ (int(round(x)) for x in CoM ]
Thanks in advance. If you need more code just ask but I dont think it will help you and its very messy.
################## MORE CODE
border = 30
[labelled_stack,no_of_label] = label(labelled,structure_array,output_type)
# RE-LABEL particles now no. of seeds has been reduced! LAST LABELLING
#Increase size of stack by increasing borders and equal them to 0; to allow us to cut out particles into cube shape which else might lye outside the border
h,w,l = labelled.shape
big_labelled_stack = np.zeros(shape=(h+60,w+60,l+60),dtype=np.uint32)
# Creates an empty border around labelled_stack full of zeros of size border
if (no_of_label > 0): #Small sample may return no particles.. so this stage not neccesary
info = np.zeros(shape=(no_of_label,19)) #Creates array to store coordinates of particles
for i in np.arange(1,no_of_label,1):
coordinates = find_objects(labelled_stack == i)[0] #Find coordinates of label i.
x_start = int(coordinates[0].start)
x_stop = int(coordinates[0].stop)
y_start = int(coordinates[1].start)
y_stop = int(coordinates[1].stop)
z_start = int(coordinates[2].start)
z_stop = int(coordinates[2].stop)
dx = (x_stop - x_start)
dy = (y_stop - y_start)
dz = (z_stop - z_start)
Particle = np.zeros(shape=(dy,dx,dz),dtype = np.uint16)
Particle = big_labelled_stack[x_start+30:x_start+dx+30,y_start+30:y_start+dy+30,z_start+30:z_start+dz+30]
Particle = np.where(Particle == i ,255,0)
big_labelled_stack[border:h+border,border:w+border,border:l+border] = labelled_stack
big_labelled_stack = np.where(big_labelled_stack == i , 255,0)
CoM_big_stack = scipy.ndimage.measurements.center_of_mass(big_labelled_stack)
C = np.asarray(CoM_big_stack) - border
if dx > dy:
b = dx
else: #Finds the largest of delta_x,y,z and saves as b, so that we create 'Cubic_Particle' of size 2bx2bx2b (cubic box)
b = dy
if dz > b:
b = dz
CoM = scipy.ndimage.measurements.center_of_mass(Particle)
CoM = [ (int(round(x))) for x in CoM ]
Cubic_Particle = np.zeros(shape=(2*b,2*b,2*b))
Cubic_Particle[(b-CoM[0]):(b+dx-CoM[0]),(b-CoM[1]):(b+dy-CoM[1]),(b-CoM[2]):(b+dz-CoM[2])] = Particle
volume = Cubic_Particle.size # Gives volume of the box in voxels
info[i-1,:] = [C[0],C[1],C[2],i,C[0]-b,C[1]-b,C[2]-b,C[0]+b,C[1]+b,C[2]+b,volume,0,0,0,0,0,0,0,0] # Fills an array with label.No., size of box, and co-ords
else:
print('No particles found, try increasing the sample size')
info = []
Ok, so I have a stack full of labelled particles, there are two things I am trying to do, first find the centre of masses of each particle with respect ot the labelled_stack which is what CoM_big_labelled_stack (and C) does. and stores the co-ords in a list (tuple) called info. I am also trying to create a cubic box around the particle, with its centre of mass as the centre (which is relating to the CoM variable), so first I use the find objects function in scipy to find a particle, i then use these coordinates to create a non-cubic box around the particle, and find its centre of mass.I then find the longest dimension of the box and call it b, creating a cubic box of size 2b and filling it with particle in the right position.
Sorry this code is a mess, I am very new to Python
[COMPLETELY REWRITTEN]
I'm looking for a way of interpolating large set of data in dimensions ranging from 3 to 7.
The data is, by nature, on a non rectangular grid and non uniformly spaced.
I looked every option I could think of (griddata, KDTree + magic, linear interpolation, reworked map_coordinates...): the fastest and most usable tool seems to be Scipy's LinearNDInterpolator function. Linear interpolation in such high dimensions space is fine and should be precise enough.
However, there is one big shortcoming with this class: data with gaps or "concave regions" will produce extrapolated results when I want only interpolation.
This is best seen with some pictures (2-D test). In the following I produce some randomly generated data for X, and VALUE, while Y is upper-bounded by a function of X (just so I create gaps).
After rescaling data (mostly done using pieces of code of the LinearNDIterpolator from the master, ie. development, branch), Delaunay triangulation will produce a Convex Hull that includes the gap, and will "extrapolate" in this region. The term "extrapolate" is not really correct here, in a technical sense, but I think would be appropriate given the fact original data is assumed to be sufficiently well sampled so that the big gaps means "no data allowed" (not physical).
To start handling the problem, I "tagged" every Delaunay (hyper-)triangles whose (hyper-)volume is higher than a user-defined threshold (by default the volume equivalent to 5% of the data extent in each dimension).
Generating random data, and evaluating the values using this technique would produce the following figure:
Black dots (with red or white rings) is the randomly generated data to be evaluated. Red rings indicates points that are being rejected (ie. value = NaN) by my custom class based on LinearNDInterpolator, and white rings show accepted points.
For clarity I've plotted triangles that have been rejected from the original Delaunay triangulation.
As you can see, there are still some white rings points that fall in the gap, which I do not want. This is because the simplex to which they belong has a volume less than the authorized maximum volume (some of these triangles even appear as lines on the figure, so it is hard to see)
My question is: how could I improve from here? What could be done?
I was thinking of grabbing all points that fall in a small ball around each evaluated point, and see if there are points in that. But this is not a good solution since it would be resource-consuming and not precise enough (eg. what about points very close to the bottom of the gap, but yet outside the upper envelope?)
Here is my custom interpolation module I used:
#!/usr/bin/env python
"""
Custom N-D linear interpolation class, based on scipy's LinearNDInterpolator.
The main differences are:
- auto-scaling
- interpolation: inside convex hull (normal behavior), and "close enough" to original data.
This rejects points that would normally be interpolated by LinearNDInterpolator.
"""
# ================
# Python modules
# ================
import cPickle
import numpy as np
from scipy.spatial import Delaunay
from scipy.interpolate import LinearNDInterpolator
from scipy.misc import factorial
# =======================
# Convenience functions
# =======================
def _inv_log10(x):
return 10**x
def det(coords): #, n):
"""
Return the determinant of the given coordinates (not the usual determinant, but the one used to compute
the hyper-volume of an hyper-triangle)
From a Delaunay triangulation, the coordinates of one simplex (ie. hyper-triangle) is given by:
coords_i = tri.points[simplex_i]
where
tri = Delaunay(points)
simplex_i = tri.simplices[i]
In an N-dimensional space, the simplex will have N+1 points, each one of them of dimension N.
Eg. in 3D, a points i has coordinates pi = (xi, yi, zi). Therefore p1 = points 1 = (x1, y1, z1)
|x1 x2 x3 x4|
|y1 y2 y3 y4| |(x1-x4) (x2-x4) (x3-x4)|
det = |z1 z2 z3 z4| = |(y1-y4) (y2-y4) (y3-y4)|
|1 1 1 1 | |(z1-z4) (z2-z4) (z3-z4)|
"""
# assert n == len(coords[0]), 'number of dimensions of coordinates (%d) != %d' % (len(coords[0]), n)
q = coords[:-1, :] - coords[-1, None, :]
sign, logdet = np.linalg.slogdet(q)
return sign * np.exp(logdet)
# ==============================
# LinearNDInterpolator wrapper
# ==============================
class Interp(object):
"""
Simple wrapper around LinearNDInterpolator.
"""
def __init__(self, points, values, **kwargs):
"""
:param points: list of coordinates (eg. [(0, 1), (0, 3), (4, 4.5)] for 3 points in 2-D)
:param values: list of associated value(s) for each point (eg. [1, 2, 3] for 3 points of single value)
:keyword rescale: rescale data points so that the final extents is [0, 1] in every dimensions
:keyword transform: transform data points (prior to rescaling). If True, automatically transform dimension coordinates
if extents span more than 2 order of magnitudes. It can also be a list of tuples of
(transformation function, inverse function), that will be applied whenever needed.
:keyword fill_value: outside bounds interpolation values (default: np.nan)
"""
try:
points = np.asanyarray(points, dtype=np.float64)
values = np.asanyarray(values, dtype=np.float64)
except ValueError:
raise ValueError('Cannot convert input points to an array of floats')
# dimensions / number of points and values
self.ndim = points.shape[1]
self.nvalues = values.shape[1]
self.npoints = points.shape[0]
# locals
self._idims = range(self.ndim)
# extents
self.minis = np.min(points, axis=0)
self.maxis = np.max(points, axis=0)
self.ranges = self.maxis - self.minis
self.magnitudes = self.maxis / self.minis
# options
rescale = kwargs.pop('rescale', True)
transform = kwargs.pop('transform', True)
fill_value = kwargs.pop('fill_value', np.nan)
# transformation
if transform:
transforms = []
if transform is True:
# automatic transformation -> if extent >= 2 order of magnitudes: f(x) = log10(x)
for i, e in enumerate(self.magnitudes):
if e >= 100.:
transforms.append((np.log10, _inv_log10))
else:
transforms.append(None)
if not transforms:
transforms = None
else:
err_msg = 'transform: both the transformation function and its inverse must be given in a tuple'
if not isinstance(transform, (tuple, list)):
raise ValueError(err_msg)
if (self.ndim > 1) and (len(transform) != self.ndim):
raise ValueError('transform: None or transformations tuple must be given for every dimension')
for t in transform:
if not isinstance(t, (tuple, list)):
raise ValueError(err_msg)
elif t is None:
transforms.append(None)
else:
transforms.append(t)
self.transforms = transforms
else:
self.transforms = None
points = self._transform(points)
# scaling
self.offset = 0.
self.scale = 1.
self.rescale = rescale
if rescale:
self.offset = np.mean(points, axis=0)
self.scale = (points - self.offset).ptp(axis=0)
self.scale[~(self.scale > 0)] = 1.0 # avoid division by 0
points = self._rescale(points)
# triangulation
self.tri = self._triangulate(points)
# volumes
self.fact = 1. / factorial(self.ndim)
self.volume_max = np.product(self.tri.points.ptp(axis=0) * 0.05) # 5% peak-to-peak in each dimension
self.rej_idx = None
self.rej_vol = None
self.cached_rej = False
# linear interpolation
self.fill_value = fill_value
self.func = LinearNDInterpolator(self.tri, values, fill_value=fill_value)
def _triangulate(self, points, **kwargs):
"""
Delaunay triangulation
"""
return Delaunay(points, **kwargs)
def _get_volume_simplex(self, point):
"""
Compute the simplex volume of the given point
"""
i = self.tri.find_simplex(point)
idx = self.tri.simplices[i]
return np.abs(self.fact * det(self.tri.points[idx]))
def cache_rejected_triangles(self, p=None, check_min=False):
"""
Cache the indexes of rejected triangles.
OPTIONS
p -- peak-to-peak percentage in each dimension for the maximum volume calculation
Default: None (default at __init__: p = 0.05)
Type: float (0 < p <= 1)
Type: list of floats (length = # dimensions)
check_min -- check that the minimum spacing in each dimension is at least equal to p * extent
Default: False
Warning: *p* must be given
"""
self.cached_rej = True
if p is not None:
p = np.array(p)
# update the maximum hyper-triangle volume (p % of the extent in each dimension)
self.volume_max = np.product(self.tri.points.ptp(axis=0) * p)
if check_min:
assert p is not None, 'You must give *p* parameter for checking minimum volume of hyper-triangle'
ptps = self.tri.points.ptp(axis=0)
ps = np.ones(self.ndim) * p
n_up = 0
for i in self._idims:
_x = np.unique(self.tri.points[:, i])
mini = np.min(_x[1:] - _x[:-1])
if mini > (ptps[i] * ps[i]):
n_up += 1
print 'WARNING: changed max. volume axis of dim. %d from %.3g to %.3g' % (i+1, ps[i], mini)
ps[i] = mini
if n_up:
new_vol = np.product(ptps * ps)
print 'CHANGE: old volume was = %.3g, and is now = %.3g' % (self.volume_max, new_vol)
self.volume_max = new_vol
rej_idx = []
rej_vol = []
for i, simplex in enumerate(self.tri.simplices):
vol = np.abs(self.fact * det(self.tri.points[simplex]))
if vol > self.volume_max:
rej_idx.append(i)
rej_vol.append(vol)
self.rej_idx = np.array(rej_idx)
self.rej_vol = np.array(rej_vol)
def _transform(self, points, inverse=False):
"""
Transform point coordinates using functions. Set 'inverse' to True to transform back.
"""
if self.transforms is not None:
j = 1 - int(inverse)
for i in self._idims:
t = self.transforms[i]
if t is None:
continue
points[:, i] = t[j](points[:, i])
return points
def _rescale(self, points, inverse=False):
"""
Rescale point coordinates so that extents in each dimensions span [0, 1]. Set 'inverse' to True to scale back.
"""
if self.rescale:
if inverse:
points = points * self.scale + self.offset
else:
points = (points - self.offset) / self.scale
return points
def _check(self, x, res):
"""
Check that interpolation results are close enough to real data and have not been extrapolated.
"""
points = np.asanyarray(x)
if points.ndim == 1:
# only 1 point
values = np.asanyarray(res).reshape(1, self.ndim)
else:
# more than 1 point
values = np.asanyarray(res).reshape(points.shape[0], self.ndim)
if self.cached_rej:
idx = np.unique(np.where(np.isfinite(values))[0])
ui_tri, uii = np.unique(self.tri.find_simplex(points[idx]), return_inverse=True)
umask = np.lib.arraysetops.in1d(ui_tri, self.rej_idx, assume_unique=True)
mask = umask[uii]
values[idx[mask], :] = self.fill_value
else:
for i, v in enumerate(values):
if not np.isnan(v[0]):
vol = self._get_volume_simplex(points[i])
if vol > self.volume_max:
# reject
values[i][:] = self.fill_value
return values.reshape(res.shape)
def __call__(self, x, check=False):
"""
Interpolate. If 'check' is True, check that interpolated points are close enough to real data.
"""
_x = self._rescale(self._transform(x))
res = self.func(_x)
if check:
res = self._check(_x, res)
return res
def ev(self, x, check=False):
"""
Alias for __call__
"""
return self.__call__(x, check=check)
def get_original_points(self):
"""
Return original points
"""
return self._transform(self._rescale(self.func.points, inverse=True), inverse=True)
def get_original_values(self):
"""
Return original values
"""
return self.func.values
# ===========================
# Save / load interpolation
# ===========================
def save(filename, interp):
"""
Dump the Interp instance to a binary file with cPickle (protocol 2)
"""
with open(filename, 'wb') as f:
cPickle.dump(interp, f, protocol=2)
def load(filename):
"""
Load a previously saved (cPickled with save_interp function) Interp instance
"""
with open(filename, 'rb') as f:
interp = cPickle.load(f)
return interp
And the test script:
#!/usr/bin/env python
"""
Test the custom interpolation class (see interp.py)
"""
import sys
import numpy as np
from interp import Interp
import matplotlib.pyplot as plt
# generate random data
n = 2000 # number of generated points
x = np.random.random(n)
def f(v):
maxi = v ** (1/(v+1e-5)) * (v - 5.) ** 2 - np.exp(v-7) + 1
return np.random.random() * maxi
y = map(f, x * 10)
z = np.random.random(n)
points = np.array((x, y)).T
values = np.random.random(points.shape)
# create interpolation function
func = Interp(points, values, transform=False)
func.cache_rejected_triangles(p=0.05, check_min=True)
# generate random data + evaluate
pts = np.random.random((500, points.shape[1]))
pts *= points.ptp(0)
pts += points.min(0)
res = func(pts, check=True)
# rejected points indexes
idx_rej = np.unique(np.where(np.isnan(res))[0])
n_rej = len(idx_rej)
print '%d points (%.0f%%) have been rejected' % (n_rej, 100.*n_rej/pts.shape[0])
# plot rejected triangles
fig = plt.figure()
ax = plt.gca()
for i in func.rej_idx:
_x = [p for p in points[func.tri.simplices[i], 0]]
_x += [points[func.tri.simplices[i][0], 0]]
_y = [p for p in points[func.tri.simplices[i], 1]]
_y += [points[func.tri.simplices[i][0], 1]]
ax.plot(_x, _y, c='k', ls='-', zorder=100)
# plot original data
ax.scatter(points[:, 0], points[:, 1], c='b', linewidths=0, s=20, zorder=50)
# plot all points (both accepted and rejected): in white
ax.scatter(pts[:, 0], pts[:, 1], c='k', edgecolors='w', linewidths=1, zorder=150, s=30)
# re-plot rejected points: in red
ax.scatter(pts[idx_rej, 0], pts[idx_rej, 1], c='k', edgecolors='r', linewidths=1, zorder=200, s=30)
fig.savefig('img_tri.png', transparent=True, dpi=300)