← All guides

Basics API Photography

Bit depth, colour profiles and file formats

Bits per channel, transfer curves, colour profiles and phone HDR, what each format holds, when not to use auto, and what to keep for the web, print, an archive and science.

Two properties decide what an image file can still be used for once it is saved: how many levels each channel holds, and which colours those levels stand for. Neither shows until something goes wrong. A sky bands after an edit, or a red goes dull when the file is re-saved. This page covers both: what the terms mean, how phones store HDR, what each format can carry, what CraterView does with each on the way through and which output format to ask for, and what to keep for the web, print, an archive and scientific work.

Bit depth

Depth is counted per channel. An 8-bit RGB image holds 256 levels in each of red, green and blue. “24-bit colour” and “48-bit colour” are the same 8 and 16 bits counted across all three.

Bits per channel Levels per channel Where you meet it
8 256 JPEG, WebP, most PNGs, an iPhone’s HEIC; what an ordinary display shows
10 1,024 HEIF and AVIF from many cameras; HDR photographs and video
12–14 4,096–16,384 Camera raw files
16 65,536 Scanner TIFFs, 16-bit PNGs, exports from raw editors
32, floating point — HDR merges, stacked astronomical frames, instrument data

Eight bits is enough to show a finished image, and too few to keep editing one. The levels are spaced along a transfer curve that puts them where the eye can tell them apart, so a finished 8-bit picture looks smooth on an ordinary screen. Editing uses levels up. Stretch the darkest quarter of an 8-bit channel, codes 0–63, across the whole range, and 64 levels have to cover 256 codes. That leaves a step of four between neighbours, which a smooth sky or a shadow shows as bands. The same stretch on a 16-bit channel has 16,384 levels to spread. Edit at 16 bits and reduce to 8 once, at the end.

Three grey ramps. Stretched from 8 bits, the darkest sixteenth shows 16 hard bands and the darkest quarter 64 finer ones; stretched from 16 bits, the ramp is smooth.

Each strip is the darkest part of a grey ramp spread across the whole range, the way an edit lifts the shadows. From 8 bits the steps show. From 16 bits there are more levels than the screen can display, so none do.

Depth records what was captured, and cannot add to it. An 8-bit JPEG saved as a 16-bit PNG is still 256 levels in a 65,536-level container. The way to check is to count the distinct values a file actually holds, which the script under Checking a file below does.

Values are encoded, not proportional to light. A transfer curve spends more codes on the shadows, where the eye discriminates finely, and fewer on the highlights. It may be sRGB’s curve, a plain gamma of 1.8 or 2.2, or HDR’s PQ or HLG. Linear data, straight off a sensor or an instrument, has no such curve. That is why it needs 16 bits or floating point to look as smooth as an 8-bit encoded picture, and why it looks almost black when shown as it is.

Stored value against linear light for three transfer curves. Half the stored range, value 128, is reached at 50% of the light for linear data, 29% for gamma 1.8 and 22% for sRGB.

Half the stored range, value 128, covers the darkest 22% of the light in sRGB, 29% at gamma 1.8, and 50% in linear data. A curve gives the shadows more of the values.

32 evenly spaced stored values placed on a scale of linear light. Linear spaces them evenly; gamma 1.8 and sRGB crowd them into the shadows, sRGB more so.

The same values placed on a scale of light. A curve crowds them into the shadows, where the eye tells neighbouring tones apart most easily. Linear data spaces them evenly, which leaves the shadows short of levels.

Colour profiles

A pixel value names a position inside a colour space, not a colour. (255, 0, 0) means “the most saturated red this space has”, and spaces disagree about how red that is. Three things fix a space: the chromaticities of its red, green and blue primaries, its white point, and its transfer curve. A colour profile is the file stating them. There are two ways to state them:

No profile means sRGB. Browsers, operating systems and CraterView all read an untagged file as sRGB. That is right for an sRGB file and wrong for anything else. A Display P3 photograph with its profile stripped is drawn as though its numbers were sRGB’s, and comes out duller: the same numbers, read against narrower primaries.

