从用户的触摸画一个完美的圆

我有这个实践项目,允许用户在屏幕上画,因为他们触摸他们的手指。很简单的应用程序,我做了一个练习回来。 我的小表弟擅自用我的 iPad 在这个应用程序上画东西(孩子们的画: 圆圈,线条,等等,想到什么就画什么)。 然后他开始画圆,然后他要求我画一个“好的圆”(根据我的理解: 让画的圆完美的圆,正如我们所知道的 不管我们用手指在屏幕上画什么东西有多稳定,圆永远不会像圆应该的那样圆)。

所以我的问题是,在代码中有没有什么方法可以让我们首先检测到用户画的一条线,这条线形成了一个圆,然后通过在屏幕上使它完美地圆起来,生成大致相同大小的圆。制作一条不那么直的直线是我会知道如何做的事情,但是对于圆,我不太知道如何用 Quartz 或其他方法来做。

我的理由是,线的起点和终点必须在用户举起手指来证明他实际上是在试图画一个圆之后相互接触或交叉。

8336 次浏览

我不是形状识别专家,但我可以这样解决这个问题。

首先,在徒手显示用户路径时,秘密地随时间累积一个点(x,y)示例列表。您可以从拖动事件中获取这两个事实,将它们封装到一个简单的模型对象中,然后将它们堆放到一个可变的数组中。

你可能需要相当频繁地采集样本,比如每0.1秒采集一次。另一种可能性是频繁地启动 真的,也许是每0.05秒,观察用户拖动多长时间; 如果他们拖动的时间超过一定数量,然后降低采样频率(并丢弃任何可能错过的样本)到大约0.2秒。

(不要把我的数字当成福音,因为我只是把它们从帽子里抽出来。尝试并找到更好的价值观。)

第二,分析样本。

你需要推导出两个事实。首先,形状的中心,它(IIRC)应该是所有点的平均值。其次,每个样本从中心的平均半径。

如果像@user1118321猜测的那样,您希望支持多边形,那么分析的其余部分包括做出决定: 用户是想画一个圆还是一个多边形。您可以从一个多边形开始查看这些样本,以便进行确定。

你可以使用以下几个标准:

  • 时间: 如果用户在某些点徘徊的时间比其他点长(如果样本间隔不变,那么样本在空间中会显示为相邻的连续样本集) ,那么这些点可能是角点。您应该使您的转角阈值小,这样用户就可以无意识地这样做,而不是必须故意在每个转角处暂停。
  • 角度: 一个圆从一个样品到下一个样品的角度大致相同。一个多边形将有几个角由直线段连接; 角是角。对于一个正多边形(圆到一个不规则多边形的椭圆) ,角角应该大致相同; 一个不规则多边形将有不同的角角。
  • 间隔: 一个规则的多边形的角将是相等的空间间隔内的角度维度,半径将是恒定的。不规则多边形将有不规则的角间隔和/或半径不恒定。

第三步,也是最后一步是创建形状,以先前确定的中心点为中心,使用先前确定的半径。

不能保证我上面所说的任何东西都会起作用或有效,但我希望它至少能让你走上正确的道路ーー如果任何比我更了解形状识别(这是一个非常低的门槛)的人看到这一点,请随意发表评论或自己的答案。

