简体   繁体   中英

How to program a circle fit in scala

I want to fit a circle to given 2D points in Scala.

Apache commons math has an example for this in java, which I am trying to translate to scala (without success, because my knowledge of Java is almost non existent).

I took the example code from "http://commons.apache.org/proper/commons-math/userguide/leastsquares.html", (see end of page) wich I tried to translate into scala:

  import org.apache.commons.math3.linear._
  import org.apache.commons.math3.fitting._
  import org.apache.commons.math3.fitting.leastsquares._
  import org.apache.commons.math3.fitting.leastsquares.LeastSquaresOptimizer._
  import org.apache.commons.math3._
  import org.apache.commons.math3.geometry.euclidean.twod.Vector2D
  import org.apache.commons.math3.util.Pair
  import org.apache.commons.math3.fitting.leastsquares.LeastSquaresOptimizer.Optimum

  def circleFitting: Unit = {
    val radius: Double = 70.0

    val observedPoints = Array(new Vector2D(30.0D, 68.0D), new Vector2D(50.0D, -6.0D), new Vector2D(110.0D, -20.0D), new Vector2D(35.0D, 15.0D), new Vector2D(45.0D, 97.0D))

    // the model function components are the distances to current estimated center,
    // they should be as close as possible to the specified radius

    val distancesToCurrentCenter = new MultivariateJacobianFunction() {
      //def value(point: RealVector): (RealVector, RealMatrix) = {
      def value(point: RealVector): Pair[RealVector, RealMatrix] = {

        val center = new Vector2D(point.getEntry(0), point.getEntry(1))

        val value: RealVector = new ArrayRealVector(observedPoints.length)
        val jacobian: RealMatrix = new Array2DRowRealMatrix(observedPoints.length, 2)

        for (i <- 0 to observedPoints.length) {
          var o = observedPoints(i)
          var modelI: Double = Vector2D.distance(o, center)
          value.setEntry(i, modelI)
          // derivative with respect to p0 = x center
          jacobian.setEntry(i, 0, (center.getX() - o.getX()) / modelI)
          // derivative with respect to p1 = y center
          jacobian.setEntry(i, 1, (center.getX() - o.getX()) / modelI)
        }
        new Pair(value, jacobian)
      }
    }

    // the target is to have all points at the specified radius from the center
    val prescribedDistances = Array.fill[Double](observedPoints.length)(radius)
    // least squares problem to solve : modeled radius should be close to target radius
    
    val problem:LeastSquaresProblem = new LeastSquaresBuilder().start(Array(100.0D, 50.0D)).model(distancesToCurrentCenter).target(prescribedDistances).maxEvaluations(1000).maxIterations(1000).build()
    
    val optimum:Optimum = new LevenbergMarquardtOptimizer().optimize(problem) //LeastSquaresOptimizer.Optimum
    val fittedCenter: Vector2D = new Vector2D(optimum.getPoint().getEntry(0), optimum.getPoint().getEntry(1))
    println("circle fitting wurde aufgerufen!")
    println("CIRCLEFITTING: fitted center: " + fittedCenter.getX() + " " + fittedCenter.getY())
    println("CIRCLEFITTING: RMS: " + optimum.getRMS())
    println("CIRCLEFITTING: evaluations: " + optimum.getEvaluations())
    println("CIRCLEFITTING: iterations: " + optimum.getIterations())
    
  }

This gives no compile errors, but crashes with:

Exception in thread "main" java.lang.NullPointerException
    at org.apache.commons.math3.linear.EigenDecomposition.<init>(EigenDecomposition.java:119)
    at org.apache.commons.math3.fitting.leastsquares.LeastSquaresFactory.squareRoot(LeastSquaresFactory.java:245)
    at org.apache.commons.math3.fitting.leastsquares.LeastSquaresFactory.weightMatrix(LeastSquaresFactory.java:155)
    at org.apache.commons.math3.fitting.leastsquares.LeastSquaresFactory.create(LeastSquaresFactory.java:95)
    at org.apache.commons.math3.fitting.leastsquares.LeastSquaresBuilder.build(LeastSquaresBuilder.java:59)
    at twoDhotScan.FittingFunctions$.circleFitting(FittingFunctions.scala:49)
    at twoDhotScan.Main$.delayedEndpoint$twoDhotScan$Main$1(hotScan.scala:14)
    at twoDhotScan.Main$delayedInit$body.apply(hotScan.scala:11)
    at scala.Function0.apply$mcV$sp(Function0.scala:34)
    at scala.Function0.apply$mcV$sp$(Function0.scala:34)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    at scala.App.$anonfun$main$1$adapted(App.scala:76)
    at scala.collection.immutable.List.foreach(List.scala:389)
    at scala.App.main(App.scala:76)
    at scala.App.main$(App.scala:74)
    at twoDhotScan.Main$.main(hotScan.scala:11)
    at twoDhotScan.Main.main(hotScan.scala)