Space Gamut Transfer Where you meet it
sRGB The reference: Rec. 709 primaries, D65 white sRGB curve, about 2.2 The web; what “untagged” means
Display P3 Wider in reds and greens sRGB curve iPhones since 2016, Macs, wide-gamut displays
Adobe RGB (1998) Wider in greens and cyans Gamma 2.2 Cameras set to it; print work
ProPhoto RGB Wider than any display; two primaries are not real colours Gamma 1.8, D50 white Raw editors’ working space
Rec. 2020 Wider still PQ or HLG, for HDR HDR photographs and video
Greyscale One channel A curve alone, often gamma 2.2 or 1.8 Scanners, black-and-white exports

The CIE 1931 chromaticity diagram, with the triangles of sRGB, Display P3, Adobe RGB, Rec. 2020 and ProPhoto RGB. Each wider space reaches further toward the edge of visible colour; two ProPhoto primaries lie outside it. The red corners of the five spaces sit at different points.

The outline holds every colour a person can see. Each triangle joins a space’s three primaries, and its red corner is what (255, 0, 0) means in that space. Two ProPhoto RGB corners fall outside the outline, so some of its values name no colour at all. Every space here has the D65 white except ProPhoto RGB, whose white is D50.

A wider gamut spreads the same levels over a larger range. At 8 bits, each step in ProPhoto RGB is much coarser than in sRGB and banding comes early, so a wide working space wants 16 bits. Display P3 is close enough to sRGB that 8 bits holds a finished picture well, which is why an 8-bit Display P3 JPEG is an ordinary file.

HDR photographs from phones

“HDR” means two different things on a phone, and only one of them changes the file.

HDR in the camera — Smart HDR on an iPhone, HDR+ on a Pixel — merges several exposures into one picture whose shadows and highlights both fit an ordinary screen. That is tone mapping, done before the file is written. On its own it produces an ordinary SDR picture.

HDR on the screen keeps light brighter than diffuse white, the brightness of a white page. A display with headroom can then show a sunlit sky or a lamp brighter than white. There are two ways to store it:

An SDR picture plus a greyscale gain map makes the HDR rendition, brighter where the map is light. An SDR screen shows the SDR picture alone, and CraterView enhances the SDR picture without the map.

An illustration of the idea: the picture and the map are drawn, not decoded from a file.

Every CraterView result is SDR. PNG, JPEG and WebP results carry no gain map and no HDR curve.

Stored value against light for an HDR picture peaking at 1,000 cd/m². Cut at white, everything above 203 cd/m² is stored as 255. Rolled off, the two agree in the shadows, white is stored at 229, and the highlights above it keep their differences up to the brightest part of the picture.

Computed with the curve CraterView applies. Below about 100 cd/m² the two agree. Cut at white, everything brighter than 203 cd/m² is stored as 255, and the gradations of a bright sky are lost. Rolled off, white is stored at 229 and the highlights keep their differences. A picture with a different peak gets a different curve.

What each format holds

Format Bits per channel Compression Transparency
JPEG 8; the standard also allows 12, which little software reads Lossy; colour usually at half resolution (4:2:0) No
PNG 1–16 Lossless Yes
WebP 8 Lossy, colour at half resolution; or lossless Yes
HEIC / HEIF 8 from an iPhone, 10 from many cameras; up to 16 in the format Lossy Yes
TIFF 1–32, integer or floating point None; lossless LZW, ZIP or PackBits; or JPEG Yes
AVIF 8, 10 or 12 Lossy or lossless Yes
JPEG XL Up to 16 integer, or 32 floating point Lossy, lossless, or a lossless repack of a JPEG Yes
Camera raw, DNG 12–14, one sensor colour per pixel Varies No
FITS 8–64, integer or floating point Usually none No
Format States its colour with Shown by browsers CraterView
JPEG ICC All Reads and writes
PNG ICC All Reads and writes
WebP ICC All current Reads and writes
HEIC / HEIF ICC or CICP; may carry a gain map Safari Reads; returns JPEG or PNG
TIFF ICC Safari Reads; returns PNG
AVIF ICC or CICP All current Does not read
JPEG XL ICC or a built-in description Safari; arriving in Chrome and Firefox Does not read
Camera raw, DNG The camera’s own colour, not yet a colour space None Does not read
FITS Nothing: the values are data None Does not read

