简体   繁体   中英

In python interpolate a line of values between two points on a 2d grid of zeroes

TLDR:

After finding 2 points in a 2d numpy array how do I interpolate a line of 1s between them in an array of 0s?

Context:

Currently I am trying to do a 2d operation on a 3d array from binarized medical image data (0 and 1). The ultimate goal is to add a line of 1s between the starting and ending point of the filled voxels/pixels (ie the first and last instance).

For this I use SimpleITK to slice out a single row, which I then convert to a numpy array. Following other examples I have written functions that return a set of arrays that show the filled (1) pixels and empty (0) pixels.

From the earliest and last instances I "fill" the points between by adding 1 to all the points and then replaces the 2's with 1. What I want to do is then add this line back to the 2d array and, ultimately, back to the 3d array.

I know that I have a list of coordinates returned though the scipy.ndimage.map_coordinates(np.transpose(z), line_array), but I have no idea how to apply that to the original array.

In my mind I could simply see creating a 2d array of 0s that may then simply be added to the original 2d array. Even so I can't figure out how to interpolate the line onto the 2d array, and then onto the 3d array. The desire to work in 2d first is because these arrays can be very very large. Any help would be very much appreciated.

import numpy as np
import scipy
import matplotlib.pyplot as plt
import simpleITK as sitk
from timeit import default_timer as timer

#Functions used are shown below

#Read in a CT scan using SimpleITK
ctscan = sitk.ReadImage("example.mhd")

#Extract a slice along the Z axis (Simple ITK uses x, y, z indexing)
z = ctscan[:,:,150:151]

#Convert the slice to a numpy array, which is then z, y, x indexing
z = sitk.GetArrayFromImage(z)

#Drop the 3rd dimension to view in matplotlib
z = z[0, :, :]
plt.imshow(z)

#Get the line and the line array
line, line_array = extract_line(0, 0, z.shape[1], z.shape[0], z)

#Returned from scipy.ndimage.map_coordinates
#1d array of 490 elements
#In [153]: len(line)
#Out[153]: 490

#2d tuple with coordinates 
#In [154]: line_array.shape
#Out[154]: (2, 490)

#Get the indices that are filled, the first and last contiguous set
points, start, end = get_line_limits(line)

#Number of arrays that are filled with 1s
#In [158]: len(points)
#Out[158]: 36

#First instance of contiguous 1's
#In [159]: start
#Out[159]: array([35, 36, 37], dtype=int64)

#Last instance of contiguous 1's
#In [160]: end
#Out[160]: array([424, 425], dtype=int64)

#If I am interpreting this then that should be the first point
#In [161]: x1 = line_array[0][35]
#In [162]: y1 = line_array[1][35]

#And this the last
#In [161]: x2 = line_array[0][425]
#In [162]: y2 = line_array[1][425]

#From here I know that I can create a "blank" array, but I don't know how
#To "place" the line...
V = np.zeros((z.shape[0], z.shape[1]))

In [166]: V = np.zeros((z.shape[0], z.shape[1]))

'''
In [167]: V
Out[167]:
array([[0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.]])
'''

#and then 
new = z + V

#Hopefully this would then show the line between the two defined points #array.
plt.imshow(new)


"""
Functions. It's likely that the answer is already somewhere in here but I've missed it from sheer idiocy.
"""

