Skip to content

Commit ec64f65

Browse files
authored
fix: validate band information is consistent (#1017)
#### Motivation When tiffs are retiled they need to have consistent banding information or GDAL will not be able to join them together easily #### Modification When a retile is requested validate that all tiffs in the retile have the same consistent band information. #### Checklist _If not applicable, provide explanation of why._ - [ ] Tests updated - [ ] Docs updated - [ ] Issue linked in Title
1 parent 0338068 commit ec64f65

File tree

5 files changed

+141
-10
lines changed

5 files changed

+141
-10
lines changed

src/commands/tileindex-validate/__test__/tileindex.validate.data.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Size, Source, Tiff, TiffImage } from '@cogeotiff/core';
1+
import { SampleFormat, Size, Source, Tiff, TiffImage, TiffTag } from '@cogeotiff/core';
22

33
import { MapSheet } from '../../../utils/mapsheet.js';
44

@@ -29,7 +29,18 @@ export class FakeCogTiff extends Tiff {
2929
image: Partial<{ origin: number[]; epsg: number; resolution: number[]; size: Size; isGeoLocated: boolean }>,
3030
) {
3131
super({ url: new URL(uri) } as Source);
32-
this.images = [{ ...structuredClone(DefaultTiffImage), valueGeo, ...image } as any];
32+
this.images = [
33+
{
34+
...structuredClone(DefaultTiffImage),
35+
valueGeo,
36+
...image,
37+
fetch(tag: TiffTag) {
38+
if (tag === TiffTag.BitsPerSample) return [8, 8, 8];
39+
if (tag === TiffTag.SampleFormat) return [SampleFormat.Uint, SampleFormat.Uint, SampleFormat.Uint];
40+
return null;
41+
},
42+
} as any,
43+
];
3344
}
3445