我有一个非常好的运气与正确训练的1美元识别器(http://depts.washington.edu/aimgroup/proj/dollar/)。我用它来画圆圈,直线,三角形和正方形。

那是很久以前的事了,在 UIGestureIdentiizer 之前,但是我认为创建合适的 UIGestureIdentiizer 子类应该很容易。

一旦确定用户完成了从哪里开始绘制的形状,就可以取得他们绘制的坐标的样本,并尝试将它们拟合成一个圆。

这里有一个 MATLAB 的解决方案: Http://www.mathworks.com.au/matlabcentral/fileexchange/15060-fitcircle-m

这篇论文是根据沃尔特 · 甘德、吉恩 · H · 格鲁布和罗尔夫 · 斯特雷贝尔的论文 圆与椭圆的最小二乘拟合写成的: Http://www.emis.de/journals/bbms/bulletin/sup962/gander.pdf

来自 NZ 坎特伯雷大学的 Ian Coope 博士发表了一篇论文,摘要如下:

确定一组点的最佳拟合圆的问题 在平面上(或者对 n 维的明显推广)是很容易的 表述为一个非线性总最小二乘问题,这个问题可能是 使用高斯-牛顿最小化算法求解 直截了当的方法被证明是低效的和极端的 对异常值的存在很敏感 使问题简化为一般最小平方法问题 这是微不足道的解决办法。推荐的方法显示有 对异常值的敏感程度远低于 非线性最小二乘法。

Http://link.springer.com/article/10.1007%2fbf00939613

MATLAB 文件既可以计算非线性最小二乘问题,也可以计算线性最小二乘问题。

一种经典的检测形状的计算机视觉技术是霍夫变换。Hough 变换的优点之一就是它对部分数据、不完全数据和噪声的容忍度很高。用 Hough 画一个圈: Http://en.wikipedia.org/wiki/hough_transform#circle_detection_process

考虑到你的圆是手绘的,我认为霍夫变换可能是一个很好的匹配。

这里有一个“简化”的解释,我为它没有那么简单而道歉。大部分是我多年前做的一个学校作业。

霍夫变换是一个投票方案。将分配一个二维整数数组,并将所有元素设置为零。每个元素对应于被分析图像中的一个像素。这个数组被称为累加器数组,因为每个元素都会积累信息、选票,表明像素可能位于圆或弧的原点。

将梯度算子边缘检测器应用于图像,并记录边缘像素或边缘。边缘是一个像素,它具有不同的强度或颜色相对于它的邻居。差别的程度称为梯度大小。对于每个足够大的边缘,应用一个投票方案,该方案将增加累加器阵列的元素。正在增加的元素(投票赞成)对应于正在考虑的通过边缘的圆的可能起源。期望的结果是,如果一个弧存在,那么真正的起源将比虚假的起源获得更多的选票。

请注意,为了投票而访问的累加器阵列的元素在考虑的边形周围形成一个圆。计算要投票的 x,y 坐标与计算正在绘制的圆的 x,y 坐标相同。

在您的手绘图像中,您可以直接使用设置(着色)像素,而不是计算边缘。

现在,由于像素位置不完美,你不一定会得到一个单一的累加器数组元素与最大数量的投票。你可能会得到一个相邻数组元素的集合,这些元素包含一系列的投票,一个集群。这个星团的重心可能为原点提供一个很好的近似值。

注意,对于半径 R 的不同值,可能需要运行 Hough 变换。产生更密集的投票集群的那一个是“更好的”匹配。

有各种各样的技术可以用来减少虚假出身的选票。例如,使用边的一个优点是,它们不仅有一个量级,而且还有一个方向。在投票时,我们只需要投票选择合适的方向。获得选票的地点将形成一个弧线,而不是一个完整的圆。

举个例子。我们从一个半径为1的圆和一个初始化的累加器数组开始。由于每个像素被认为是潜在的起源投票。真正的起源得到最多的选票,在这种情况下是四票。

.  empty pixel
X  drawn pixel
*  drawn pixel currently being considered


. . . . .   0 0 0 0 0
. . X . .   0 0 0 0 0
. X . X .   0 0 0 0 0
. . X . .   0 0 0 0 0
. . . . .   0 0 0 0 0


. . . . .   0 0 0 0 0
. . X . .   0 1 0 0 0
. * . X .   1 0 1 0 0
. . X . .   0 1 0 0 0
. . . . .   0 0 0 0 0


. . . . .   0 0 0 0 0
. . X . .   0 1 0 0 0
. X . X .   1 0 2 0 0
. . * . .   0 2 0 1 0
. . . . .   0 0 1 0 0


. . . . .   0 0 0 0 0
. . X . .   0 1 0 1 0
. X . * .   1 0 3 0 1
. . X . .   0 2 0 2 0
. . . . .   0 0 1 0 0


. . . . .   0 0 1 0 0
. . * . .   0 2 0 2 0
. X . X .   1 0 4 0 1
. . X . .   0 2 0 2 0
. . . . .   0 0 1 0 0

下面是一个相当简单的使用方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

假设这个矩阵网格:

 A B C D E F G H
1      X X
2    X     X
3  X         X
4  X         X
5    X     X
6      X X
7
8

在“ X”位置放置一些 UIView 并测试它们是否被击中(按顺序)。如果它们都按顺序被击中,我认为让用户说“干得好,你画了一个圆”

听起来可以吗? (和简单)

还有一个办法。使用 UIView touch esBegan、 touch esMoved、 touch esEnded 并向数组中添加点。您将数组分成两半,并测试一个数组中的每个点与另一个数组中的对应点的直径是否与其他所有对的直径大致相同。

    NSMutableArray * pointStack;


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// Detect touch anywhere
UITouch *touch = [touches anyObject];




pointStack = [[NSMutableArray alloc]init];


CGPoint touchDownPoint = [touch locationInView:touch.view];




[pointStack addObject:touchDownPoint];


}