What CraterView does with them

Every image model returns its result at the input’s depth and in the input’s colour space, as far as the output format can hold them. Each model’s published output_format description says so. Enhance (cv-enhance-v3) also hands the model every level the file holds: all 65,536 of a 16-bit scan and all 1,024 of a 10-bit HEIF, rather than an 8-bit reduction of them.

The details:

Choosing output_format

Every model that returns an image takes output_format: auto, png, jpeg or webp. In the app it is one of the model’s settings; through the API it goes in params with the rest. auto, the default, prefers the format you sent, except for the two formats CraterView reads and never writes. Whatever is asked for or sent is a preference: where that format cannot hold the picture whole — its alpha channel, its depth or its size — the result comes back as PNG, and its content type says so.

You send auto returns Bits per channel
PNG PNG 16 if it was 16, otherwise 8
TIFF PNG 16 if it was 16, otherwise 8
JPEG JPEG 8
WebP WebP 8
HEIC / HEIF JPEG, or PNG if it has an alpha channel or more than 8 bits 8 as JPEG, its own depth as PNG

auto is right when the result should be the same kind of file you sent. When the result has a different job, name the format:

When auto returns Ask for
A lossless WebP: a screenshot, a diagram, a map A lossy WebP, colour at half resolution png
Text, line art or fine coloured edges, sent as WebP Colour at half resolution png or jpeg
A result you will edit, or send to another model A lossy file, if you sent one png
A large 8-bit PNG or TIFF you only need to look at or share A PNG, often very large jpeg

Checking a file

ExifTool reads what a file declares:

exiftool -s -BitDepth -BitsPerSample -SignificantBits -BitDepthLuma -ProfileDescription -ColorPrimaries photo.png

A header states a depth; it does not prove the levels are there. To count them in a PNG:

# pip install pypng numpy
import sys
import numpy as np
import png

for path in sys.argv[1:]:
    width, height, rows, info = png.Reader(filename=path).read()
    levels = np.unique(np.vstack([np.asarray(row) for row in rows])).size
    print(f"{path}: {info['bitdepth']} bits per channel, {levels} distinct levels")

A 16-bit file with 256 distinct levels or fewer holds 8 bits of information in a 16-bit container. Some readers reduce 16-bit colour to 8 bits when they open a file, without saying so. Check depth with a reader that keeps it, or a correct result will look truncated.

Choosing for the use

For Keep Send to CraterView Ask for
The web 8-bit sRGB or Display P3, as JPEG, WebP or AVIF The best copy you have png, then resize and encode for the page
Print A 16-bit master; a print file in the lab’s space The master png
An archive The original, untouched; scans as 16-bit TIFF or PNG The original or the scan png
Scientific work The data, in its own format A stretched 16-bit TIFF or PNG png

The web

Print

A personal archive

Scientific work

From the API

output_format is a parameter like any other. With the Python client:

# pip install craterview
from craterview import CraterView

cv = CraterView(api_key="cv_...")

# A 16-bit scanner TIFF, enlarged 2x: back as a 16-bit PNG in the scan's own colour space.
job = cv.run("scan-16bit.tif", model="cv-enhance-v3", scale=2)
job.save("scan-16bit-2x.png")

# A 10-bit HEIF from a camera: back as a PNG that keeps all of its levels, which auto
# chooses because a JPEG holds 8 bits.
job = cv.run("camera-10bit.heif", model="cv-enhance-v3")
job.save("camera-10bit-4x.png")

# A lossless WebP screenshot: auto would return a lossy WebP.
job = cv.run("screenshot.webp", model="cv-enhance-v3", output_format="png")
job.save("screenshot-4x.png")

Over HTTP, it goes in params on the submit: {"scale": 2, "output_format": "png"}. For the upload and polling under cv.run, see your first API call. The formats each model returns, its size limits and its price are published by GET /v1/models and shown in the app, so read them there rather than from this page.

Try it on your own file

No signup and no card — a workspace opens for your browser the moment you do.

Open Web App