def extract_line(x0, y0, x1, y1, z):
    """
    Extract a line from a 2d slice.
    :param x0: Starting point of line on x axis, usually 0
    :param y0: Starting point of line on y axis, usually 0
    :param x1: Ending point of line on x axis, usually the len
    :param y1: Ending point of line on x axis, usually the len
    :param z: The 2d Numpy array
    :return: Returns an interpolated line with filled pixels and their coordinates. Taken from https://stackoverflow.com/questions/7878398/how-to-extract-an-arbitrary-line-of-values-from-a-numpy-array
    """

    start = timer()
    x0, y0 = float(x0), float(y0) # These are in pixel coordinates!!
    x1, y1 = float(x1), float(y1)
    x_len = abs(x0 - x1)
    y_len = abs(y0 - y1)
    line_length = int(np.sqrt((x_len**2) + (y_len**2))) #Length of line
    x, y = np.linspace(x0, x1, line_length), np.linspace(y0, y1, line_length)
    line_array = np.vstack((x, y))
    # Extract the values along the line, using cubic interpolation
    zi = scipy.ndimage.map_coordinates(np.transpose(z), line_array)
    '''
    #Uncomment this is you want to view the results.
    #Also I understand matplotlib clearly does this somehow, any way to
    #Simple return the array? 
    fig, axes = plt.subplots(nrows=2)
    axes[0].imshow(z)
    axes[0].plot([x0, x1], [y0, y1], 'ro-')
    axes[0].axis('image')
    axes[1].plot(zi)
    plt.show()
    '''
    stop = timer()
    print(abs(start - stop))
    return zi, line_array

def consecutive(data, stepsize=1):
    """
    Function to find consective elements in an array. 
    :param data: numpy array.
    :param stepsize: how many values between elements before splitting the array. 
    :return: Returns an array broken along the step size.
    """
    consecutive = np.split(data, np.where(np.diff(data) != stepsize)[0]+1)
    return consecutive


def get_line_limits(line):
    """
    Function to find the first and last instance of the filed pixels as determined by extract_line.
    :param line: numpy array returned from extract line.
    :return: Returns the indeces of the filled (i.e. 1) pixels, along with the first and last set. 
    """
    start = timer()
    line_points = np.where(line==1)
    #Call the consecutive function to get the contiguous points.
    line_points = consecutive(line_points[0])
    line_start = line_points[0]
    line_end = line_points[(len(line_points) - 1)]
    stop = timer()
    print("It took {} seconds to get limits.".format(abs(start-stop)))
    return line_points, line_start, line_end


def place_line(x0, y0, x1, y1, z):
    """
    Mystical function that doesn't exist yet because I can't seem to work it out. 
    """

The expected result would be an array that looked like the original but with a clear line from the start to the end of the "filled" pixels.

So thanks to Ardweaden for suggesting I find a simple way to state my objective, and Mad Physicist for pointing me in the direction at the Bresenham line algorithm. After that I was able to find a couple python implementation that bridged the gap. The working code for two versions is below (if anyone has a faster way to do it I will mark that as the accepted answer):

#Starting from the line limits obtained in the original questions code
x0 = np.ceil(line_array[0][start[0]]) #first filled element on x
y0 = np.ceil(line_array[1][start[0]]) #first filled element on y

x1 = np.floor(line_array[0][end[(len(end) - 1)]]) #last filled element on x
y1 = np.floor(line_array[1][end[(len(end) - 1)]]) #last filled element on y

#The skimage implementation of Bresenham's line algorithm 
# https://scikit-image.org/docs/dev/api/skimage.draw.html#skimage.draw.line
from skimage.draw import line

#Make a 2d array of 0s that is the same size as the original 2d array z
V = np.zeros((z.shape[0], z.shape[1]), dtype=np.uint8)

#This flips the y and x from how I think of it...
rr, cc = line(int(y0), int(x0), int(y1), int(x1))

#Cast the line coordinates as 1s
V[rr, cc] = 1

#Then add the new array to the old array (although could just write the line on z).
new_z = V + z

#Use numpy where to change the 2 values back to 1 values.
new_z[np.where(new_z==2)] = 1

#Show the final image
plt.imshow(new_z)


# This implementation is a short script from https://github.com/encukou/bresenham and can just be 
# brought into the existing code base without importing an external library. 

# The code can be modified, but as is doesn't have a return so place it into a list
new_line = list(bresenham(int(y0), int(x0), int(y1), int(x1)))

#Use list comprehension to get the arrays
x_array = np.array([item[0] for item in new_line])
y_array = np.array([item[1] for item in new_line])

#Then cast them as 1's
V[x_array, y_array] = 1

#Combine the original and the new array
new_z = V + z

#Recast the 2's to 1's
new_z[np.where(new_z==2)] = 1

#And view
plt.imshow(new_z)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM