太久没来这里了,这两天又在之前批量调整照片日期的那段代码的基础上整了两个程序,拿来分享一下。
上周我买了个牛排,对就是the new iPad。哈哈,从此我可以在这上面得瑟我的照片啦~~ 啦呀啦,想一想这是一件多么美妙的事情啊!
题外话,iTunes很无耐,iTools很好用。当我满心欢喜地导入几千张以往的照片后,我崩溃了。
iOS的“照片”软件可以说做的很不错,除了不能建立子目录(可能是iOS限制的)以往,其他功能都还不错,尤其是“地图”功能,在Google Map中看到那一堆堆的“图钉”,那可是我曾走过的足迹啊!
【发现问题】
啊!不对,我没去过俄罗斯啊,也没到过钓鱼岛海域!那明明是昆明的石林啊,怎么回事?照片怎么出现在哪里?
我的照片中的GPS数据,都是我用软件(顺便广告一个GPicSync,类似的还有PhotoMapper(好大啊)和cPicture(2个文件,还是绿色的))把手机记录的GPS轨迹文件(.gpx文件)导入到JPG里的。(事先要给相机对表呦)还不明白?那你自己百度、Google吧。
是不是手机的GPS又因为没信号而漂移了? 不对啊,飘的也太多了,就没有一个准的!
是不是数据错了?在AcdSee里看看,没问题啊!
【分析问题】
我研究了一下Exif里的经纬度坐标,是个24长的byte数组,8个byte“度”,8个byte “分”,8个byte “秒”
8个byte当中,前4个代表分子(高位在后),后4个代表分母(高位在后)
如图,这是 纬度 29 33’ 43.3872”
我试过修改PGX文件,弄了一个整数的经纬度,然后导入JPG,在弄到pad上,它的位置就正常了。
我以为是我的写入软件不好,但是又找了2个同样功能的软件(就是上面说的那一大一小),问题依旧。
是我这种DIY式的带GPS信息的JPG的问题吗? 我的Android手机打开GPS,拍一张,导入iPad,哇咔咔,俄罗斯去啦!
于是,我又找了2张iPhone拍的照片,GPS数据和iPad拍照的有同样的规律:
1.没有“秒”信息(固定分子0,分母1),“秒”是靠“分”位的小数来表示的
2.“分”位的分母一律是100。两位小数精度
而通常的照片,那8个Byte来看,4个分子,4个分母,所以无限不循环是家常便饭了。
让我想不通的是,同样都是Exif 2.21格式,Apple怎么只认满足上面2点的特例的呢!?
唯一能让自己听着过的去的解释,就是为了降低精度,避免一些法律问题(怕你去炸大楼)
而Apple用的方法不像Google加入人为偏移量,而是最简单也最彻底的办法:缩小数据精度。这样一来,最小精度只有0.01分,就是0.6秒。在赤道上就是18米!
所以,坐标上记录的位置和真实位置,差个8、9米就很常见啦。我一路走,一路拍的照片,也变成三五成堆的啦。
【解决问题】
那么,打开一个已有GPS信息的JPG,从Exif中读取出经纬度,并按照苹果的格式重新写入,再做保存。这不就解决问题了吗,我的那上千张带GPS信息的照片,终于在iPad上出现在Google Map的正确位置了。(由于强制采用GCJ-02等加密算法而导致的几米、几十米的偏差,可在App Store中找“地图相册”来解决,这个App可以做修正)
在网上搜索来的“C#读取Exif源码”基础上,追加如下代码:
首先是“坐标”类
public struct Coordinates { private double degrees; public double Degrees { get { return degrees; } } private double minutes; public double Minutes { get { return minutes; } } private double seconds; public double Seconds { get { return seconds; } } public Coordinates(double degrees) : this(degrees, 0, 0) { } public Coordinates(double degrees, double minutes) : this(degrees, minutes, 0) { } public Coordinates(double degrees, double minutes, double seconds) { this.degrees = Math.Floor(degrees); minutes += (degrees - this.degrees) * 60; this.minutes = Math.Floor(minutes); this.seconds = seconds + (minutes - this.minutes) * 60; } public new string ToString() { string str = ""; try { str = this.degrees.ToString() + "," + this.minutes.ToString() + "' " + Math.Round(this.seconds, 2).ToString() + "\" "; } catch { } return str; } public double Value { get { return this.degrees + this.minutes / 60 + this.seconds / 60 / 60; } } }
然后,给ExifManager类增加GPS相关的属性
//北纬 or 南纬? public string GpsLatitudeRef { get { return this.GetPropertyString((int)TagNames.GpsLatitudeRef); } set { this.SetPropertyString((int)TagNames.GpsLatitudeRef, value); } }//纬度 public Coordinates GpsLatitude { get { double degrees = this.GetPropertyRational((int)TagNames.GpsLatitude).ToDouble(); double minutes = this.GetPropertyRational((int)TagNames.GpsLatitude, 8).ToDouble(); double seconds = this.GetPropertyRational((int)TagNames.GpsLatitude, 16).ToDouble(); minutes += 60 * (degrees - Math.Floor(degrees)); degrees = Math.Floor(degrees); seconds += 60 * (minutes - Math.Floor(minutes)); minutes = Math.Floor(minutes); return new Coordinates(degrees, minutes, seconds); } set { try { byte[] bytes = new byte[24]; for (int i = 0; i < 24; i++) { bytes[i] = 0; } bytes[0] = (byte)value.Degrees; bytes[4] = 1; int min = (int)(Math.Round(value.Minutes + value.Seconds / 60,2) * 100); if (min > 256) { bytes[9] = (byte)(int)Math.Floor(min / 256D); bytes[8] = (byte)(min - bytes[9] * 256); } else { bytes[8] = (byte)min; } bytes[12] = 100; bytes[20] = 1; this.SetProperty((int)TagNames.GpsLatitude, bytes, ExifDataTypes.UnsignedRational); } catch(Exception e) { string a = e.ToString(); } } }//经度就略了。。。//海拔 public double GpsAltitude { get { return this.GetPropertyRational((int)TagNames.GpsAltitude).ToDouble(); } }
主程序只需递归遍历所有JPG文件,然后
ExifManager exif = new ExifManager(fi.FullName); string latRef = exif.GpsLatitudeRef; if (latRef.Length > 0) { txtFileName.Text = fi.FullName; txtExif.Text = exif.ToString(); this.Refresh(); if (this.backupToolStripMenuItem.Checked) { string backupPath = fi.DirectoryName + "\\backup"; if (!Directory.Exists(backupPath)) { Directory.CreateDirectory(backupPath); } fi.CopyTo(backupPath + "\\" + fi.Name); } ExifManager.Coordinates x = exif.GpsLatitude; exif.GpsLatitude = new ExifManager.Coordinates(x.Value); ExifManager.Coordinates y = exif.GpsLongitude; exif.GpsLongitude = new ExifManager.Coordinates(y.Value); try { if (File.Exists(fi.DirectoryName + TEMP)) { File.Delete(fi.DirectoryName + TEMP); } exif.Save(fi.DirectoryName + TEMP); exif.Dispose(); fi.Delete(); File.Move(fi.DirectoryName + TEMP, fi.FullName); count++; //Thread.Sleep(10); } catch { MessageBox.Show("文件操作失败,请确保没有其他程序正在打开文件 " + fi.Name, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } exif.Dispose(); progressBar.Value++; this.Refresh(); }
注意,在读取经纬度时,我对原来的代码GetPropertyRational进行了重载。
因为这个函数原本的作用是读取8个byte的内容,而经纬度都有3*8个byte,用原来的函数只能读到“度”,要想读出“分”和“秒”就要向后便宜8个和16个byte
public Rational GetPropertyRational(Int32 PID) { return GetPropertyRational(PID, 0); } public Rational GetPropertyRational(Int32 PID, Int16 disp) { if (IsPropertyDefined(PID)) { byte[] arr = new byte[8]; Array.Copy(this._Image.GetPropertyItem(PID).Value, disp, arr, 0, 8); return GetRational(arr); } else { Rational R; R.Numerator = 0; R.Denominator = 1; return R; } }
【结束语】
终于大功告成了。
我在这个程序的About中写了如下文字:
“本软件用于将照片中的GPS坐标信息批量修改为苹果iOS系统所能识别的格式。
作为摄影爱好者,我喜欢在照片中保留拍摄地点的经纬度坐标,无论是使用内置GPS模块的相机,还是外置GPS附件,甚至是通过手机记录GPS轨迹,回家后再使用软件批量写入JPG文件(这需要相机的时钟要准确)。
然而我却发现这些照片在iPhone和iPad中所显示的位置与真实坐标相去甚远,并不是Google地图的混淆性偏移,那个也就几米、几十米,而这个偏差有几百几千甚至上万公里!
分析得知,虽然都是Exif2.21标准下的GPS信息,但苹果的格式存在某些特殊要求,这也许是苹果出于混淆精确度的考虑,苹果的最小精度为18米(赤道上)。
如果你准备把一组照片同步到iOS设备上,那么之前你可以使用本软件将它们的GPS格式改为iOS可以正确识别的。但这也带来了精度降低的问题,因此我建议你最好在PC上保留照片的转换前版本。
欢迎交流,新浪微博 @长江游泳鱼 http://weibo.com/10391867
”
最后提供下载(需要.Net Framework2.0及以上环境)
耐心一点,并没死。根据文件大小,大概1-3秒一张。虽然用了委托,但是进度条和Exif信息,到后面还是有些卡。谁能给点提示?
======================================================================
最后还要提一点,就是当我有多个相簿的时候,只有最后一个导入的相簿中的照片的GPS位置才能显示出来。也就是说一旦导入了新照片(不管是否含有GPS信息),那么以前的坐标位置就荡然无存了。
如果之前的问题可以用“非军事用途”解释,那么这一点应该是iOS的Bug了吧?
有线索的话,请跟帖告诉我好吗?