0%

解析tiff

核心原理和流程

读取文件缓冲区

1
2
3
const buffer = fs.readFileSync(filePath);
const tiff = await GeoTIFF.fromArrayBuffer(buffer.buffer);
const image = await tiff.getImage();
  • fs.readFileSync 同步读取整个 .tif/.tiff 二进制内容到内存。
  • GeoTIFF.fromArrayBuffer 将这个二进制数据解析成一个 TIFF 对象,内部会读取 TIFF 头、IFD(图像文件目录)等结构。
  • getImage() 拿到第一个影像帧(TIFF 可以包含多帧,但我们通常只关心第一个图层)。

获取栅格尺寸

1
2
const width = image.getWidth();
const height = image.getHeight();
  • 直接从 TIFF 元数据里读出栅格宽高,用于后续的像素遍历和画布创建。

解析地理参考信息(TiePoints + PixelScale)

1
2
3
4
5
const tiePoints = image.getTiePoints();
const scale = image.getFileDirectory().ModelPixelScale;
const originX = tiePoints[0].x;
const originY = tiePoints[0].y;
const [pixelSizeX, pixelSizeY] = scale;
  • TiePoints:一对或多对像素坐标 ↔ 地理坐标(通常只有一对),告诉我们影像左上角(或某个像素)的地图投影坐标。
  • ModelPixelScale:每个像素在投影坐标系下的真实长度(X、Y 方向)。
  • 由此,就能算出整张影像在投影坐标系下的边界:
    - 左下角 (minX, minY)
    - 右上角 (maxX, maxY)

读取投影(EPSG)信息

1
2
3
const geoKeys = image.getGeoKeys();
const epsgCode = geoKeys.ProjectedCSTypeGeoKey || geoKeys.GeographicTypeGeoKey;
const sourceCrs = `EPSG:${epsgCode}`;
  • GeoKeys 中存着 TIFF 里记录的投影代码,比如 3857、4326 等。
  • 拼成 EPSG:XXXX 字符串,方便后续用 proj4 做坐标转换。

投影坐标 → 地理坐标(经纬度)

1
2
3
const transformer = proj4(sourceCrs, "EPSG:4326");
const [minLon, minLat] = transformer.forward([minX, minY]);
const [maxLon, maxLat] = transformer.forward([maxX, maxY]);
  • proj4 在不同 CRS(如 WebMercator、UTM)之间无缝投影转换。
  • 将四个角的投影坐标转换到 WGS84(EPSG:4326)下的经纬度。

读取像素栅格并准备渲染

1
2
3
4
const rasters = await image.readRasters({ interleave: true });
const rasterData = rasters instanceof Uint8ClampedArray
? rasters
: new Uint8ClampedArray(rasters as unknown as Uint8Array);
  • readRasters({ interleave: true }) 会一次性拿到所有波段数据并按 RGBA、灰度等格式排列。
  • Canvas 的 ImageData 要求底层数据是 Uint8ClampedArray,所以这里做了类型兼容转换。

在 Canvas 上绘制栅格

1
2
3
4
5
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
const imgData = ctx.createImageData(width, height);
imgData.data.set(rasterData);
ctx.putImageData(imgData, 0, 0);
  • createCanvas 根据影像尺寸新建内存画布。
  • createImageData 新建一个空白的像素缓冲区,data.set(...) 将栅格数据写入,再一口气绘制到画布。

导出为图片文件

1
2
3
const outPath = filePath.replace(/\.(tif|tiff)$/i, ".png");
fs.writeFileSync(outPath, canvas.toBuffer("image/png"));
fs.writeFileSync(outPath, webpBuffer);
  • canvas.toBuffer('image/png') 会调用底层的图像库(如 libvips 或 Cairo)将画布内容编码为 PNG 格式。
  • 最后写磁盘生成 .png 文件。

返回综合元信息

1
2
3
4
5
6
7
8
9
return {
width,
height,
sourceCrs,
bboxSource: { minX, minY, maxX, maxY },
bboxLatLng: { minLat, minLon, maxLat, maxLon },
geoKeys,
webp: outPath,
};
  • 将所有关键信息(原始投影坐标边界、地理坐标边界、输出文件路径等)打包返回,方便后续自动化流水线使用或记录。

核心思路

  1. 解析 TIFF 元数据 获取投影、地理定位信息(TiePoints + Scale + GeoKeys);
  2. 转换坐标 得到人类易读的经纬度;
  3. 读取并可视化影像 构建 Canvas,渲染成标准图像格式;
  4. 输出文件和元数据 供后续使用。

这样,你就能在纯 Node.js 环境下,实现对任何 GeoTIFF 文件的“读投影、算范围、出图片”这一整套流程。


完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/*
* Node.js script to read a GeoTIFF (.tif/.tiff), extract its projection,
* compute geographic bounding box, and render the raster to a PNG image.
*
* Dependencies:
* npm install geotiff proj4 canvas
*/

import fs from 'fs';
import * as GeoTIFF from 'geotiff';
import proj4 from 'proj4';
import { createCanvas } from 'canvas';

async function parseGeoTIFF(filePath: string) {
// 1. Read file into buffer
const buffer = fs.readFileSync(filePath);

// 2. Parse the GeoTIFF
const tiff = await GeoTIFF.fromArrayBuffer(buffer.buffer);
const image = await tiff.getImage();

// 3. Image dimensions
const width = image.getWidth();
const height = image.getHeight();

// 4. Geo tie points & pixel scale
const tiePoints = image.getTiePoints();
const scale = image.getFileDirectory().ModelPixelScale;
if (!tiePoints.length || !scale) {
throw new Error('Missing geo-referencing tags (tie points or pixel scale)');
}
const { x: originX, y: originY } = tiePoints[0];
const [pixelSizeX, pixelSizeY] = scale;

// 5. Compute bounding box in source CRS
const minX = originX;
const maxY = originY;
const maxX = originX + width * pixelSizeX;
const minY = originY - height * pixelSizeY;

// 6. Projection info (EPSG code)
const geoKeys = image.getGeoKeys();
const epsgCode = geoKeys.ProjectedCSTypeGeoKey || geoKeys.GeographicTypeGeoKey;
const sourceCrs = EPSG:${epsgCode};

// 7. Transform corners to WGS84 (lat/lon)
const transformer = proj4(sourceCrs, 'EPSG:4326');
const [minLon, minLat] = transformer.forward([minX, minY]);
const [maxLon, maxLat] = transformer.forward([maxX, maxY]);

// 8. Read raster data (interleaved)
const rastersRaw = await image.readRasters({ interleave: true });
// Convert to Uint8ClampedArray for ImageData
const rasterData =
rastersRaw instanceof Uint8ClampedArray
? rastersRaw
: new Uint8ClampedArray(rastersRaw as any);

// 9. Render to canvas
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(width, height);
imgData.data.set(rasterData);
ctx.putImageData(imgData, 0, 0);

// 10. Save PNG
const outPath = filePath.replace(/\.(tif|tiff)$/i, '.png');
fs.writeFileSync(outPath, canvas.toBuffer('image/png'));

// 11. Return metadata
return {
width,
height,
sourceCrs,
bboxSource: { minX, minY, maxX, maxY },
bboxLatLng: { minLat, minLon, maxLat, maxLon },
geoKeys,
png: outPath
};
}

// Example usage:
(async () => {
try {
const result = await parseGeoTIFF(process.argv[2]);
console.log('Parsed GeoTIFF metadata:');
console.log(JSON.stringify(result, null, 2));
} catch (err) {
console.error('Error parsing GeoTIFF:', err);
}
})();