geotiffjs / geotiff.js

geotiff.js is a small library to parse TIFF files for visualization or analysis. It is written in pure JavaScript, and is usable in both the browser and node.js applications.
https://geotiffjs.github.io/
MIT License
872 stars 183 forks source link

Support for webp compression #154

Open robin-snt opened 4 years ago

robin-snt commented 4 years ago

Seems like a really useful format for quicklooks and presentational purposes.

https://medium.com/@_VincentS_/do-you-really-want-people-using-your-data-ec94cd94dc3f

constantinius commented 4 years ago

Hi @robin-snt

Thanks for the notice. I already had WebP on the radar, unfortunately there does not seem to be a decoder library I can use as a dependency to decode WebP images. All the stuff on NPM is either really dubious or uses C dependencies which do not work well browsers at the moment.

Unless someone is willing to investigate or has a good alternative this will have to wait.

robin-snt commented 4 years ago

Fully understand. Thanks for supporting a really nice library btw.

robin-snt commented 4 years ago

This seems like a really good case for WASM tough.

constantinius commented 4 years ago

This seems like a really good case for WASM tough.

Agreed. Unfortunately I have very limited experience with both development and support of wasm.

marty-sullivan commented 3 years ago

+1 for WebP support, this lib would allow me to get rid of some server-side items. WebP is truly superior to all other compression options for Cloud Optimized GeoTIFFs.

I'm wondering if, at the very least, can tiles be pulled and decoded by.the browser? WebP support is pretty much complete in all major browsers.

Of course, I don't really understand the intricacies of GeoTiff decoding, so I'm guessing that's not possible

constantinius commented 3 years ago

Hi @marty-sullivan

This is actually hard to say! One problem is that WebP inside TIFF is not standardized at all, unlike e.g JPEG compressed images.

With JPEG the answer was: it depends. In some images you could just take the raw tile and interpret it as a standalone JPEG, but if some of the information is actually stored in TIFF Tags instead (like https://www.awaresystems.be/imaging/tiff/tifftags/jpegtables.html) this would not be possible.

I simply don't know if WebP in TIFF uses any additional Tags.

If you have an image, you could actually try this. Here is some (untested, pseudo-) code to do this:

const tiff = await fromUrl(sourceUrl);
const image = await tiff.getImage();

const rawData = await image.getTileOrStrip(0, 0); // this is now an ArrayBuffer

// create a Blob to later read from
const blob = new Blob([rawData]);

// get the image from the HTML document to load the WebP into
const htmlImage = document.getElementById('image'); 

// construct a filereader to read the Blob as a file
const reader = new FileReader();
reader.onload = function(e) {
   htmlImage.src = e.target.result;
};
reader.readAsDataURL(blob);
marty-sullivan commented 3 years ago

Hi @constantinius

This does seem like a cool experiment anyway :)

I am able to load an example WebP GeoTIFF and it seems like it is able to read the metadata fine. I see all the expected tiling information in the Image object.

However, when I try to run await image.getTileOrStrip(0, 0); I get the following stack trace:

Uncaught (in promise) TypeError: Cannot read property 'decode' of undefined
    at e.<anonymous> (geotiffimage.js:265)
    at l (runtime.js:45)
    at Generator._invoke (runtime.js:274)
    at Generator.forEach.e.<computed> [as next] (runtime.js:97)
    at n (asyncToGenerator.js:3)
    at s (asyncToGenerator.js:25)

After looking at geotiffimage.js, I I have a few ideas of what is happening here but what is your take?

constantinius commented 3 years ago

@marty-sullivan

Ah! Of course, I can't just drop half of the parameters and then expect it to work 🙄

I also realized, that getTileOrStrip will try to uncompress the image so this is also not the right function to call anyhow. So we need to go deeper and access the file directly.

This is an updated example, assuming that all samples/channels are interlaced and not separate:

const tiff = await fromUrl(sourceUrl);
const image = await tiff.getImage();

// ---- start new stuff
const index = (y * numTilesPerRow) + x;
let offset;
let byteCount;
if (image.isTiled) {
  offset = image.fileDirectory.TileOffsets[index];
  byteCount = image.fileDirectory.TileByteCounts[index];
} else {
  offset = image.fileDirectory.StripOffsets[index];
  byteCount = image.fileDirectory.StripByteCounts[index];
}
const rawData = await image.source.fetch(offset, byteCount);
// ---- end new stuff

// create a Blob to later read from
const blob = new Blob([rawData]);

// get the image from the HTML document to load the WebP into
const htmlImage = document.getElementById('image'); 

// construct a filereader to read the Blob as a file
const reader = new FileReader();
reader.onload = function(e) {
   htmlImage.src = e.target.result;
};
reader.readAsDataURL(blob);

Let me know if this worked!

