-
Notifications
You must be signed in to change notification settings - Fork 135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Best plane fit from points #216
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for this PR, it seems interesting. Let me know if my comments are not clear enough.
src/Spatial/Euclidean/Plane.cs
Outdated
/// <remarks>this method uses SVD to fit the plane.</remarks> | ||
public static Plane CreateFittedPlaneFrom(IEnumerable<Point3D> points) | ||
{ | ||
var c = Point3D.Centroid(points); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use more descriptive variable names, like throughPoint
instead of just c
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will do, thanks.
src/Spatial/Euclidean/Plane.cs
Outdated
{ | ||
var c = Point3D.Centroid(points); | ||
|
||
var A = CreateMatrix.DenseOfRowVectors( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also called the scatter matrix
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understood your comment correctly, it seems to me that the scatter matrix means a different matrix from the matrix A
written in the code.
To say that these 2 matrices are different, I checked their sizes(=rows count and columns count).
The scatter matrix is an 3x3 matrix,
and the matrix A
is an nx3 matrix,
where
I agree with you to give the variable a descriptive name.
Do you have any ideas?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My apologies, you are right. The scatter matrix (M
in the paper) would be A^T * A
in your notation. Perhaps we can call this relativePointMatrix
, since this is exactly what it is.
src/Spatial/Euclidean/Plane.cs
Outdated
|
||
var A = CreateMatrix.DenseOfRowVectors( | ||
points.Select(p => p - c) | ||
.Select(p => CreateVector.DenseOfArray(new double[] { p.X, p.Y, p.Z }))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I would do in this case, is to first allocate the necessary matrix space (new DenseMatrix(rows, columns)
), and then write a single for-each to populate each row of it. This way we could easily parallelize the loop in case we need it, if for example we have a LOT of points.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will do, thanks.
I think that the parallelization is out of scope for me.
Thanks again for your point of view.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this fix follow the policy of Math.NET Spatial?
I'm afraid the case where you maintainers respect RAII(Resource acquisition is initialization) policy and I would break it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the parallelization is out of scope for me.
Sure, I wasn't expecting this to be part of this PR. I don't want to prematurely optimize things in any case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this fix follow the policy of Math.NET Spatial?
I'm afraid the case where you maintainers respect RAII(Resource acquisition is initialization) policy and I would break it.
I am not sure I understand your question/concern here, can you explain it again please?
src/Spatial/Euclidean/Plane.cs
Outdated
|
||
var svd = A.Svd(true); | ||
var matV = svd.VT.Transpose(); | ||
var theIndex = svd.S.Count-1; // in this case, theIndex = 2. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can entirely skip this variable and use var normal = UnitVector3D.OfVector(matV.Column(matV.Cols - 1));
. This better captures semantically the intention, that we need the last eigenvector of V.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for comment.
I introduced an explaining variable, theIndex
, but it didn't work.
I wanted to say "take the corresponding column of matV as the normal vector, to the smallest singular value's index in the diagonal matrix S"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough. Perhaps we can call it smallestEigenvalueColumnIndex
. You may have noticed that I have a tendency towards long, descriptive names 😃
src/Spatial/Euclidean/Plane.cs
Outdated
var theIndex = svd.S.Count-1; // in this case, theIndex = 2. | ||
var normal = UnitVector3D.OfVector(matV.Column(theIndex)); | ||
|
||
var result = new Plane(normal, c); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var bestFit = new Plane(normal, c);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm afraid that I didn't define any properties the best Fit plane should have.
Can I use fittedPlane
for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should evaluate the best fit based on the orthogonal euclidean distance.
Sorry, I couldn't read all your comments at that moment. my fault.
You had already defined "the best fit" for this problem.
Now, we can use var bestFit = new Plane(normal, c);
[TestCase(ZeroPoint, Y, Z, YZ, X, "-1,0,0")] //all 4 points fall on the plane x=-1 | ||
[TestCase(ZeroPoint, Z, X, ZX, Y, "0,+1,0")] //all 4 points fall on the plane y=+1 | ||
[TestCase(ZeroPoint, Z, X, ZX, Y, "0,-1,0")] //all 4 points fall on the plane y=-1 | ||
public void CreateFittedPlaneFrom_With4PointsOnAxisAlignedPlane(string sV1, string sV2, string sV3, string sV4, string sENormal, string sRootPoint) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not really clear to me, what this test is supposed to guarantee.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I totally agree with you.
I think I should remove this test.
Assert.That(actual, Is.EqualTo(expected)); | ||
} | ||
|
||
[TestCase("1,1,9;1,2,14;1,3,20;2,1,11;2,2,17;2,3,23;3,1,15;3,2,20;3,3,26", -21.7240278973, -41.0640090729, 7.3269978417, 1)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need more tests, which cover what happens when we have less than 3 points (2, 1, or none).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Plane.CreateFittedPlaneFrom(points)
is not for the testcases with insufficient points.
Plane.CreateFittedPlaneFrom(points)
is for over-determined system.
Instead, Plane.Validate(points)
will do for this purpose.
It goes a detailed implementation, Plane.Validate(points)
will be something like:
public static void Validate(IEnumerable<Point3D> points)
{
// [p0x, p0y, p0z]
// [p1x, p1y, p1z]
// mat= [p2x, p2y, p2z]
// [p3x, p3y, p3z]
// [p4x, p4y, p4z]
// [..., ..., ...]
//this expression will be fixed, according to the discussion
var mat = CreateMatrix.DenseOfRowVectors(
points.Select(p => CreateVector.DenseOfArray(new double[] { p.X, p.Y, p.Z })));
var rankOfMat = mat.Rank(); // the library uses SVD.
if(rankOfMat <= 0) // DoF=0
{
throw new Exception("The same point?");
}
if(rankOfMat <= 1) // DoF=1
{
throw new Exception("Are they collinear?");
}
// do nothing intentionally
// because there are no problem, we can span 2d-vector-space with given points
}
If few (2 or less) points are given, the method Plane.TryFromPonits()
should return false
. It is similar to int.TryParse(string)
's behavior, as mentioned in #184.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is over-engineering the problem. We don't need a Plane.Validate
method, we only need to check in this "best fit method" that before we dive in and calculate the SVD of A, its row count is more than or equal to 3. Something like if (A.Rows < 3) { throw ...}
(the syntax might not be 100% perfect)
Assert.That(actual.A, Is.EqualTo(expected.A).Within(tolerance), "A"); | ||
Assert.That(actual.B, Is.EqualTo(expected.B).Within(tolerance), "B"); | ||
Assert.That(actual.C, Is.EqualTo(expected.C).Within(tolerance), "C"); | ||
Assert.That(actual.D, Is.EqualTo(expected.D).Within(tolerance), "D"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AssertGeometry.AreEqual(expectedPlane, actualPlane);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True. I wish I could have.
I need time to adjust AssertGeometry.AreEqual(PlaneA, PlaneB)
to pass this test
where
PlaneA.Normal == -PlaneB.Normal;
PlaneA.RootPoint == PlaneB.RootPoint;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another problem I spotted in the Plane
implementation after I took over the maintenance of this repo; the plane is not defined in its Hesse normal form, which I find problematic.
Then the normal direction is uniquely defined and fixed, for any plane. In addition the equality operator should be adjusted, so that we don't check equality of the root points, but that something like other.Conatins(rootPoint)
, where we check that the root point is contained in the other plane. There is nothing special in the choice of a root point for a plane, any point lying on it is equally valid.
I wanted to open another PR and fix this (breaking) change for the next (v0.8.0) release. Maybe you want to wait with this PR after I am finished (?)
TestContext.WriteLine($"{nameof(actual)}={actual}"); | ||
TestContext.WriteLine($"{nameof(expected)}={expected}"); | ||
|
||
var tolerance = 6e-3; // for this example |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a magic number. Perhaps the values from the original example are not as exact as we want them to be. I would adjust the values in our example, so that the default tolerance would pass the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok. thank you for your cooperation.
I think your implementation is correct, we should evaluate the best fit based on the orthogonal euclidean distance. |
… vectors Test: adjust AssertGeometry.AreEqual(expectedPlane, actualPlane)
Fixed accuracy test in #218 |
I added
Plane.CreateFittedPlaneFrom(points)
method with some unit tests for it.I would like to discuss the following topic:
a. (z)axis-aligned distance
b. orthogonal distance