3546
static fromTileName(tileName: string): FakeCogTiff {

src/commands/tileindex-validate/__test__/tileindex.validate.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,18 @@ describe('getTileName', () => {
5353
assert.equal(convertTileName('CK08_50000_0101', 50000), 'CK08');
5454
});
5555

56-
for (const sheet of MapSheetData) {
57-
it('should get the top left 1:50k, 1:10k, 1:5k, 1:1k, and 1:500 for ' + sheet.code, () => {
56+
it('should get the top left 1:50k, 1:10k, 1:5k, 1:1k, and 1:500 for all sheets', () => {
57+
for (const sheet of MapSheetData) {
5858
assert.equal(getTileName(sheet.origin.x, sheet.origin.y, 50000), sheet.code);
5959
assert.equal(getTileName(sheet.origin.x, sheet.origin.y, 10000), sheet.code + '_10000_0101');
6060
assert.equal(getTileName(sheet.origin.x, sheet.origin.y, 5000), sheet.code + '_5000_0101');
6161
assert.equal(getTileName(sheet.origin.x, sheet.origin.y, 1000), sheet.code + '_1000_0101');
6262
assert.equal(getTileName(sheet.origin.x, sheet.origin.y, 500), sheet.code + '_500_001001');
63-
});
63+
}
64+
});
6465

65-
it('should get the bottom right 1:50k, 1:10k, 1:5k, 1:1k for ' + sheet.code, () => {
66+
it('should get the bottom right 1:50k, 1:10k, 1:5k, 1:1k for all sheets', () => {
67+
for (const sheet of MapSheetData) {
6668
// for each scale calculate the bottom right tile then find the mid point of it
6769
// then look up the tile name from the midpoint and ensure it is the same
6870
for (const scale of [10_000, 5_000, 1_000, 500] as const) {
@@ -74,8 +76,8 @@ describe('getTileName', () => {
7476
const midPointY = ret.origin.y - ret.height / 2;
7577
assert.equal(getTileName(midPointX, midPointY, scale), sheetName);
7678
}
77-
});
78-
}
79+
}
80+
});
7981
});
8082

8183
describe('tiffLocation', () => {

src/commands/tileindex-validate/tileindex.validate.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { boolean, command, flag, number, option, optional, restPositionals, stri
66
import { CliInfo } from '../../cli.info.js';
77
import { logger } from '../../log.js';
88
import { isArgo } from '../../utils/argo.js';
9+
import { extractBandInformation } from '../../utils/band.js';
910
import { FileFilter, getFiles } from '../../utils/chunk.js';
1011
import { findBoundingBox } from '../../utils/geotiff.js';
1112
import { GridSize, GridSizes, MapSheet, MapSheetTileGridSize, SheetRanges } from '../../utils/mapsheet.js';
@@ -276,7 +277,11 @@ export const commandTileIndexValidate = command({
276277
for (const val of outputs.values()) {
277278
if (val.length < 2) continue;
278279
if (args.retile) {
279-
logger.info({ tileName: val[0]?.tileName, uris: val.map((v) => v.source) }, 'TileIndex:Retile');
280+
const bandType = validateConsistentBands(val);
281+
logger.info(
282+
{ tileName: val[0]?.tileName, uris: val.map((v) => v.source), bands: bandType },
283+
'TileIndex:Retile',
284+
);
280285
} else {
281286
retileNeeded = true;
282287
logger.error({ tileName: val[0]?.tileName, uris: val.map((v) => v.source) }, 'TileIndex:Duplicate');
@@ -300,6 +305,31 @@ export const commandTileIndexValidate = command({
300305
},
301306
});
302307

308+
/**
309+
* Validate all tiffs have consistent band information
310+
* @returns list of bands in the first image if consistent with the other images
311+
* @throws if one image does not have consistent band information
312+
*/
313+
function validateConsistentBands(locs: TiffLocation[]): string[] {
314+
const firstBands = locs[0]?.bands ?? [];
315+
const firstBand = firstBands.join(',');
316+
317+
for (let i = 1; i < locs.length; i++) {
318+
const currentBands = locs[i]?.bands.join(',');
319+
320+
// If the current image doesn't have the same band information gdalbuildvrt will fail
321+
if (currentBands !== firstBand) {
322+
// Dump all the imagery and their band types into logs so it can be debugged later
323+
for (const v of locs) {
324+
logger.error({ path: v.source, bands: v.bands.join(',') }, 'TileIndex:Bands:Heterogenous');
325+
}
326+
327+
throw new Error(`heterogenous bands: ${currentBands} vs ${firstBand} from: ${locs[0]?.source}`);
328+
}
329+
}
330+
return firstBands;
331+
}
332+
303333
export function groupByTileName(tiffs: TiffLocation[]): Map<string, TiffLocation[]> {
304334
const duplicates: Map<string, TiffLocation[]> = new Map();
305335
for (const loc of tiffs) {
@@ -319,6 +349,12 @@ export interface TiffLocation {
319349
epsg?: number | null;
320350
/** Output tile name */
321351
tileName: string;
352+
/**
353+
* List of bands inside the tiff in the format `uint8` `uint16`
354+
*
355+
* @see {@link extractBandInformation} for more information on bad types
356+
*/
357+
bands: string[];
322358
}
323359

324360
/**
@@ -368,7 +404,13 @@ export async function extractTiffLocations(
368404
// // Also need to allow for ~1.5cm of error between bounding boxes.
369405
// // assert bbox == MapSheet.getMapTileIndex(tileName).bbox
370406
// }
371-
return { bbox, source: tiff.source.url.href, tileName, epsg: tiff.images[0]?.epsg };
407+
return {
408+
bbox,
409+
source: tiff.source.url.href,
410+
tileName,
411+
epsg: tiff.images[0]?.epsg,
412+
bands: await extractBandInformation(tiff),
413+
};
372414
} catch (e) {
373415
logger.error({ reason: e, source: tiff.source }, 'ExtractTiffLocation:Failed');
374416
return null;
@@ -386,6 +428,7 @@ export async function extractTiffLocations(
386428

387429
return output;
388430
}
431+
389432
export function getSize(extent: [number, number, number, number]): Size {
390433
return { width: extent[2] - extent[0], height: extent[3] - extent[1] };
391434
}

src/utils/__test__/band.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import assert from 'node:assert';
2+
import { describe, it } from 'node:test';
3+
4+
import { createTiff } from '../../commands/common.js';
5+
import { extractBandInformation } from '../band.js';
6+
7+
describe('extractBandInformation', () => {
8+
it('should extract basic band information (8-bit)', async () => {
9+
const testTiff = await createTiff('./src/commands/tileindex-validate/__test__/data/8b.tiff');
10+
const bands = await extractBandInformation(testTiff);
11+
assert.equal(bands.join(','), 'uint8,uint8,uint8');
12+
});
13+
14+
it('should extract basic band information (16-bit)', async () => {
15+
const testTiff = await createTiff('./src/commands/tileindex-validate/__test__/data/16b.tiff');
16+
const bands = await extractBandInformation(testTiff);
17+
assert.equal(bands.join(','), 'uint16,uint16,uint16');
18+
});
19+
});

src/utils/band.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { SampleFormat, Tiff, TiffImage, TiffTag } from '@cogeotiff/core';
2+
3+
function getDataType(i: SampleFormat): string {
4+
switch (i) {
5+
case SampleFormat.Uint:
6+
return 'uint';
7+
case SampleFormat.Int:
8+
return 'int';
9+
case SampleFormat.Float:
10+
return 'float';
11+
case SampleFormat.Void:
12+
return 'void';
13+
case SampleFormat.ComplexFloat:
14+
return 'cfloat';
15+
case SampleFormat.ComplexInt:
16+
return 'cint';
17+
default:
18+
return 'unknown';
19+
}
20+
}
21+
/**
22+
* Load the band information from a tiff and return it as a array of human friendly names
23+
*
24+
* @example
25+
* `[uint16, uint16, uint16]` 3 band uint16
26+
*
27+
* @param tiff Tiff to extract band information from
28+
* @returns list of band information
29+
* @throws {Error} if cannot extract band information
30+
*/
31+
export async function extractBandInformation(tiff: Tiff): Promise<string[]> {
32+
const firstImage = tiff.images[0] as TiffImage;
33+
34+
const [dataType, bitsPerSample] = await Promise.all([
35+
/** firstImage.fetch(TiffTag.Photometric), **/ // TODO enable RGB detection
36+
firstImage.fetch(TiffTag.SampleFormat),
37+
firstImage.fetch(TiffTag.BitsPerSample),
38+
]);
39+
40+
if (bitsPerSample == null) {
41+
throw new Error(`Failed to extract band information from: ${tiff.source.url.href}`);
42+
}
43+
44+
if (dataType && dataType.length !== bitsPerSample.length) {
45+
throw new Error(`Datatype and bits per sample miss match: ${tiff.source.url.href}`);
46+
}
47+
48+
const imageBands: string[] = [];
49+
for (let i = 0; i < bitsPerSample.length; i++) {
50+
const type = getDataType(dataType ? (dataType[i] as SampleFormat) : SampleFormat.Uint);
51+
const bits = bitsPerSample[i];
52+
imageBands.push(`${type}${bits}`);
53+
}
54+
55+
return imageBands;
56+
}

0 commit comments

Comments
 (0)