/**
*
*/
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{


UITouch* touch = [touches anyObject];
CGPoint touchDownPoint = [touch locationInView:touch.view];


[pointStack addObject:touchDownPoint];


}


/**
* So now you have an array of lots of points
* All you have to do is find what should be the diameter
* Then compare opposite points to see if the reach a similar diameter
*/
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
uint pointCount = [pointStack count];


//assume the circle was drawn a constant rate and the half way point will serve to calculate or diameter
CGPoint startPoint = [pointStack objectAtIndex:0];
CGPoint halfWayPoint = [pointStack objectAtIndex:floor(pointCount/2)];


float dx = startPoint.x - halfWayPoint.x;
float dy = startPoint.y - halfWayPoint.y;




float diameter = sqrt((dx*dx) + (dy*dy));


bool isCircle = YES;// try to prove false!


uint indexStep=10; // jump every 10 points, reduce to be more granular


// okay now compare matches
// e.g. compare indexes against their opposites and see if they have the same diameter
//
for (uint i=indexStep;i<floor(pointCount/2);i+=indexStep)
{


CGPoint testPointA = [pointStack objectAtIndex:i];
CGPoint testPointB = [pointStack objectAtIndex:floor(pointCount/2)+i];


dx = testPointA.x - testPointB.x;
dy = testPointA.y - testPointB.y;




float testDiameter = sqrt((dx*dx) + (dy*dy));


if(testDiameter>=(diameter-10) && testDiameter<=(diameter+10)) // +/- 10 ( or whatever degree of variance you want )
{
//all good
}
else
{
isCircle=NO;
}


}//end for loop


NSLog(@"iCircle=%i",isCircle);


}

听起来不错吧

有时候花点时间在重造轮子上是很有用的。您可能已经注意到有很多框架,但是在不引入所有复杂性的情况下实现一个简单但有用的解决方案并不难。(请不要误解我的意思,对于任何严肃的目的,最好使用一些成熟的和被证明是稳定的框架)。

我将首先介绍我的结果,然后解释其背后简单而直接的想法。

enter image description here

在我的实现中,您将看到没有必要分析每一个点并进行复杂的计算。这个想法是为了发现一些有价值的元信息。我将使用 切线作为一个例子:

enter image description here

让我们来确定一个简单而直接的模式,这是所选形状的典型模式:

enter image description here

