0

I have done a lot of searching but have yet to find an answer. I am currently working on some data of a crop field. I have PLY files for multiple fields which I have successfully read into, filtered, and visualised using Python and VTK. My main goal is to eventually segment and run analysis on individual crop plots.

However to make that task easier I first want to "Normalize" my point cloud so that all plots are essentially "on the same level". From the image I have attached you can see that the point clod slopes from one corner to its opposite. So what I want to flatten out the image so the ground points are all on the same plane/ level. And the reset of the points adjusted accordingly.

Point Cloud

I've also included my code to show how I got to this point. If anyone has any advice on how I can achieve the normalising to one plane I would be very appreciative. Sadly I cannot include my data as it is work related.

Thanks. Josh

import vtk
from vtk.util import numpy_support
import numpy as np

filename = 'File.ply'

# Reader
r = vtk.vtkPLYReader()
r.SetFileName(filename)

# Filters
vgf = vtk.vtkVertexGlyphFilter()
vgf.SetInputConnection(r.GetOutputPort())

# Elevation
pc = r.GetOutput()
bounds = pc.GetBounds()
#print(bounds)
minz = bounds[4]
maxz = bounds[5]
#print(bounds[4], bounds[5])
evgf = vtk.vtkElevationFilter()
evgf.SetInputConnection(vgf.GetOutputPort())
evgf.SetLowPoint(0, 0, minz)
evgf.SetHighPoint(0, 0, maxz)
#pc.GetNumberOfPoints()

# Look up table
lut = vtk.vtkLookupTable()
lut.SetHueRange(0.667, 0)
lut.SetSaturationRange(1, 1)
lut.SetValueRange(1, 1)
lut.Build

# Renderer
mapper = vtk.vtkPolyDataMapper()
mapper.SetInputConnection(evgf.GetOutputPort())
mapper.SetLookupTable(lut)

actor = vtk.vtkActor()
actor.SetMapper(mapper)

renderer = vtk.vtkRenderer()
renWin = vtk.vtkRenderWindow()
renWin.AddRenderer(renderer)
iren = vtk.vtkRenderWindowInteractor()
iren.SetRenderWindow(renWin)

renderer.AddActor(actor)
renderer.SetBackground(0, 0, 0) 

renWin.Render()
iren.Start()
J.Ball
  • 13
  • 4
  • How about computing a linear regression over the point cloud and then transforming the resulting plane into the XY-plane (using some assumptions)? – normanius Oct 09 '18 at 08:53
  • Thank you for the advice, I will give this a try. I am still fairly new to working with 3D point clouds and VTK (about 2 Weeks now). Would the best way to achieve your suggestion be to convert the data to a numpy array and then compute the linear regression? – J.Ball Oct 09 '18 at 09:41
  • There are several ways to compute a reference plane through a set of points. Are the points lying in a (perfect) plane, or result these points from noisy measurements of a (non-planar) surface? – normanius Oct 09 '18 at 10:32
  • Non-planar, there are a few noisy measurements below the main cloud. – J.Ball Oct 09 '18 at 10:53

1 Answers1

0

I once solved a similar problem. Find below some code that I used back then. It uses two functions fitPlane and findTransformFromVectors that you could replace with your own implementations.

Note that there are many ways to fit a plane through a set of points. This SO post discusses compares scipy.optimize.minimize with scipy.linalg.lstsq. In another SO post, the use of PCA or RANSAC and other methods are suggested. You probably want to use methods provided by sklearn, numpy or other modules. My solution simply (and non-robustly) computes ordinary least squares regression.

import vtk
import numpy as np
# Convert vtk to numpy arrays
from vtk.util.numpy_support import vtk_to_numpy as vtk2np

# Create a random point cloud.
center = [3.0, 2.0, 1.0]
source = vtk.vtkPointSource()
source.SetCenter(center)
source.SetNumberOfPoints(50)
source.SetRadius(1.)
source.Update()
source = source.GetOutput()
# Extract the points from the point cloud.
points = vtk2np(source.GetPoints().GetData())
points = points.transpose()

# Fit a plane. nRegression contains the normal vector of the
# regression surface.
nRegression = fitPlane(points)
# Compute a transform that maps the source center to the origin and
# plane normal to the z-axis.
trafo = findTransformFromVectors(originFrom=center,
                                 axisFrom=nRegression.transpose(),
                                 originTo=(0,0,0),
                                 axisTo=(0.,0.,1.))

# Apply transform to source.
sourceTransformed = vtk.vtkTransformFilter()
sourceTransformed.SetInputData(source)
sourceTransformed.SetTransform(trafo)
sourceTransformed.Update()

# Visualize output...

Here my implementations of fitPlane and findTransformFromVectors:

# The following code has been written by normanius under the CC BY-SA 4.0 
# license.
# License:    https://creativecommons.org/licenses/by-sa/4.0/
# Author:     normanius: https://stackoverflow.com/users/3388962/normanius
# Date:       October 2018
# Reference:  https://stackoverflow.com/questions/52716438