I guess the problem is somewhere in the definition of the function distancesToCurrentCenter. I don't even know if this MultivariateJacobianFunction is supposed to be a real function or an object or what ever.

After some long fiddeling with the code, I got it running

The NullPointerException was gone after I updated apache-commons-math3 from version 3.3 to version 3.6.1 in my build.sbt file. Don't know if I forgot a paramater of if it was a bug. There were also 2 bugs in the example on the apache-commons-math website: They had two times a .getX operator where should have been an .getY.

So here is a running example for a circle fit with known radius:

import org.apache.commons.math3.analysis.{ MultivariateVectorFunction, MultivariateMatrixFunction }
import org.apache.commons.math3.fitting.leastsquares.LeastSquaresOptimizer.Optimum
import org.apache.commons.math3.fitting.leastsquares.{ MultivariateJacobianFunction, LeastSquaresProblem, LeastSquaresBuilder, LevenbergMarquardtOptimizer }
import org.apache.commons.math3.geometry.euclidean.twod.Vector2D
import org.apache.commons.math3.linear.{ Array2DRowRealMatrix, RealMatrix, RealVector, ArrayRealVector }

object Main extends App {
  val radius: Double = 20.0
  val pointsList: List[(Double, Double)] = List(
    (18.36921795, 10.71416674),
    (0.21196357, -22.46528791),
    (-4.153845171, -14.75588526),
    (3.784114125, -25.55910336),
    (31.32998899, 2.546924253),
    (34.61542186, -12.90323269),
    (19.30193011, -28.53185596),
    (16.05620863, 10.97209111),
    (31.67011956, -20.05020878),
    (19.91175561, -28.38748712))
/*******************************************************************************
 ***** Random values on a circle with centerX=15, centerY=-9 and radius 20 *****
 *******************************************************************************/

  val observedPoints: Array[Vector2D] = (pointsList map { case (x, y) => new Vector2D(x, y) }).toArray

  val vectorFunktion: MultivariateVectorFunction = new MultivariateVectorFunction {
    def value(variables: Array[Double]): Array[Double] = {
      val center = new Vector2D(variables(0), variables(1))
      observedPoints map { p: Vector2D => Vector2D.distance(p, center) }
    }
  }

  val matrixFunction = new MultivariateMatrixFunction {
    def value(variables: Array[Double]): Array[Array[Double]] = {
      val center = new Vector2D(variables(0), variables(1))
      (observedPoints map { p: Vector2D => Array((center.getX - p.getX) / Vector2D.distance(p, center), (center.getY - p.getY) / Vector2D.distance(p, center)) })
    }
  }

  // the target is to have all points at the specified radius from the center
  val prescribedDistances = Array.fill[Double](observedPoints.length)(radius)
  // least squares problem to solve : modeled radius should be close to target radius
  val problem = new LeastSquaresBuilder().start(Array(100.0D, 50.0D)).model(vectorFunktion, matrixFunction).target(prescribedDistances).maxEvaluations(25).maxIterations(25).build
  val optimum: Optimum = new LevenbergMarquardtOptimizer().optimize(problem)
  val fittedCenter: Vector2D = new Vector2D(optimum.getPoint.getEntry(0), optimum.getPoint.getEntry(1))

  println("Ergebnisse des LeastSquareBuilder:")
  println("CIRCLEFITTING: fitted center: " + fittedCenter.getX + " " + fittedCenter.getY)
  println("CIRCLEFITTING: RMS: " + optimum.getRMS)
  println("CIRCLEFITTING: evaluations: " + optimum.getEvaluations)
  println("CIRCLEFITTING: iterations: " + optimum.getIterations + "\n")
}

