javascriptimagemapboxmapbox-gl-jsheightmap

How do you convert a 32bit RGB png with encoded height values to 16 bit png using browser side javascript?


I am trying to create a 16bit heightmap png from a 32bit Rgb encoded height value png.

According to mapbox I can decode their png pixel values to height values in meters with this formula. height = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1)

I am new to understanding this type of information and have tried to figure it out myself but I need some help.

I found this program called image-js, docs for this program are here . Note I am using this in the browser not with node.

I think it has the abilities that I need. However, I am really not sure how to get the data from the original image and create a new 16bit png with the new height values calculated from the above formula.

If you look in the docs under image I can set many properties such as

bitDepth and set that to 16

getPixelsArray() this function will give me an array of pixels values in the form of r,g,b. I think I can then run each pixel through the above formula to get the height. After that how do I turn this height data back into a 16bit grayscale heightmap png?

Thanks!

Example images. 32bit Rgb encoded height value png enter image description here

16 bit heightmap png. You have to zoom in to see the image changes enter image description here

Update:

Thanks to traktor for the suggestion. I wanted to add some information to my question.

I don't necessarily need to use image-js. My main goal is to get a 16 bit heightmap from a 32 bit RGB. So any other browser side javscript method would be fine. I also am using browser side createWritable(); stream to write file to disk.

I have a method used from this nice module https://github.com/colkassad/terrain-rgb-height however I cannot get the browser side version of pngjs to work correctly. I think there is a difference between browser side stream reader/writers and node reader/writers that make it not work.

Thanks!


Solution

  • Looking through the source code for Image-js package on GitHub turns up that the options object used in calls to the library is verified in source file kind.js which enumerates supported options properties in the assignment statement:

    const { components, alpha, bitDepth, colorModel } = definition;
    

    However, many options are defaulted using kind option property, which itself defaults to "RGBA".

    Knowing more about option properties allows using them (I couldn't find options documentation outside of the code).

    I would suggest

    1. Create an Image-js image from the 32bit encoded height png. Omit a kind property to use the default 32bit PNG pixel model of 3 color channels plus one alpha channel.

    2. Convert the image data (held as a typed array in the image object's data property) to a single dimensional array of height values using the MapBox conversion algorithm. The layout of the (image.data) typed array appears to be the same as that used for ImageData

    3. Create a new image-js image using

      new Image(width, height, decodedHeightArray, {kind="GREY", bitDepth:16})
      

      where decodedHeightArray is the array prepared in the previous step.

    Just adding my code to what tracktor's answer provided Thank you very much traktor

    This worked perfectly. For anyone who wants the code

    I am using image-js to encode and decode the image

     let file_handleSixteen = await dir_handle.getFileHandle(sixteen_file_name, {create: true})
    
     let writableSixteen = await file_handleSixteen.createWritable();
    
    
    async function convert16(arrayBuff) {
    
      let image = await Image.load(arrayBuff);
    
      let width = image.width
      let height = image.height
    
      let decodedHeightArray = []
      let pixelsArray = image.getPixelsArray()
      for (const pixel of pixelsArray) {
        let r = pixel[0]
        let g = pixel[1]
        let b = pixel[2]
        let height = getHeightFromRgb(r, g, b);
        decodedHeightArray.push(height)
      }
      let newImage = new Image(width, height, decodedHeightArray, {kind: "GREY", bitDepth: 16})
      return newImage.toBlob()
    }
    
    function getHeightFromRgb(r, g, b) {
      return -10000 + ((r * 256 * 256 + g * 256 + b) * 0.1);
    }
    
    
      const file = await file_handleRgb.getFile();
      let imageBuffer = await file.arrayBuffer()
      let convertedArray = await convert16(imageBuffer)
    
     await writableSixteen.write(convertedArray)
     await writableSixteen.close();
    

    I am also using the browser streams api to write the file to disk. Note the filestreams writable.write only accepts certain values one of them being a Blob that is why converted it to Blob before passing it to the write method