16 bit to 8 bit RGB with PDAL and Python

Sometimes LAS files have RGB values stored as 16 bit colours. This currently upsets the Potree viewer, and maybe some other things.  following recipe using Python and PDAL makes your colours play nicely.

The files I was given used 16 bit unsigned integers (0 – 65535) to hold RGB information – I needed those numbers to be scaled to fit an 8 bit unsigned integer (0-255) range. The basic function is ‘divide by 255, remove 1’.

Interestingly, ASPRS LAS specifications 1.1 through 1.4 use ‘unsigned short’ as a data type for storing RGB in all point formats. The data I was using reported LAS 1.2 point format 3, but had unsigned long RGB values – I’ll raise that with the provider. Thankfully a workaround is short – and provides a topic for this post!

As a quick refresh, PDAL uses readers, filters and writers to perform operations on point cloud data. These are ‘stages’ of operation. Operations, in particular filters, can be chained together to build ‘pipelines’ representing your point cloud work flows.

Here, I describe a workflow to read .LAS format files, apply a python function to some of it’s dimensions, and write the results to a compressed LAZ format file.

First, I need to create a python function to scale colours. Note – this may produce floating point values, but PDAL seems to wisely deal with them.

import numpy as np

def eightbitify(colour):
    A function to ingest an array of numbers, and if they are greater than zero
    divide by 255 and subtract 1.
    notzero = np.where(colour > 0)
    colour[notzero] = (colour[notzero]/255) - 1
    return colour

def scale_colour(ins,outs):
    A function called by PDAL to apply eightbitify to specified point dimensions
     - in this case red, green and blue. Return the result to the PDAL point view.
    outs['Red'] = eightbitify(ins['Red'])
    outs['Green'] = eightbitify(ins['Green'])
    outs['Blue'] = eightbitify(ins['Blue'])
    return True

A future me might write in something to ensure integers are returned, say return np.floor(colour)  as the output of eightbitify().

We apply this operation using PDAL’s filters.python – invoked with a pipeline operation. Here is the JSON configuration for the pipeline:

    "pipeline": [
            "type" : "readers.las",
            "filename" : "file.las"
            "type" : "filters.python",
            "script": "/opt/data/scalecolour.py",
            "function": "scale_colour",
            "module": "scalecolour"
            "type" : "writers.las",
            "filename" : "outfile.laz"

…and finally, here is the the PDAL invocation to make it go, using the pdal/pdal:latest docker image.:

docker run -it -v /path/to/workspace:/opt/data \
                pdal/pdal pdal \
                pipeline /opt/data/scale_colour.json \
                --readers.las.filename=/opt/data/infile.las \

Finally, I wrapped it in a shell script to process a bunch of files, with the PDAL bits in lines 6-10 (highlighted). It loops over a list of files with the .las extension, passes their name to the pdal readers.las (useful trick! override pipeline directives using command line parameters!); and constructs a .laz filename to pass to writers.las.


for f in lasfiles/*.las;
    #echo $f
    fileout=$(basename $f ".las")
    #echo $fileout
    docker run -it -v /path/to/workspace:/opt/data \
                    pdal/pdal pdal \
                    pipeline /opt/data/scale_colour.json \
                    --readers.las.filename=/opt/data/${f} \


…and we’re done. Hopefully a whole directory of compressed LAZ files with 8 bit RGB values is ours for the taking now!