2010/07/30

[C#] 讀取圖檔中的 GPS 基本資訊

由於現在很多 Google Map 的應用, 主要用到圖片的 GPS 資訊不外乎緯度, 經度, 高度, 版本.
所以本篇僅說明如何利用 .Net 本身的 Library 從圖檔中取出這些資訊.
不過, 若圖檔本身未包裝這些資訊進去, 程式本身亦不可能讀到.
在說明如何取得這些資訊前, 請記得參考以下資訊:

下表是此次會用到的 GPS 資訊:
Tag ID Tag Name Writable Values / Notes
0x0000 GPSVersionID int8u[4]:
0x0001 GPSLatitudeRef string[2] 'N' = North
'S' = South
0x0002 GPSLatitude rational64u[3]
0x0003 GPSLongitudeRef string[2] 'E' = East
'W' = West
0x0004 GPSLongitude rational64u[3]
0x0005 GPSAltitudeRef int8u 0 = Above Sea Level
1 = Below Sea Level
0x0006 GPSAltitude rational64u

開發工具: Vistual Studio 2008 + .Net Framework 3.5
首先, 先利用 Image 這個 Class 載入一張圖檔, 並取得該圖片的屬性清單, 如下:
//載入圖片
Image objImage = Image.FromFile(@"c:\3685983913_f633a22195_o.jpg");
//取得所有的屬性(以PropertyId做排序)
var propertyItems = objImage.PropertyItems.OrderBy(x => x.Id);
//暫訂緯度為N(北緯)
char chrGPSLatitudeRef = 'N';
//暫訂經度為E(東經)
char chrGPSLongitudeRef = 'E';
foreach (PropertyItem objItem in propertyItems)
{
  //只取Id範圍為0x0000到0x001e
  if (objItem.Id >= 0x0000 && objItem.Id <= 0x001e)
  {
    switch (objItem.Id)
    {
        //分別處理各個屬性
    }
  }
}
上述的 Id 部分請參考上表中的 Tag ID.
雖然程式中取的範圍是 0x0000 至 0x001e, 不過實際上我們僅會用到 0x0000 至 0x0006.
接下來我們開始處理每個 case:
  • GPSVersionID (0x0000):
    case 0x0000:
      //將byte[]中的元素都轉成string
      var query = from tmpb in objItem.Value 
                  select tmpb.ToString();
      //將各個元素以'.'串接
      string sreVersion = string.Join(".", query.ToArray());
      break;
  • GPSLatitudeRef (0x0001): 'N' = North, 'S' = South:
    case 0x0001:
      //透過BitConverter, 將Value轉成Char('N' / 'S')
      //此值在後續的Latitude計算上會用到
      chrGPSLatitudeRef = BitConverter.ToChar(objItem.Value, 0);
      break;
  • GPSLatitude (0x0002):
    從表格中可以得知, 該屬性值是 rational64u[3], 但其 Value 會是 byte[24].
    例如:
    byte[] { 
        37, 0, 0, 0, 1, 0, 0, 0 
      , 44, 0, 0, 0, 1, 0, 0, 0 
      , 51, 29, 0, 0, 5, 1, 0, 0 
    }
    計算方式為: 每 8 個 byte 為一組, 每一組的前 4bytes 轉 uint, 之後除以後 4bytes 轉成的 uint.
    經此計算後, 這三組將依序為 degrees, minutes, seconds.
    • 算出緯度值:
      case 0x0002:
        if (objItem.Value.Length == 24)
        {
          //degrees(將byte[0]~byte[3]轉成uint, 除以byte[4]~byte[7]轉成的uint)
          double d = BitConverter.ToUInt32(objItem.Value, 0) * 1.0d 
                         / BitConverter.ToUInt32(objItem.Value, 4);
          //minutes(將byte[8]~byte[11]轉成uint, 除以byte[12]~byte[15]轉成的uint)
          double m = BitConverter.ToUInt32(objItem.Value, 8) * 1.0d 
                         / BitConverter.ToUInt32(objItem.Value, 12);
          //seconds(將byte[16]~byte[19]轉成uint, 除以byte[20]~byte[23]轉成的uint)
          double s = BitConverter.ToUInt32(objItem.Value, 16) * 1.0d 
                         / BitConverter.ToUInt32(objItem.Value, 20);
          //計算緯度的數值, 如果是南緯, 要乘上(-1)
          double dblGPSLatitude = (((s/60 + m)/60)+d)*(chrGPSLatitudeRef.Equals('N') ? 1 : -1);
        }
        break;
    • 呈現緯度的表示式: 例如, 37 deg 44' 28.64" N
      //只要轉換一下輸出的格式
      string strLatitude=string.Format("{0:#} deg {1:#}' {2:#.00}\" {3}", 
        d, m, s, chrGPSLatitudeRef);
  • GPSLongitudeRef (0x0003): 可參考 GPSLatitudeRef 的作法 ('E' = East, 'W' = West)
    case 0x0003:
      //透過BitConverter, 將Value轉成Char('E' / 'W')
      //此值在後續的Longitude計算上會用到
      chrGPSLongitudeRef = BitConverter.ToChar(objItem.Value, 0);
      break;
  • GPSLongitude (0x0004): 可參考 GPSLatitude 的計算方式
    case 0x0004:
      if (objItem.Value.Length == 24)
      {
        //degrees(將byte[0]~byte[3]轉成uint, 除以byte[4]~byte[7]轉成的uint)
        double d = BitConverter.ToUInt32(objItem.Value, 0) * 1.0d 
                      / BitConverter.ToUInt32(objItem.Value, 4);
        //minutes(將byte[8]~byte[11]轉成uint, 除以byte[12]~byte[15]轉成的uint)
        double m = BitConverter.ToUInt32(objItem.Value, 8) * 1.0d 
                      / BitConverter.ToUInt32(objItem.Value, 12);
        //seconds(將byte[16]~byte[19]轉成uint, 除以byte[20]~byte[23]轉成的uint)
        double s = BitConverter.ToUInt32(objItem.Value, 16) * 1.0d 
                      / BitConverter.ToUInt32(objItem.Value, 20);
        //計算緯度的數值, 如果是西經, 要乘上(-1)
        double dblGPSLongitude = (((s/60 + m)/60)+d)*(chrGPSLongitudeRef.Equals('E') ? 1 : -1);
      }
      break;
    同樣地, 如果要輸出經度的表示式 (97 deg 15' 56.72" W), 只要調輸出的格式:
    Console.WriteLine("{0:#} deg {1:#}' {2:#.00}\" {3}", 
                       d, m, s, chrGPSLongitudeRef);
  • GPSAltitudeRef (0x0005): 因為該屬性值是 0/1 (0 = Above Sea Level, 1 = Below Sea Level), 所以透過 BitConverter 轉成 Boolean.
    case 0x0005:
      string strAltitude = BitConverter.ToBoolean(objItem.Value, 0) ? "0" : "1";
      break;
  • GPSAltitude (0x0006): 這個屬性值是 byte[8], 計算方式與經緯度相同, 取前 4bytes 轉成 uint, 再除以後 4bytes 轉成的 uint 值.
    case 0x0006:
      if (objItem.Value.Length == 8)
      {
        //將byte[0]~byte[3]轉成uint, 除以byte[4]~byte[7]轉成的uint
        double dblAltitude = BitConverter.ToUInt32(objItem.Value, 0) * 1.0d 
                                / BitConverter.ToUInt32(objItem.Value, 4);
      }
      break;
以 Google Map 的應用為例, 上述的範例圖檔可計算出 Latitude 為 37.741289, Longitude 為 -97.265755.
將此值帶入 http://maps.google.com/?q=37.741289,-97.265755&spn=0.05,0.05&t=h&om=1&hl=zh-tw, 便可得到該圖片在 Map 中的定位.
若有人想開發類似的 Map 應用, 也可在使用者上傳圖檔後, 透過此方式將該圖檔的 GPS 圖檔存入至資料庫, 後續再取出這些資訊做加值應用.

4 則留言:

  1. 您好,我想請教一下一些有關這個程式碼的問題QQ"

    我引用了您的程式碼,可是還是跑不出來GPS的訊息耶ˊˋ"

    是我哪裡出錯了嗎?

    他的import要import哪些東西呢

    ~"~試了很多次都還是不行...

    回覆刪除
  2. Hi,
    You need to add a reference "System.Drawing".

    and using below namespace:

    using System.Drawing;

    using System.Drawing.Imaging;

    回覆刪除
  3. 請問 var propertyItems = objImage.PropertyItems.OrderBy(x => x.Id);

    這行程式是在顯示所有屬性嗎?
    還是只是宣告?

    回覆刪除
  4. var propertyItems = objImage.PropertyItems.OrderBy(x => x.Id);
    這段是將 Image 的屬性(Property) 簡單地用 id 做一個排序.
    你也可以直接處理, 將 foreach 改成如下的code.
    foreach (PropertyItem objItem in objImage.PropertyItems)

    回覆刪除