def fitPlane(X, tolerance=1e-10):
    '''
    Estimate the plane normal by means of ordinary least dsquares.
    Requirement: points X span the full column rank. If the points lie in a
    perfect plane, the regression problem is ill-conditioned!

    Formulas:
        a = (XX^T)^(-1)*X*z
    Surface normal:
        n = [a[0], a[1], -1]
        n = n/norm(n)
    Plane intercept:
        c = a[2]/norm(n)

    NOTE: The condition number for the pseudo-inverse improves if the
          formulation is changed to homogenous notation.
    Formulas (homogenous):
        a = (XX^T)^(-1)*[1,1,1]^T
        n = a[:-1]
        n = n/norm(n)
        c = a[-1]/norm(n)

    Arguments:
        X:          A matrix with n rows and 3 columns
        tolerance:  Minimal condition number accepted. If the condition
                    number is lower, the algorithm returns None.

    Returns:
        If the computation was successful, a numpy array of length three is
        returned that represents the estimated plane normal. On failure,
        None is returned.
    '''
    X = np.asarray(X)
    d,N = X.shape
    X = np.vstack([X,np.ones([1,N])])
    z = np.ones([d+1,1])
    XXT = np.dot(X, np.transpose(X)) # XXT=X*X^T
    if np.linalg.det(XXT) < 1e-10:
        # The test covers the case where n<3
        return None
    n = np.dot(np.linalg.inv(XXT), z)
    intercept = n[-1]
    n = n[:-1]
    scale = np.linalg.norm(n)
    n /= scale
    intercept /= scale
    return n

def findTransformFromVectors(originFrom=None, axisFrom=None,
                             originTo=None, axisTo=None,
                             origin=None,
                             scale=1):
    '''
    Compute a transformation that maps originFrom and axisFrom to originTo
    and axisTo respectively. If scale is set to 'auto', the scale will be
    determined such that the axes will also match in length:
        scale = norm(axisTo)/norm(axisFrom)

    Arguments:  originFrom:     sequences with 3 elements, or None
                axisFrom:       sequences with 3 elements, or None
                originTo:       sequences with 3 elements, or None
                axisTo:         sequences with 3 elements, or None
                origin:         sequences with 3 elements, or None,
                                overrides originFrom and originTo if set
                scale:          - scalar (isotropic scaling)
                                - sequence with 3 elements (anisotropic scaling),
                                - 'auto' (sets scale such that input axes match
                                  in length after transforming axisFrom)
                                - None (no scaling)

    Align two axes alone, assuming that we sit on (0,0,0)
        findTransformFromVectors(axisFrom=a0, axisTo=a1)
    Align two axes in one point (all calls are equivalent):
        findTransformFromVectors(origin=o, axisFrom=a0, axisTo=a1)
        findTransformFromVectors(originFrom=o, axisFrom=a0, axisTo=a1)
        findTransformFromVectors(axisFrom=a0, originTo=o, axisTo=a1)
    Move between two points:
        findTransformFromVectors(orgin=o0, originTo=o1)
    Move from one position to the other and align axes:
        findTransformFromVectors(orgin=o0, axisFrom=a0, originTo=o1, axisTo=a1)
    '''
    # Prelude with trickle-down logic.
    # Infer the origins if an information is not set.
    if origin is not None:
        # Check for ambiguous input.
        assert(originFrom is None and originTo is None)
        originFrom = origin
        originTo = origin
    if originFrom is None:
        originFrom = originTo
    if originTo is None:
        originTo = originFrom
    if originTo is None:
        # We arrive here only if no origin information was set.
        originTo = [0.,0.,0.]
        originFrom = [0.,0.,0.]
    originFrom = np.asarray(originFrom)
    originTo = np.asarray(originTo)
    # Check if any rotation will be involved.
    axisFrom = np.asarray(axisFrom)
    axisTo = np.asarray(axisTo)
    axisFromL2 = np.linalg.norm(axisFrom)
    axisToL2 = np.linalg.norm(axisTo)
    if axisFrom is None or axisTo is None or axisFromL2==0 or axisToL2==0:
        rotate = False
    else:
        rotate = not np.array_equal(axisFrom, axisTo)
    # Scale.
    if scale is None:
        scale = 1.
    if scale == 'auto':
        scale = axisToL2/axisFromL2 if axisFromL2!=0. else 1.
    if np.isscalar(scale):
        scale = scale*np.ones(3)
    if rotate:
        rAxis = np.cross(axisFrom.ravel(), axisTo.ravel())  # rotation axis
        angle = np.dot(axisFrom, axisTo) / axisFromL2 / axisToL2
        angle = np.arccos(angle)

    # Here we finally compute the transform.
    trafo = vtk.vtkTransform()
    trafo.Translate(originTo)
    if rotate:
        trafo.RotateWXYZ(angle / np.pi * 180, rAxis[0], rAxis[1], rAxis[2])
    trafo.Scale(scale[0],scale[1],scale[2])
    trafo.Translate(-originFrom)
    return trafo
normanius
  • 8,629
  • 7
  • 53
  • 83
  • @downvoters: I'd be glad to know how I can improve my answer. – normanius Oct 09 '18 at 12:44
  • Sorry it's taken a while to get back to you on this. Your method has worked great for me, thank you. There are a few minor undulations in the resulting cloud but it is a massive improvement on my original cloud. – J.Ball Oct 23 '18 at 12:30