marty-sullivan commented 3 years ago

@constantinius

It worked!

btw, the GeoTIFF I'm using is generated by GDAL 3.2 as a COG (Cloud Optimized GeoTIFF) using the GoogleMapsCompatible option and WEBP compression (lossy). If you were to choose a WEBP standard, I'd say this would be a good one.

Here's the final code, just some minor tweaks from what you had:

const tiff = await GeoTIFF.fromUrl('/my_geotiff.tif');

const image = await tiff.getImage();

const numTilesPerRow = Math.ceil(image.getWidth() / image.getTileWidth());
const numTilesPerCol = Math.ceil(image.getHeight() / image.getTileHeight());

const x = 0;
const y = 0;
const index = (y * numTilesPerRow) + x;

let offset;
let byteCount;

offset = image.fileDirectory.TileOffsets[index];
byteCount = image.fileDirectory.TileByteCounts[index];

const rawData = await image.source.fetch(offset, byteCount);
const blob = new Blob([rawData]);
const reader = new FileReader()

const html_image = document.getElementById('webpimg');

reader.onload = function(e) {
  var b64_data = 'data:image/webp;' + e.target.result.split(';')[1];
  html_image.src = b64_data;
}

reader.readAsDataURL(blob);
constantinius commented 3 years ago

Awesome!

Would you share your image? Or the GDAL commands to create it?

As this seems to be a viable way to decode WebP images in the Browser, I'm thinking of a way to make this re-usable and plug this in to the normal reading process (at least for Browsers).

marty-sullivan commented 3 years ago

Sure, I create two formats of these COGs, one in the native projection and another in web mercator to be compatible with the various map SDKs out there (GoogleMapsCompatible). QUALITY can be set to 100 to enable LOSSLESS mode for WEBP.

Here is the Python GDAL command I use to create the COG in "GoogleMapsCompatible" scheme:

gdal.Translate(
  destName=mercator_tiled_path,
  srcDS=geotiff_path,
  format='COG',
  creationOptions=[
    'COMPRESS=WEBP',
    'QUALITY=95',
    'NUM_THREADS=ALL_CPUS',
    'RESAMPLING=NEAREST',
    'OVERVIEWS=AUTO',
    'TILING_SCHEME=GoogleMapsCompatible',
    'ZOOM_LEVEL_STRATEGY=UPPER',
  ],
)

and the native projection:

gdal.Translate(
  destName=native_tiled_path,
  srcDS=geotiff_path,
  format='COG',
  creationOptions=[
    'COMPRESS=WEBP',
    'QUALITY=95',
    'NUM_THREADS=ALL_CPUS',
    'RESAMPLING=NEAREST',
    'OVERVIEWS=AUTO',
    'TILING_SCHEME=CUSTOM',
    'ZOOM_LEVEL_STRATEGY=UPPER',
  ]
)
constantinius commented 3 years ago

@marty-sullivan

I created a PR for this: #194

Can you please test this and let me know if this works for you?

Caveat: it only works in the browser that support WebP

marty-sullivan commented 3 years ago

I'm happy to test but I don't have a good environment to build JS modules from source at the moment. Can you provide a CDN link or built code for this branch to work with?

constantinius commented 3 years ago

@marty-sullivan There you go: geotiffjs-webp.zip

constantinius commented 3 years ago

@marty-sullivan did you have a chance to test?

marty-sullivan commented 3 years ago

@constantinius I did quickly test the other day and was going to return to it but I think I ran into the same issue as above await image.getTileOrStrip(0, 0) results in TypeError: Cannot read property 'decode' of undefined when I expected the tile to be returned.

I could be doing something different than you expect though. If this behavior is expected, what would your recommended test be?

constantinius commented 3 years ago

@marty-sullivan No, that is not expected. Do you have a complete stacktrace for that?

marty-sullivan commented 3 years ago

@constantinius It's the same trace as from the above trace when I first tried it. I just went through and confirmed that webimage.js is available and that the compression code you're looking for matches that in my geotiff (50001) so it should be calling the right decoder.

I'm mainly testing using Chrome 88.0.4324.96, but also tried in Firefox 85.0.1 and got a similar, but slightly different trace:

Uncaught (in promise) TypeError: n is undefined
    e geotiffimage.js:264
    h runtime.js:45
    _invoke runtime.js:274
    r runtime.js:97
    Babel 2
        n
        c
constantinius commented 3 years ago

@marty-sullivan

I think the issue is that no decoder is passed to the function.

import WebImageDecoder from 'geotiff/compression/webimage';

// ...

await image.getTileOrStrip(0, 0, 0, new WebImageDecoder());

Would you mind testing this?

sguimmara commented 1 year ago

Is there any update on this issue ?

@constantinius Does your solution work for any COG produced by GDAL ?