Tested on Scala version 2.12.6, compiled with sbt version 1.2.8

Does anabody know how to do this without a fixed radius?

After some reasearch on circle fitting I've found a wonderful algorith in the paper: "Error alalysis for circle fitting algorithms" by H. Al-Sharadqah and N. Chernov (available here: http://people.cas.uab.edu/~mosya/cl/ ) I implemented it in scala:

import org.apache.commons.math3.linear.{ Array2DRowRealMatrix, RealMatrix, RealVector, LUDecomposition, EigenDecomposition }

object circleFitFunction {
  def circleFit(dataXY: List[(Double, Double)]) = {

    def square(x: Double): Double = x * x
    def multiply(pair: (Double, Double)): Double = pair._1 * pair._2

    val n: Int = dataXY.length
    val (xi, yi) = dataXY.unzip
    //val S: Double = math.sqrt(((xi map square) ++ yi map square).sum / n)
    val zi: List[Double] = dataXY map { case (x, y) => x * x + y * y }
    val x: Double = xi.sum / n
    val y: Double = yi.sum / n
    val z: Double = ((xi map square) ++ (yi map square)).sum / n
    val zz: Double = (zi map square).sum / n
    val xx: Double = (xi map square).sum / n
    val yy: Double = (yi map square).sum / n
    val xy: Double = ((xi zip yi) map multiply).sum / n
    val zx: Double = ((zi zip xi) map multiply).sum / n
    val zy: Double = ((zi zip yi) map multiply).sum / n

    val N: RealMatrix = new Array2DRowRealMatrix(Array(
      Array(8 * z, 4 * x, 4 * y, 2),
      Array(4 * x, 1, 0, 0),
      Array(4 * y, 0, 1, 0),
      Array(2.0D, 0, 0, 0)))

    val M: RealMatrix = new Array2DRowRealMatrix(Array(
      Array(zz, zx, zy, z),
      Array(zx, xx, xy, x),
      Array(zy, xy, yy, y),
      Array(z, x, y, 1.0D)))

    val Ninverse = new LUDecomposition(N).getSolver().getInverse()
    val eigenValueProblem = new EigenDecomposition(Ninverse.multiply(M))
    // Get all eigenvalues
    // As we need only the smallest positive eigenvalue, all negative eigenvalues are replaced by Double.MaxValue
    val eigenvalues: Array[Double] = eigenValueProblem.getRealEigenvalues() map (lambda => if (lambda < 0) Double.MaxValue else lambda)

    // Now get the index of the smallest positive eigenvalue, to get the associated eigenvector
    val i: Int = eigenvalues.zipWithIndex.min._2
    val eigenvector: RealVector = eigenValueProblem.getEigenvector(3)

    val A = eigenvector.getEntry(0)
    val B = eigenvector.getEntry(1)
    val C = eigenvector.getEntry(2)
    val D = eigenvector.getEntry(3)

    val centerX: Double = -B / (2 * A)
    val centerY: Double = -C / (2 * A)
    val Radius: Double = math.sqrt((B * B + C * C - 4 * A * D) / (4 * A * A))
    val RMS: Double = (dataXY map { case (x, y) => (Radius - math.sqrt((x - centerX) * (x - centerX) + (y - centerY) * (y - centerY))) } map square).sum / n
    (centerX, centerY, Radius, RMS)
  }
}

I kept all the Names form the paper (see Chaper 4 and 8 and look for the Hyperfit-Algorithm) and I tried to limit the Matrix operations.

It's still not what I need, cause this sort of algorithm (algebraic fit) has known issues with fitting partially circles (arcs) and maybe big circles.

With my data, I had once the situation that it spit out completly wrong results, and I found out that I had an Eigenvalue of -0.1... The Eigenvector of this Value produced the right result, but it was sorted out because of the negative Eigenvalue. So this one is not always stable (as so many other circle fitting algorithms)

But what a nice Algorithm!!! Looks a bit like dark magic to me.

If someone needs not to much precision and a lot of speed (and has data from a full circle not to big) this would be my choice.

Next thing I will try is to implement a Levenberg Marquardt Algorithm form the same page I mentioned above.

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