Skip to content

Commit

Permalink
fix: validate band information is consistent (#1017)
Browse files Browse the repository at this point in the history
#### 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
  • Loading branch information
blacha authored Jul 23, 2024
1 parent 0338068 commit ec64f65
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Size, Source, Tiff, TiffImage } from '@cogeotiff/core';
import { SampleFormat, Size, Source, Tiff, TiffImage, TiffTag } from '@cogeotiff/core';

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

Expand Down Expand Up @@ -29,7 +29,18 @@ export class FakeCogTiff extends Tiff {
image: Partial<{ origin: number[]; epsg: number; resolution: number[]; size: Size; isGeoLocated: boolean }>,
) {
super({ url: new URL(uri) } as Source);
this.images = [{ ...structuredClone(DefaultTiffImage), valueGeo, ...image } as any];
this.images = [
{
...structuredClone(DefaultTiffImage),
valueGeo,
...image,
fetch(tag: TiffTag) {
if (tag === TiffTag.BitsPerSample) return [8, 8, 8];
if (tag === TiffTag.SampleFormat) return [SampleFormat.Uint, SampleFormat.Uint, SampleFormat.Uint];
return null;
},
} as any,
];
}

static fromTileName(tileName: string): FakeCogTiff {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,18 @@ describe('getTileName', () => {
assert.equal(convertTileName('CK08_50000_0101', 50000), 'CK08');
});

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

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

describe('tiffLocation', () => {
Expand Down
47 changes: 45 additions & 2 deletions src/commands/tileindex-validate/tileindex.validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { boolean, command, flag, number, option, optional, restPositionals, stri
import { CliInfo } from '../../cli.info.js';
import { logger } from '../../log.js';
import { isArgo } from '../../utils/argo.js';
import { extractBandInformation } from '../../utils/band.js';
import { FileFilter, getFiles } from '../../utils/chunk.js';
import { findBoundingBox } from '../../utils/geotiff.js';
import { GridSize, GridSizes, MapSheet, MapSheetTileGridSize, SheetRanges } from '../../utils/mapsheet.js';
Expand Down Expand Up @@ -276,7 +277,11 @@ export const commandTileIndexValidate = command({
for (const val of outputs.values()) {
if (val.length < 2) continue;
if (args.retile) {
logger.info({ tileName: val[0]?.tileName, uris: val.map((v) => v.source) }, 'TileIndex:Retile');
const bandType = validateConsistentBands(val);
logger.info(
{ tileName: val[0]?.tileName, uris: val.map((v) => v.source), bands: bandType },
'TileIndex:Retile',
);
} else {
retileNeeded = true;
logger.error({ tileName: val[0]?.tileName, uris: val.map((v) => v.source) }, 'TileIndex:Duplicate');
Expand All @@ -300,6 +305,31 @@ export const commandTileIndexValidate = command({
},
});

/**
* Validate all tiffs have consistent band information
* @returns list of bands in the first image if consistent with the other images
* @throws if one image does not have consistent band information
*/
function validateConsistentBands(locs: TiffLocation[]): string[] {
const firstBands = locs[0]?.bands ?? [];
const firstBand = firstBands.join(',');

for (let i = 1; i < locs.length; i++) {
const currentBands = locs[i]?.bands.join(',');

// If the current image doesn't have the same band information gdalbuildvrt will fail
if (currentBands !== firstBand) {
// Dump all the imagery and their band types into logs so it can be debugged later
for (const v of locs) {
logger.error({ path: v.source, bands: v.bands.join(',') }, 'TileIndex:Bands:Heterogenous');
}

throw new Error(`heterogenous bands: ${currentBands} vs ${firstBand} from: ${locs[0]?.source}`);
}
}
return firstBands;
}

export function groupByTileName(tiffs: TiffLocation[]): Map<string, TiffLocation[]> {
const duplicates: Map<string, TiffLocation[]> = new Map();
for (const loc of tiffs) {
Expand All @@ -319,6 +349,12 @@ export interface TiffLocation {
epsg?: number | null;
/** Output tile name */
tileName: string;
/**
* List of bands inside the tiff in the format `uint8` `uint16`
*
* @see {@link extractBandInformation} for more information on bad types
*/
bands: string[];
}

/**
Expand Down Expand Up @@ -368,7 +404,13 @@ export async function extractTiffLocations(
// // Also need to allow for ~1.5cm of error between bounding boxes.
// // assert bbox == MapSheet.getMapTileIndex(tileName).bbox
// }
return { bbox, source: tiff.source.url.href, tileName, epsg: tiff.images[0]?.epsg };
return {
bbox,
source: tiff.source.url.href,
tileName,
epsg: tiff.images[0]?.epsg,
bands: await extractBandInformation(tiff),
};
} catch (e) {
logger.error({ reason: e, source: tiff.source }, 'ExtractTiffLocation:Failed');
return null;
Expand All @@ -386,6 +428,7 @@ export async function extractTiffLocations(

return output;
}

export function getSize(extent: [number, number, number, number]): Size {
return { width: extent[2] - extent[0], height: extent[3] - extent[1] };
}
Expand Down
19 changes: 19 additions & 0 deletions src/utils/__test__/band.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import assert from 'node:assert';
import { describe, it } from 'node:test';

import { createTiff } from '../../commands/common.js';
import { extractBandInformation } from '../band.js';

describe('extractBandInformation', () => {
it('should extract basic band information (8-bit)', async () => {
const testTiff = await createTiff('./src/commands/tileindex-validate/__test__/data/8b.tiff');
const bands = await extractBandInformation(testTiff);
assert.equal(bands.join(','), 'uint8,uint8,uint8');
});

it('should extract basic band information (16-bit)', async () => {
const testTiff = await createTiff('./src/commands/tileindex-validate/__test__/data/16b.tiff');
const bands = await extractBandInformation(testTiff);
assert.equal(bands.join(','), 'uint16,uint16,uint16');
});
});
56 changes: 56 additions & 0 deletions src/utils/band.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { SampleFormat, Tiff, TiffImage, TiffTag } from '@cogeotiff/core';

function getDataType(i: SampleFormat): string {
switch (i) {
case SampleFormat.Uint:
return 'uint';
case SampleFormat.Int:
return 'int';
case SampleFormat.Float:
return 'float';
case SampleFormat.Void:
return 'void';
case SampleFormat.ComplexFloat:
return 'cfloat';
case SampleFormat.ComplexInt:
return 'cint';
default:
return 'unknown';
}
}
/**
* Load the band information from a tiff and return it as a array of human friendly names
*
* @example
* `[uint16, uint16, uint16]` 3 band uint16
*
* @param tiff Tiff to extract band information from
* @returns list of band information
* @throws {Error} if cannot extract band information
*/
export async function extractBandInformation(tiff: Tiff): Promise<string[]> {
const firstImage = tiff.images[0] as TiffImage;

const [dataType, bitsPerSample] = await Promise.all([
/** firstImage.fetch(TiffTag.Photometric), **/ // TODO enable RGB detection
firstImage.fetch(TiffTag.SampleFormat),
firstImage.fetch(TiffTag.BitsPerSample),
]);

if (bitsPerSample == null) {
throw new Error(`Failed to extract band information from: ${tiff.source.url.href}`);
}

if (dataType && dataType.length !== bitsPerSample.length) {
throw new Error(`Datatype and bits per sample miss match: ${tiff.source.url.href}`);
}

const imageBands: string[] = [];
for (let i = 0; i < bitsPerSample.length; i++) {
const type = getDataType(dataType ? (dataType[i] as SampleFormat) : SampleFormat.Uint);
const bits = bitsPerSample[i];
imageBands.push(`${type}${bits}`);
}

return imageBands;
}

0 comments on commit ec64f65

Please sign in to comment.