簡體   English   中英

如何在scala中編寫適合的圓

[英]How to program a circle fit in scala

我想在 Scala 中為給定的 2D 點擬合一個圓。

Apache commons math 在 java 中有一個例子,我試圖將其轉換為 scala(沒有成功,因為我的 Java 知識幾乎不存在)。

我從“http://commons.apache.org/proper/commons-math/userguide/leastsquares.html”中獲取了示例代碼(見頁尾),我試圖將其轉換為 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())
    
  }

這不會產生編譯錯誤,但會崩潰:

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)

我想問題出在函數 distancesToCurrentCenter 的定義中。 我什至不知道這個 MultivariateJacobianFunction 是否應該是一個真正的函數或對象或什么。

在對代碼進行了長時間的擺弄之后,我讓它運行了

在我的 build.sbt 文件中將 apache-commons-math3 從 3.3 版更新到 3.6.1 版后,NullPointerException 消失了。 不知道我是否忘記了一個參數,如果它是一個錯誤。 在 apache-commons-math 網站上的示例中也有 2 個錯誤:他們有兩次 .getX 運算符,而本應是 .getY。

所以這是一個已知半徑的圓擬合的運行示例:

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")
}

在 Scala 版本 2.12.6 上測試,使用 sbt 版本 1.2.8 編譯

有人知道如何在沒有固定半徑的情況下做到這一點嗎?

在對圓擬合進行了一些研究之后,我在論文中找到了一個很棒的算法:H. Al-Sharadqah 和 N. Chernov 的“圓擬合算法的誤差分析”(可在此處獲得: http ://people.cas.uab.edu /~mosya/cl/ ) 我在 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)
  }
}

我保留了論文中的所有名稱(參見第 4 章和第 8 章並尋找 Hyperfit 算法),並嘗試限制矩陣運算。

這仍然不是我需要的,因為這種算法(代數擬合)在擬合部分圓(弧)和大圓方面存在已知問題。

用我的數據,曾經有過它吐出完全錯誤結果的情況,我發現我的特征值為-0.1……這個值的特征向量產生了正確的結果,但由於負特征值。 所以這個並不總是穩定的(就像許多其他圓擬合算法一樣)

但多么好的算法啊!!! 對我來說看起來有點像黑魔法。

如果有人不需要很高的精度和很高的速度(並且從一個完整的圓圈中獲得的數據並不大),這將是我的選擇。

接下來我將嘗試從我上面提到的同一頁面實現 Levenberg Marquardt 算法。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM