oozcitak / exiflibrary

A .Net Standard library for editing Exif metadata
MIT License
131 stars 48 forks source link

Imprecision results from inconsistent use of Float vs Rational in reading & writing GPS lat/long #95

Closed extellior closed 2 years ago

extellior commented 2 years ago

What an amazing project - thanks to everyone involved in this! I have run into what I think is a logic problem with how setting & retrieving GPS coordinates works that degrades the accuracy of the coordinates.

  1. The EXIF specification calls for 3 Rational values for deg, min & sec. Rational values are expressed as a fraction, having both a numerator and a denominator. As the specification states, in the case of EXIF data, this is often X/1 for deg & min.
  2. When you use the library to retrieve GPS coordinates, what it retrieves are of type ExifLibrary.MathEx.UFraction32. In examining an instance's properties after reading image data, I see that it does return a numerator and denominator.
  3. Since it is returning a UFraction32, I assumed you'd write one as well, but that's not the case. If I try to set UFraction32 values to the GPS coordinates, Intellisense tells me that it can't be converted to Float. In other words, it is reading Rational but writing Float.

This inconsistency creates multiplicative imprecision that will get worse over time each time you modify the image. What I mean by this is that when you set it, you instantly lose precision in requiring we provide a Float instead of Rational which is more precise, and if you were to read it back, make some other metadata change and rewrite the images, you now have additional loss of imprecision with every single write. Over multiple such read/write cycles, your values will become increasingly wrong.

I am ultimately consuming this from inside a VB.NET application so I've written my own test DLL in C# as a proxy. This is an example of the output which demonstrates the problem:

String conversion of complete lat/long Tag: 33.00°52.00'21.76" South, 151.00°12.00'20.50" East

String conversion of deg, min & sec: LATITUDE: 33/1 52/1 979/45 South LONGITUDE: 151/1 12/1 23306/1137 East

Numerator / Denominator: LAT: 33/1 52/1 979/45 South LONG: 151/1 12/1 23306/1137 East

Converting each component to Float: LAT: 33 52 21.75556 South LONG: 151 12 20.4978 South

On a calculator, the values 979/45 we got back divide into 21.75555556, which is different from the 21.75556 result from a Float conversion, like so:

var f = ((float)ilatd);

And my code for the display you see generated:

back += $"String conversion of complete lat/long Tag:\r\n";
back += $"{latTag} {ilatRef}, {longTag} {ilongRef}\r\n\r\n";
back += $"String conversion of deg, min & sec:\r\n";
back += $"LATITUDE: {ilatd} {ilatm} {ilats} {ilatRef}\r\n";
back += $"LONGITUDE: {ilongd} {ilongm} {ilongs} {ilongRef}\r\n\r\n";
back += "Numerator / Denominator:\r\n";
back += $"LAT: {num}/{denom} {ilatm.Numerator}/{ilatm.Denominator} {ilats.Numerator}/{ilats.Denominator} {ilatRef}\r\n";
 back += $"LONG: {ilongd.Numerator}/{ilongd.Denominator} {ilongm.Numerator}/{ilongm.Denominator} {ilongs.Numerator}/{ilongs.Denominator} {ilongRef}\r\n\r\n";
back += "Converting each component to Float:\r\n";
back += $"LAT: {((float)ilatd)} {((float)ilatm)} {((float)ilats)} {ilatRef}\r\n";
back += $"LONG: {((float)ilongd)} {((float)ilongm)} {((float)ilongs)} {ilatRef}\r\n";

As I explained, in my DLL I have to declare the coordinate values as Float because that is what the Set method requires, so it has:

public float latdeg;
public float latmin;
public float latsec;
public float longdeg;
public float longmin;
public float longsec;

Also from my own DLL just for reference:

                var file = ImageFile.FromFile(source);
                var latTag = file.Properties.Get<GPSLatitudeLongitude>(ExifTag.GPSLatitude);
                var longTag = file.Properties.Get<GPSLatitudeLongitude>(ExifTag.GPSLongitude);

                latdn = (int)ilatd.Numerator;
                latmn = (int)ilatm.Numerator;
                latsn = (int)ilats.Numerator;
                longdn = (int)ilongd.Numerator;
                longmn = (int)ilongm.Numerator;
                longsn = (int)ilongs.Numerator;

                latdd = (int)ilatd.Denominator;
                latmd = (int)ilatm.Denominator;
                latsd = (int)ilats.Denominator;
                longdd = (int)ilongd.Denominator;
                longmd = (int)ilongm.Denominator;
                longsd = (int)ilongs.Denominator;

It's those numerators and denominators that I grab from VB.NET, set the properties and save it again:


                    imagetest.latdeg = imagetest.latdn / imagetest.latdd
                    imagetest.latmin = imagetest.latmn / imagetest.latmd
                    imagetest.latsec = imagetest.latsn / imagetest.latsd

                    imagetest.longdeg = imagetest.longdn / imagetest.longdd
                    imagetest.longmin = imagetest.longmn / imagetest.longmd
                    imagetest.longsec = imagetest.longsn / imagetest.longsd

                    Dim wroteOK As Boolean = imagetest.Set()

Over in my main UI program in VB.NET, I'm actually using this code to populating my own DLL class instance's properties (remember, my DLL is serving as a proxy), which works without any error or issue and is the correct, mathematically accurate Rational representation:

imagetest.latdeg = 33 / 1
imagetest.latmin = 52 / 1
imagetest.latsec = 217554 / 10000
imagetest.latref = "S"
imagetest.longdeg = 151 / 1
imagetest.longmin = 12 / 1
imagetest.longsec = 204978 / 10000
imagetest.longref = "E"

Dim wroteOK As Boolean = imagetest.Set()

So in all of the conversions that take place, my originally supplied 217554/10000, or 21.7554, is never, ever what I get back.

I speculated that if I was to reload the image, get the now inaccurate values, make other changes to the metadata and then save it again, and do so multiple times, the GPS coordinates become increasingly wrong with each such read/write operation. In fact, that is not actually what happens. It actually maintained the same slightly incorrect 979/45 over 10 successive iterations of read/write, so it isn't getting worse, but it's still wrong.

What's the correct way to set the values so that you get back exactly what you put in? The whole point of Rational is exact precision, but I'm not seeing how to actually get it.

oozcitak commented 2 years ago

You can get and set rational values directly without ever converting to a float. Please see the test I added for this issue above.