因此,基于这种思想实现一种圆检测机制并不困难。参见下面的工作演示(对不起,我使用 Java 作为最快的方法来提供这个快速和有点脏的例子) :

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.HeadlessException;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;


public class CircleGestureDemo extends JFrame implements MouseListener, MouseMotionListener {


enum Type {
RIGHT_DOWN,
LEFT_DOWN,
LEFT_UP,
RIGHT_UP,
UNDEFINED
}


private static final Type[] circleShape = {
Type.RIGHT_DOWN,
Type.LEFT_DOWN,
Type.LEFT_UP,
Type.RIGHT_UP};


private boolean editing = false;
private Point[] bounds;
private Point last = new Point(0, 0);
private List<Point> points = new ArrayList<>();


public CircleGestureDemo() throws HeadlessException {
super("Detect Circle");


addMouseListener(this);
addMouseMotionListener(this);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);


setPreferredSize(new Dimension(800, 600));
pack();
}


@Override
public void paint(Graphics graphics) {
Dimension d = getSize();
Graphics2D g = (Graphics2D) graphics;


super.paint(g);


RenderingHints qualityHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHints(qualityHints);


g.setColor(Color.RED);
if (cD == 0) {
Point b = null;
for (Point e : points) {
if (null != b) {
g.drawLine(b.x, b.y, e.x, e.y);
}
b = e;
}
}else if (cD > 0){
g.setColor(Color.BLUE);
g.setStroke(new BasicStroke(3));
g.drawOval(cX, cY, cD, cD);
}else{
g.drawString("Uknown",30,50);
}
}




private Type getType(int dx, int dy) {
Type result = Type.UNDEFINED;


if (dx > 0 && dy < 0) {
result = Type.RIGHT_DOWN;
} else if (dx < 0 && dy < 0) {
result = Type.LEFT_DOWN;
} else if (dx < 0 && dy > 0) {
result = Type.LEFT_UP;
} else if (dx > 0 && dy > 0) {
result = Type.RIGHT_UP;
}


return result;
}


private boolean isCircle(List<Point> points) {
boolean result = false;
Type[] shape = circleShape;
Type[] detected = new Type[shape.length];
bounds = new Point[shape.length];


final int STEP = 5;


int index = 0;
Point current = points.get(0);
Type type = null;


for (int i = STEP; i < points.size(); i += STEP) {
Point next = points.get(i);
int dx = next.x - current.x;
int dy = -(next.y - current.y);


if(dx == 0 || dy == 0) {
continue;
}


Type newType = getType(dx, dy);
if(type == null || type != newType) {
if(newType != shape[index]) {
break;
}
bounds[index] = current;
detected[index++] = newType;
}
type = newType;
current = next;


if (index >= shape.length) {
result = true;
break;
}
}


return result;
}


@Override
public void mousePressed(MouseEvent e) {
cD = 0;
points.clear();
editing = true;
}


private int cX;
private int cY;
private int cD;


@Override
public void mouseReleased(MouseEvent e) {
editing = false;
if(points.size() > 0) {
if(isCircle(points)) {
cX = bounds[0].x + Math.abs((bounds[2].x - bounds[0].x)/2);
cY = bounds[0].y;
cD = bounds[2].y - bounds[0].y;
cX = cX - cD/2;


System.out.println("circle");
}else{
cD = -1;
System.out.println("unknown");
}
repaint();
}
}


@Override
public void mouseDragged(MouseEvent e) {
Point newPoint = e.getPoint();
if (editing && !last.equals(newPoint)) {
points.add(newPoint);
last = newPoint;
repaint();
}
}


@Override
public void mouseMoved(MouseEvent e) {
}


@Override
public void mouseEntered(MouseEvent e) {
}


@Override
public void mouseExited(MouseEvent e) {
}


@Override
public void mouseClicked(MouseEvent e) {
}


public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {


@Override
public void run() {
CircleGestureDemo t = new CircleGestureDemo();
t.setVisible(true);
}
});
}
}

