图形拟合(三):椭圆拟合

图形拟合(三):椭圆拟合

算法背景

参考论文实现《Direct least square fitting of ellipses》

OpenCV中也有对应实现,可参考文档:

https://docs.opencv.org/3.4/d3/dc0/group__imgproc__shape.html#gaf259efaad93098103d6c27b9e4900ffa

JavaScript版本实现

JavaScript版本仅实现了 fitEllipse中 的 fitEllipseNoDirect 部分。

class Point {
  x : number
  y : number
  constructor(_x : number, _y : number) {
    this.x = _x
    this.y = _y
  }
}

class MiniOpenCV {

  static getOfs(i: number, eps: number) {
    return new Point(((i & 1)*2 - 1)*eps, ((i & 2) - 1)*eps);
  }

  static fitEllipseNoDirect(_points: Point[]) {
    let points = _points;
    let i, n = points.length;
    if (n < 5) {
      console.error("There should be at least 5 points to fit the ellipse");
      return
    }
    const FLT_EPSILON = 1.19209e-07
    let box = {
      center: {
        x: 0,
        y: 0,
      },
      size: {
        width: 0,
        height: 0
      },
      angle: 0
    }

    let c = new Point(0,0);
    let t;
    let gfp = new Array(5).fill(0)
    let rp = new Array(5).fill(0)
    let vd = new Array(25).fill(0)
    let wd = new Array(5).fill(0)
    const min_eps = 1e-8

    let _AD = new Array(n * 12 + n).fill(0);
    const Ad = 0
    const ud = Ad + 5 * n
    const bd = ud + 5 * n
    const ptsf_copy = bd + n;

    let A = null
    let b = null
    let x = null
    let u = null
    let vt = null
    let w = null

    {
      const ptsf = points;
      for (i = 0; i < n; i++) {
        let p = ptsf[i]
        _AD[ptsf_copy + i] = p;
        c.x += p.x;
        c.y += p.y;
      }
    }
    c.x /= n;
    c.y /= n;
    
    let s = 0;
    for (i = 0; i < n; i++) {
      let p = JSON.parse(JSON.stringify(_AD[ptsf_copy + i]));
      p.x -= c.x;
      p.y -= c.y;
      s += Math.abs(p.x) + Math.abs(p.y);
    }
    let scale = 100 / (s > FLT_EPSILON ? s : FLT_EPSILON);

    for (i = 0; i < n; i++) {
      let p = JSON.parse(JSON.stringify(_AD[ptsf_copy + i]));
      p.x -= c.x;
      p.y -= c.y;
      let px = p.x * scale;
      let py = p.y * scale;

      _AD[bd + i] = 10000; // 1.0?
      _AD[Ad + i*5] = -px * px; // A - C signs inverted as proposed by APP
      _AD[Ad + i*5 + 1] = -py * py;
      _AD[Ad + i*5 + 2] = -px * py;
      _AD[Ad + i*5 + 3] = px;
      _AD[Ad + i*5 + 4] = py;
    }

    A = Mat.matrix(n, 5, _AD)
    
    let { E, U, V } = Mat.svd(A)
    w = E
    u = U
    vt = Mat.transpose(V)

    let count = 0;
    Mat.forEach(Mat.diag(w), (item: any) => {
      wd[count++] = item;
    })

    if (wd[0] * FLT_EPSILON > wd[4]) {
      let eps = (s / (n * 2) * 1e-3);
      for (i = 0; i < n; i++) {
        let err = MiniOpenCV.getOfs(i, eps);
        let p = _AD[ptsf_copy + i];
        p.x += err.x;
        p.y += err.y;
      }

      for (i = 0; i < n; i++) {
        let p = JSON.parse(JSON.stringify(_AD[ptsf_copy + i]));
        p.x -= c.x;
        p.y -= c.y;
        let px = p.x * scale;
        let py = p.y * scale;
        _AD[bd + i] = 10000.0; // 1.0?
        _AD[Ad + i*5] = -px * px; // A - C signs inverted as proposed by APP
        _AD[Ad + i*5 + 1] = -py * py;
        _AD[Ad + i*5 + 2] = -px * py;
        _AD[Ad + i*5 + 3] = px;
        _AD[Ad + i*5 + 4] = py;
      }

      A = Mat.matrix(n, 5, _AD)
    
      let { E, U, V } = Mat.svd(A)
      w = E
      u = U
      vt = Mat.transpose(V)
    }

    b = Mat.matrix(n, 1, _AD.slice(bd))
    x = Mat.backSubst(w, u, vt, b);

    count = 0;
    Mat.forEach(x, (item: any) => {
      gfp[count++] = item;
    })

    // now use general-form parameters A - E to find the ellipse center:
    // differentiate general form wrt x/y to get two equations for cx and cy
    _AD[Ad + 0] = 2 * gfp[0];
    _AD[Ad + 1] = _AD[Ad + 2] = gfp[2];
    _AD[Ad + 3] = 2 * gfp[1];
    _AD[bd + 0] = gfp[3];
    _AD[bd + 1] = gfp[4];
    A = Mat.matrix(2, 2, _AD);
    b = Mat.matrix(2, 1, _AD.slice(bd));
    x = Mat.solve(A, b);

    count = 0;
    Mat.forEach(x, (item: any) => {
      rp[count++] = item;
    })

    // re-fit for parameters A - C with those center coordinates
    for (i = 0; i < n; i++) {
      let p = JSON.parse(JSON.stringify(_AD[ptsf_copy + i]));
      p.x -= c.x;
      p.y -= c.y;
      let px = p.x*scale;
      let py = p.y*scale;
      _AD[bd + i] = 1.0;
      _AD[Ad + i * 3] = (px - rp[0]) * (px - rp[0]);
      _AD[Ad + i * 3 + 1] = (py - rp[1]) * (py - rp[1]);
      _AD[Ad + i * 3 + 2] = (px - rp[0]) * (py - rp[1]);
    }
    A = Mat.matrix( n, 3, _AD);
    b = Mat.matrix( n, 1, _AD.slice(bd));
    x = Mat.solve(A, b);

    count = 0;
    Mat.forEach(x, (item: any) => {
      gfp[count++] = item;
    })

    // store angle and radii
    rp[4] = -0.5 * Math.atan2(gfp[2], gfp[1] - gfp[0]); // convert from APP angle usage
    if( Math.abs(gfp[2]) > min_eps )
      t = gfp[2]/Math.sin(-2.0 * rp[4]);
    else // ellipse is rotated by an integer multiple of pi/2
      t = gfp[1] - gfp[0];
    rp[2] = Math.abs(gfp[0] + gfp[1] - t);
    if( rp[2] > min_eps )
      rp[2] = Math.sqrt(2.0 / rp[2]);
    rp[3] = Math.abs(gfp[0] + gfp[1] + t);
    if( rp[3] > min_eps )
      rp[3] = Math.sqrt(2.0 / rp[3]);

    box.center.x = (rp[0] / scale) + c.x;
    box.center.y = (rp[1] / scale) + c.y;
    box.size.width = (rp[2] * 2 / scale);
    box.size.height = (rp[3] * 2 / scale);
    if (box.size.width > box.size.height) {
      let tmp = box.size.width;
      box.size.width = box.size.height;
      box.size.height = tmp;
      box.angle = (90 + rp[4] * 180 / Math.PI);
    }
    if (box.angle < -180)
      box.angle += 360;
    if (box.angle > 360)
      box.angle -= 360;

    return box;
  }

  static fitEllipse(points: Point[]) {
    return MiniOpenCV.fitEllipseNoDirect(points);
  }
}