Coordinate Transforms in .Net Core 3 with MathNet

Coordinate Transforms in .Net Core 3 with MathNet

Sometimes you will need to convert between two coordinate systems. In this case, we are going to transform the coordinates using .Net Core 3.0 and MathNet.

The MathNet assemblies can be download from NuGet; I am using MathNet 4.8.1, which is the latest version at the time of writing and is already compatible with .Net Core 3.0.

In my example, I have two coordinate systems, one starting at point 0,0 and with a max x and y of 600 and 400. The second coordinate system has a starting point of -8, 4, and a max x and y of 4, -4. I want to calculate the points 400,200 from the first coordinate system into coordinates in the second.

I want to calculate x3′ and y3′ from x3 and y3.

First, we need to set the bounds of the coordinate systems we want to convert between; we will hold this information in a structure defined as follows.

public struct BoundingBox
    {
        public double XMin;
        public double YMin;
        public double XMax;
        public double YMax;
    }

To convert between two coordinate systems, we will create two bounding boxes with the following values.

var boundingBox = new BoundingBox
            {
                XMin = 0,
                YMin = 0,
                XMax = 600,
                YMax = 400
            };

 var boundingBoxPrime = new BoundingBox
            {
                XMin = -8,
                YMin = 4,
                XMax = 4,
                YMax = -4
            };

Once we have our bounds, we need to generate the transformation variables that we are later going to plug into the conversion calculation.
First, for the BoundingBox, containing the coordinate system we will convert into, we will create a dense matrix using the following definition.

The code in MathNet is as follows.

var coordinationMatrix = DenseMatrix.OfArray(new double[,] {
     {boundingBoxOne.XMin, boundingBoxOne.YMin, 1d, 0d},
     {-(boundingBoxOne.YMin), boundingBoxOne.XMin, 0d, 1d},
     {boundingBoxOne.XMax, boundingBoxOne.YMax, 1d, 0d},
     {-(boundingBoxOne.YMax), boundingBoxOne.XMax, 0d, 1d}}
);

Once we have the dense matrix, we are going to get the inverse. The MathNet inverse function handles all this work.

var inverseMatrix = coordinationMatrix.Inverse();

Next, from the bounding box, we are converting from, we create a second dense matrix.

In MathNet, this equates to the following code.

var primeBounds = DenseMatrix.OfArray(new double[,] {
                    {boundingBoxTwo.XMin}, 
                    {boundingBoxTwo.YMin}, 		 
                    {boundingBoxTwo.XMax},
                    {boundingBoxTwo.YMax}
                }
            );

Next, we multiply the two matrices.

var transformVariables = inverseMatrix.Multiply(primeBounds);

Rather than return the variables as a simple tuple, we will create another structure to hold the variables.

public struct TranformVariables
    {
        public double A;
        public double B;
        public double C;
        public double D;
    }

We then copy the transform variables into our new structure.

return new TranformVariables
                {
                    A = Math.Round(transformVariables[0, 0], 2),
                    B = Math.Round(transformVariables[1, 0], 2),
                    C = Math.Round(transformVariables[2, 0], 2),
                    D = Math.Round(transformVariables[3, 0], 2)
                };

The full code for the method to generate the variables is as follows:

private static TranformVariables GetTransformVariables(BoundingBox boundingBox, BoundingBox boundingBoxPrime)
        {
            var coordinationMatrix = DenseMatrix.OfArray(new double[,] {
                {boundingBox.XMin, boundingBox.YMin, 1d, 0d},
                {-(boundingBox.YMin), boundingBox.XMin, 0d, 1d},
                {boundingBox.XMax, boundingBox.YMax, 1d, 0d},
                {-(boundingBox.YMax), boundingBox.XMax, 0d, 1d}}
            );

            var inverseMatrix = coordinationMatrix.Inverse();

            var primeMatrix = DenseMatrix.OfArray(new double[,] {
                    {boundingBoxPrime.XMin}, 
                    {boundingBoxPrime.YMin}, 
                    {boundingBoxPrime.XMax},
                    {boundingBoxPrime.YMax}
                }
            );

            // generates a 4 row 1 column matrix with a,b,c,d
            var transformVariables = inverseMatrix.Multiply(primeMatrix);

            return
                new TranformVariables
                {
                    A = Math.Round(transformVariables[0, 0], 2),
                    B = Math.Round(transformVariables[1, 0], 2),
                    C = Math.Round(transformVariables[2, 0], 2),
                    D = Math.Round(transformVariables[3, 0], 2)
                };
        }

Once I have the A, B, C and D variables, I can use the following calculations to covert a value from x to x’

(variables.A * x) + (variables.B * y) + variables.C;

and from y to y’

(variables.B * x) - (variables.A * y) + variables.D;

I can also calculate from x’ to x

((variables.A * xp1) + (variables.B * yp1) - (variables.B * variables.D) - (variables.A * variables.C)) / ((variables.A * variables.A) + (variables.B * variables.B));

and from y’ to y

((variables.B * xp1) - (variables.A * yp1) - (variables.B * variables.C) + (variables.A * variables.D)) / ((variables.A * variables.A) + (variables.B * variables.B));

To test our transformation, we are going to transform the x and y coordinates for the known points and check whether they match the x’ and y’ values.