在 iOS 上实现类似的行为应该不成问题,因为您只需要几个事件和坐标。如下所示(见 例子) :

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* touch = [[event allTouches] anyObject];
}


- (void)handleTouch:(UIEvent *)event {
UITouch* touch = [[event allTouches] anyObject];
CGPoint location = [touch locationInView:self];


}


- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
[self handleTouch: event];
}


- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self handleTouch: event];
}

有几个可能的增强。

从任意点 开始

由于以下简化,目前的要求是从中间顶点开始画圆:

        if(type == null || type != newType) {
if(newType != shape[index]) {
break;
}
bounds[index] = current;
detected[index++] = newType;
}

请注意使用了 index的默认值。对形状的可用“部分”进行简单的搜索就可以消除这个限制。请注意,您需要使用一个循环缓冲区,以检测一个完整的形状:

enter image description here

顺时针逆时针方向

为了同时支持这两种模式,你需要使用之前增强的循环缓冲区,并在两个方向上搜索:

enter image description here

画一个椭圆

bounds数组中已经有了所需的所有内容。

enter image description here

简单地使用这些数据:

cWidth = bounds[2].y - bounds[0].y;
cHeight = bounds[3].y - bounds[1].y;

其他手势(可选)

最后,你只需要正确处理 dx(或 dy)等于零的情况,以支持其他手势:

enter image description here

更新

这个小小的 PoC 得到了很高的关注,所以我稍微更新了一下代码,以便让它顺利地工作,并提供一些绘图提示,突出支持点,等等:

enter image description here

密码如下:

import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.HeadlessException;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;


public class CircleGestureDemo extends JFrame {


enum Type {


RIGHT_DOWN,
LEFT_DOWN,
LEFT_UP,
RIGHT_UP,
UNDEFINED
}


private static final Type[] circleShape = {
Type.RIGHT_DOWN,
Type.LEFT_DOWN,
Type.LEFT_UP,
Type.RIGHT_UP};


public CircleGestureDemo() throws HeadlessException {
super("Circle gesture");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout());
add(BorderLayout.CENTER, new GesturePanel());
setPreferredSize(new Dimension(800, 600));
pack();
}


public static class GesturePanel extends JPanel implements MouseListener, MouseMotionListener {


private boolean editing = false;
private Point[] bounds;
private Point last = new Point(0, 0);
private final List<Point> points = new ArrayList<>();


public GesturePanel() {
super(true);
addMouseListener(this);
addMouseMotionListener(this);
}


@Override
public void paint(Graphics graphics) {
super.paint(graphics);


Dimension d = getSize();
Graphics2D g = (Graphics2D) graphics;


RenderingHints qualityHints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
qualityHints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);


g.setRenderingHints(qualityHints);


if (!points.isEmpty() && cD == 0) {
isCircle(points, g);
g.setColor(HINT_COLOR);
if (bounds[2] != null) {
int r = (bounds[2].y - bounds[0].y) / 2;
g.setStroke(new BasicStroke(r / 3 + 1));
g.drawOval(bounds[0].x - r, bounds[0].y, 2 * r, 2 * r);
} else if (bounds[1] != null) {
int r = bounds[1].x - bounds[0].x;
g.setStroke(new BasicStroke(r / 3 + 1));
g.drawOval(bounds[0].x - r, bounds[0].y, 2 * r, 2 * r);
}
}


g.setStroke(new BasicStroke(2));
g.setColor(Color.RED);


if (cD == 0) {
Point b = null;
for (Point e : points) {
if (null != b) {
g.drawLine(b.x, b.y, e.x, e.y);
}
b = e;
}


} else if (cD > 0) {
g.setColor(Color.BLUE);
g.setStroke(new BasicStroke(3));
g.drawOval(cX, cY, cD, cD);
} else {
g.drawString("Uknown", 30, 50);
}
}


