Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to extract geographical tags from a tiff image using javascript?

I am working on a project which requires me work with a orthomosaic map. This map is in .tiff format. I want to extract the geographical information from that map so that I can overlay it correctly on a google map. I am using mean stack for this project.

So my final question is: Is there a way to extract geographical tags from a tiff image using javascript?

EDIT : When I use the Maptiler(https://www.maptiler.com/) it automatically places the .tif file correctly on the google map. I want to know how it extracts the information and places it on the map so that I can do it myself.

like image 510
mistletoe Avatar asked Sep 13 '25 22:09

mistletoe


1 Answers

In order to read the TIFF tags, you'll need a proper TIFF parser, like the tiff package, so that you can run through the non-bitmap parts of an IFD block:

import fs from "node:fs";
import tiff from "tiff";

// Load a GeoTIFF file
const file = fs.readFileSync(`ALPSMLC30_S045E168_DSM.tif`);

// We can do this in the browser, too, the `decode` function takes an ArrayBuffer
const image = tiff.decode(file.buffer);

// get the first IFD block's pixels and fields
const { data:pixels, fields, width, height } = image[0];

// let's see what tags apply
console.log(fields);

Running this on the example file from the ALOS Word 3D - 30m dataset yields:

Map(19) {
  254 => 0,
  256 => 3600,
  257 => 3600,
  258 => 16,
  259 => 1,
  262 => 1,
  273 => Uint32Array(3600) [
    ... 3600 more items
  ],
  274 => 1,
  277 => 1,
  278 => 1,
  279 => Uint32Array(3600) [
    ... 3600 more items
  ],
  282 => 1,
  283 => 1,
  284 => 1,
  339 => 2,
  33550 => Float64Array(3) [
    0.0002777777777777778,
    0.0002777777777777778,
    0
  ],
  33922 => Float64Array(6) [
    0,
    0,
    0,
    168,
    -44,
    0
  ],
  34735 => Uint16Array(24) [
       1,    1,    0,    5,
    1024,    0,    1,    2,
    1025,    0,    1,    1,
    2048,    0,    1, 4326,
    2052,    0,    1, 9001,
    2054,    0,    1, 9102
  ],
  34737 => 'WGS-84'
}

I want to know how it extracts the information and places it on the map so that I can do it myself.

Strap in, this is going to get detailed.

The tag numbers are all well-defined things, and can be found listed over on (the now unfortunately intentionally no longer available) https://www.awaresystems.be/imaging/tiff/tifftags.html, where all the low numbered ones are universal TIFF tags, e.g.

  • 256 and 257 are the image width and height,
  • 274 is "is this image rotated",
  • 284 tells us whether pixels are stored RGBRGBetc or as separate R, G, and B arrays,
  • etc.

The higher number ones are "private" tags, only meaningful when the TIFF is used in a specific context. For example, that 339 tells us pixel values are signed integers that have been encoded using two's complement, 33550 is the model pixel scale tag, 34737 is the GeoASCII params tag, etc.

In this TIFF's tag set 34735 is arguably the most important tag, as it houses the GeoKey dictionary that is used by mapping software to properly place images on the world map:

The first line, 1 1 0 5, reads:

  • "Tiff version 1",
  • "keys revision 1.0" (encoded as separate "before the period" and "after the period" integer values), and
  • "there are 5 entries in this dictionary"

Each entry in the GeoKey dictionary is ordered as "key, tifftaglocation, count, value(s)", with the tifftaglocation field having either value 0 (all "value" fields use SHORT, an unsigned 16 bit integer datatype) or 1 (the tiff tag's formal specification tells you how to decode the value), and the count field tells us how many records we need to parse, immediately following the count value.

In this case, there are five:

  • 1024 (model type) has value 2: the data represents a mapping based on the Geographic latitude-longitude System.
  • 1025 (raster type) has value 1: Each pixel represents an area (e.g it's an averaged height map, as opposed to a pixel being a specific point with an exact known elevation)
  • 2048 (geographic type) has value 4326: This is WGS84 data, e.g. it uses (Web) Mercator projection, used by pretty much all mapping software
  • 2052 (linear units) has value 9001: The data uses EPSG Linear Units (i.e. "decimal degrees" so a value of 38.25 means 38 whole degrees plus a quarter degree)
  • 2054 (angular units) has value 9102: The angular aspect of the data is "degrees of arc", so combined with the previous value we know that if we see 38.25 that means 38.25 arc degrees.

For doing map-related things (in absence of tools that can automatically place your tiff for you), we look at tag 33500 (which gives us our map scale) and 33922 (which gives us our map translation):

  ...
  33550 => Float64Array(3) [
    0.0002777777777777778,
    0.0002777777777777778,
    0
  ],
  33922 => Float64Array(6) [
      0,   0,  0,
    168, -44,  0
  ],
  ...

The values in 33922 tell us that pixel index x=0, y=0 (with an unused z-value 0) maps to real world arc degree coordinate x=168, y=-44 (also with an unused z-value 0) i.e. S44 E168 just off the West coast of New Zealand's South Island, and 33550 tells us that pixels in the image are spaced 1 degree of arc * 0.00027[...] = 1/3600 arc degree = 1 arc second = 30.87 meters apart. The same value is used for both the x and y components, so both x and y spacing is 30.87 meters, which lets us determine the proper translation and scaling for placing and aligning this data on a map.

It also lets us compute the "GPS coordinate given a TIFF pixel" and "TIFF pixel given a GPS coordinate". Based on https://gdal.org/tutorials/geotransforms_tut.html:

function transform(matrix, x, y) {
  return [
    matrix[0] + matrix[1] * x + matrix[2] * y,
    matrix[3] + matrix[4] * x + matrix[5] * y,
  ];
}

let [sx, sy, _sz] = fields.get(33550);

const [
  _px, _py,  _k,
   gx,  gy, _gz
] = fields.get(33922);

// Just like SVG or <canvas>, GeoTIFF uses a "flipped" y coordinate.
sy = -sy;

// Our "forward transform" goes from pixels to geographic coordinate,
// and is the (partial) matrix from the link above.
const pixelToGeoMatrix = [
  gx,  sx,   0,
  gy,   0,  sy
];

// Our "reverse transform" is literally the inverse matrix operation,
// converting geographic coordinates to pixels. 
const geoToPixelMatrix = [
  -gx/sx,  1/sx,   0,
  -gy/sy,   0,    1/sy
];

function pixelToGeo(x, y) {
  const [lat, long] = transform(pixelToGeoMatrix, x, y);
  return { lat, long };
}

function geoToPixel(long, lat) {
  const [x, y] = transform(geoToPixelMatrix, long, lat);
  return { x: x|0, y: y|0 };
}

(the transform reversal is explained over on https://gis.stackexchange.com/a/452575/219296)

Using this code, we can convert from GPS coordinate to pixel coordinate and back. For example, running the following code:

const lat = 168.321971;
const long = -44.9856891;
const { x, y } = geoToPixel(long, lat);
const elevation  = pixels[x + y * width];
console.log(`GPS: (${lat},${long}), PX: (${x},${y}), elevation: ${elevation}m`);

yields the following log for the above TIFF data:

GPS: (-44.9856891,168.321971), PX: (1159,3548), elevation: 1421m

Or going the other way:

const [ x, y ] = [
  (Math.random() * width)|0,
  (Math.random() * height)|0
];
const { lat, long } = pixelToGeo(x, y);
console.log(`PX: (${x},${y}) maps to GPS: (${lat},${long})`);

yields the following log for the above TIFF data:

PX: (278,1882) maps to GPS: (-44.522777777777776,168.07722222222222)
like image 162
Mike 'Pomax' Kamermans Avatar answered Sep 16 '25 14:09

Mike 'Pomax' Kamermans