// check that we can convert the x and y update and lower bound values to x' and y'
var xp1 = CalculateXPrime(transformVariables, 600, 400);
var yp1 = CalculateYPrime(transformVariables, 600, 400);
// xp1 and yp1 are 4,-4

var xp2 = CalculateXPrime(transformVariables, 0, 0);
var yp2 = CalculateYPrime(transformVariables, 0, 0);
// xp2 and yp2 are -8,4

Now we can transform the known x’ and y’ coordinates to ensure that they match the x and y values.

// check we can convert the x' and y' upper and lower bound values to x and y
var x1 = CalculateX(transformVariables, -8, 4);
var y1 = CalculateY(transformVariables, -8, 4);
// x1 and y1 are 0,0

var x2 = CalculateX(transformVariables, 4, -4);
var y2 = CalculateY(transformVariables, 4, -4);
// x2 and y2 are 600,400

Finally we can convert the x3 and y3 values into the unknown x3′ and y3′ values.

 // calculate x3' and y3' from x3 and y3
 var xp3 = CalculateXPrime(transformVariables, 400, 200);
 var yp3 = CalculateYPrime(transformVariables, 400, 200);

The full code for the example is.

using MathNet.Numerics.LinearAlgebra.Double;
using System;

namespace MathDotNetExample
{
    class Program
    {
        static void Main(string[] args)
        {
            // bounding box for (0,0)(600,400)
            var boundingBox = new BoundingBox
            {
                XMin = 0,
                YMin = 0,
                XMax = 600,
                YMax = 400
            };

            // bounding box for (-8,4)(4,-4)
            var boundingBoxPrime = new BoundingBox
            {
                XMin = -8,
                YMin = 4,
                XMax = 4,
                YMax = -4
            };

            // create the transform
            var transformVariables = GetTransformVariables(boundingBox, boundingBoxPrime);

            // check that we can convert the x and y update and lower bound values to x' and y'
            var xp1 = CalculateXPrime(transformVariables, 600, 400);
            var yp1 = CalculateYPrime(transformVariables, 600, 400);
            // xp1 and yp1 are 4,-4

            var xp2 = CalculateXPrime(transformVariables, 0, 0);
            var yp2 = CalculateYPrime(transformVariables, 0, 0);
            // xp2 and yp2 are -8,4
            
            // check we can convert the x' and y' upper and lower bound values to x and y
            var x1 = CalculateX(transformVariables, -8, 4);
            var y1 = CalculateY(transformVariables, -8, 4);

            var x2 = CalculateX(transformVariables, 4, -4);
            var y2 = CalculateY(transformVariables, 4, -4);

            // calculate x3' and y3' from x3 and y3
            var xp3 = CalculateXPrime(transformVariables, 400, 200);
            var yp3 = CalculateYPrime(transformVariables, 400, 200);


            // do the reverse by calculating back from x3' and y3' to x3 and y3
            var x3 = CalculateX(transformVariables, 0, 0);
            var y3 = CalculateY(transformVariables, 0, 0);
        }

        private static double CalculateX(TranformVariables variables, double xp1, double yp1)
        {
            return ((variables.A * xp1) + (variables.B * yp1) - (variables.B * variables.D) - (variables.A * variables.C)) 
                   / ((variables.A * variables.A) + (variables.B * variables.B));
        }

        private static double CalculateY(TranformVariables variables, double xp1, double yp1)
        {
            return ((variables.B * xp1) - (variables.A * yp1) - (variables.B * variables.C) + (variables.A * variables.D)) 
                   / ((variables.A * variables.A) + (variables.B * variables.B));
        }

        private static double CalculateXPrime(TranformVariables variables, double x, double y)
        {
            return (variables.A * x) + (variables.B * y) + variables.C;
        }

        private static double CalculateYPrime(TranformVariables variables, double x, double y)
        {
            return (variables.B * x) - (variables.A * y) + variables.D;
        }

        private static TranformVariables GetTransformVariables(BoundingBox boundingBox, BoundingBox boundingBoxPrime)
        {
            var coordinationMatrix = DenseMatrix.OfArray(new double[,] {
                {boundingBox.XMin, boundingBox.YMin, 1d, 0d},
                {-(boundingBox.YMin), boundingBox.XMin, 0d, 1d},
                {boundingBox.XMax, boundingBox.YMax, 1d, 0d},
                {-(boundingBox.YMax), boundingBox.XMax, 0d, 1d}}
            );

            var inverseMatrix = coordinationMatrix.Inverse();

            var primeMatrix = DenseMatrix.OfArray(new double[,] {
                    {boundingBoxPrime.XMin}, 
                    {boundingBoxPrime.YMin}, 
                    {boundingBoxPrime.XMax},
                    {boundingBoxPrime.YMax}
                }
            );

            // generates a 4 row 1 column matrix with a,b,c,d
            var transformVariables = inverseMatrix.Multiply(primeMatrix);

            return
                new TranformVariables
                {
                    A = Math.Round(transformVariables[0, 0], 2),
                    B = Math.Round(transformVariables[1, 0], 2),
                    C = Math.Round(transformVariables[2, 0], 2),
                    D = Math.Round(transformVariables[3, 0], 2)
                };
        }
    }
}

Just a couple of final points on the code, the code is structured for clarity of the example rather than real-world reuse. In an actual implementation, I would use a unit test framework to check the expected values for the transformation.

Leave a Reply

Your email address will not be published. Required fields are marked *