private Type getType(int dx, int dy) {
Type result = Type.UNDEFINED;


if (dx > 0 && dy < 0) {
result = Type.RIGHT_DOWN;
} else if (dx < 0 && dy < 0) {
result = Type.LEFT_DOWN;
} else if (dx < 0 && dy > 0) {
result = Type.LEFT_UP;
} else if (dx > 0 && dy > 0) {
result = Type.RIGHT_UP;
}


return result;
}


private boolean isCircle(List<Point> points, Graphics2D g) {
boolean result = false;
Type[] shape = circleShape;
bounds = new Point[shape.length];


final int STEP = 5;
int index = 0;
int initial = 0;
Point current = points.get(0);
Type type = null;


for (int i = STEP; i < points.size(); i += STEP) {
final Point next = points.get(i);
final int dx = next.x - current.x;
final int dy = -(next.y - current.y);


if (dx == 0 || dy == 0) {
continue;
}


final int marker = 8;
if (null != g) {
g.setColor(Color.BLACK);
g.setStroke(new BasicStroke(2));
g.drawOval(current.x - marker/2,
current.y - marker/2,
marker, marker);
}


Type newType = getType(dx, dy);
if (type == null || type != newType) {
if (newType != shape[index]) {
break;
}
bounds[index++] = current;
}


type = newType;
current = next;
initial = i;


if (index >= shape.length) {
result = true;
break;
}
}
return result;
}


@Override
public void mousePressed(MouseEvent e) {
cD = 0;
points.clear();
editing = true;
}


private int cX;
private int cY;
private int cD;


@Override
public void mouseReleased(MouseEvent e) {
editing = false;
if (points.size() > 0) {
if (isCircle(points, null)) {
int r = Math.abs((bounds[2].y - bounds[0].y) / 2);
cX = bounds[0].x - r;
cY = bounds[0].y;
cD = 2 * r;
} else {
cD = -1;
}
repaint();
}
}


@Override
public void mouseDragged(MouseEvent e) {
Point newPoint = e.getPoint();
if (editing && !last.equals(newPoint)) {
points.add(newPoint);
last = newPoint;
repaint();
}
}


@Override
public void mouseMoved(MouseEvent e) {
}


@Override
public void mouseEntered(MouseEvent e) {
}


@Override
public void mouseExited(MouseEvent e) {
}


@Override
public void mouseClicked(MouseEvent e) {
}
}


public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {


@Override
public void run() {
CircleGestureDemo t = new CircleGestureDemo();
t.setVisible(true);
}
});
}


final static Color HINT_COLOR = new Color(0x55888888, true);
}

用户触摸的像素是 x-y 坐标的集合。Ian Coope 提出了一个非迭代地拟合圆的一般最小平方法算法: https://ir.canterbury.ac.nz/handle/10092/11104。这个想法是使用一个简单的变量来线性化拟合。

我做了一个简单的 python 实现,这里描述: https://scikit-guess.readthedocs.io/en/latest/generated/skg.nsphere_fit.html

你可以在 GitHub 上找到源代码: https://github.com/madphysicist/scikit-guess/blob/master/src/skg/nsphere.py。由于该函数只有大约20行长,所以只要能够访问允许逆矩阵的库,将其翻译成所选语言应该不成问题。实际上,这里描述的问题只需要反转一个3x3的矩阵,您可以通过算术运算手动完成。

下面是特定于2D 案例的一个简单 Java 实现。我没有包括缩放,这可能是一个很好的想法,为生产应用程序,或如果你有大量的像素,但这是一个非常简单的前后处理步骤,留给读者的练习:

// This is just a container for the result for the example.
// Make it proper with getters and setters if you like.
public class Circle
{
public final double radius;
public final double x;
public final double y;


public Circle(double radius, double x, double y)
{
this.radius = radius;
this.x = x;
this.y = y;
}


public static fit(int[] x, int[] y)
{
// exercise for the reader: check that x.length == y.length


// To solve b * x = d in terms of least-squares projection
//   1. bT * b * x = bT * y
//   2. x = inv(bT * b) * bT * d
// Matrix b[i] = [x[i], y[i], 1]
// Vector d[i] = [x[i]*x[i] + y[i]*y[i]]
long[][] bTb = new long[3][3] = \{\{0L, 0L, 0L},
{0L, 0L, 0L},
{0L, 0L, 0L}};
long[] bTd = new long[3] {0L, 0L, 0L};


for(int i = 0; i < x.length; i++) {
long x2 = x[i] * x[i];
long y2 = y[i] * y[i];
long xy = x[i] * y[i];
bTb[0][0] += x2;
bTb[0][1] += xy;
bTb[1][0] += xy;
bTb[1][1] += y2;
bTb[0][2] += x[i];
bTb[2][0] += x[i];
bTb[1][2] += y[i];
bTb[2][1] += y[i];
bTb[2][2] += 1L;
long d = x2 + y2;
bTd[0] += x[i] * d;
bTd[1] += y[i] * d;
bTd[2] += d;
}


// invert the matrix, e.g.: https://www.wikihow.com/Find-the-Inverse-of-a-3x3-Matrix
double det_bTb =
bTb[0][0] * (bTb[1][1] * bTb[2][2] - bTb[2][1] * bTb[1][2]) -
bTb[0][1] * (bTb[1][0] * bTb[2][2] - bTb[2][0] * bTb[1][2]) +
bTb[0][2] * (bTb[1][0] * bTb[2][1] - bTb[2][0] * bTb[1][1]);
// exercise for reader: check if determinant is zero
double[][] inv_bTb = new double[3][3];
inv_bTb[0][0] = (double)(bTb[1][1] * bTb[2][2] - bTb[1][2] * bTb[2][1]) / det_bTb;
inv_bTb[0][1] = (double)(bTb[0][2] * bTb[2][1] - bTb[0][1] * bTb[2][2]) / det_bTb;
inv_bTb[0][2] = (double)(bTb[0][1] * bTb[1][2] - bTb[0][2] * bTb[1][1]) / det_bTb;
inv_bTb[1][0] = (double)(bTb[2][0] * bTb[1][2] - bTb[1][0] * bTb[2][2]) / det_bTb;
inv_bTb[1][1] = (double)(bTb[0][0] * bTb[2][2] - bTb[2][0] * bTb[0][2]) / det_bTb;
inv_bTb[1][2] = (double)(bTb[1][0] * bTb[0][2] - bTb[0][0] * bTb[1][2]) / det_bTb;
inv_bTb[2][0] = (double)(bTb[1][0] * bTb[2][1] - bTb[2][0] * bTb[1][1]) / det_bTb;
inv_bTb[2][1] = (double)(bTb[2][0] * bTb[0][1] - bTb[0][0] * bTb[2][1]) / det_bTb;
inv_bTb[2][2] = (double)(bTb[0][0] * bTb[1][1] - bTb[0][1] * bTb[1][0]) / det_bTb;


double[] result = new double[3] {
bTd[0] * inv_bTb[0][0] + bTd[1] * inv_bTb[0][1] + bTd[2] * inv_bTb[0][2],
bTd[0] * inv_bTb[1][0] + bTd[1] * inv_bTb[1][1] + bTd[2] * inv_bTb[1][2],
bTd[0] * inv_bTb[2][0] + bTd[1] * inv_bTb[2][1] + bTd[2] * inv_bTb[2][2]
};




return new Circle(Math.sqrt(result[2] +
0.25 * result[0] * result[0] +
0.25 * result[1] * result[1]),
0.5 * result[0], 0.5 * result[1]);
}
}

下面是我手绘的一个圆的样本,当所有黑色像素的坐标被传入时,这个解的拟合是:

enter image description here