libstb Documentation
GitHub
libstb benchmark report · generated 2026-10-18 22:54 UTC

libstb, measured honestly

Image decoding, encoding, resizing and text, each measured on its own against the libraries people actually reach for: Pillow, OpenCV, imageio and scikit-image.

88 corpus entries · 7 repeats per cell · 225 output checks passed before timing
Method

How the numbers are computed

Each corpus entry \(i\) is one image (a genre, a size and a format). For a library \(L\), \(t_i^{L}\) is the best of 7 timed runs of one operation on entry \(i\), \(s_i^{L}\) is the size in bytes of the file it wrote, and \(m_i^{L}\) is its extra peak memory. Two libraries are compared only on the entries where both ran.

Install footprint

What pip install pulls in

Download size of the wheel for this platform plus every dependency, resolved from PyPI by pip-size. Capability is not the same thing as size: a bigger package is usually doing far more, which is why the last column is here.

Total download size (lower is better)

PackageVersionTotalDepsWhat it is
libstb ★ 0.5.0 2.1 MB 0 image decode, png/jpg/bmp/tga encode, resize, TrueType text, zlib (stb, no system dependencies)
Pillow 12.3.0 6.6 MB 0 many-format image io, resize, filters, drawing, FreeType text
numpy 2.5.4 16.0 MB 0 arrays only (a dependency of OpenCV, imageio and scikit-image; optional for libstb, not used by Pillow)
imageio 2.38.1 22.9 MB 2 many-format image io (delegates to Pillow and friends)
scikit-image 0.26.0 72.0 MB 8 image processing algorithms (io + resize + far more)
OpenCV 5.0.0.93 74.3 MB 1 image io, resize, and a large computer-vision toolkit
Decoding

Reading images

Header-only reads and full decodes, from bytes and from a file, on every corpus entry: synthetic images of four shapes and real photographs, each stored as png, jpg, bmp and tga. Speed is the best of 7 runs; memory is the extra peak RSS of one isolated process per measurement.

info: read the header only, no pixels

Width, height and channel count without decoding anything. Only libstb and Pillow have a header-only call; OpenCV and imageio do not, so they are absent here.

libstb is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{libstb}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): Pillow 6.4× as long.

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

decode: encoded bytes in, pixel array out

Each file was written by Pillow, never by stb. OpenCV has no TGA decoder, so it has no TGA entries. imageio delegates to Pillow, so it is Pillow plus its own overhead.

libstb is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{libstb}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): OpenCV 0.9×, Pillow 2.0×, imageio 3.3× as long.

libstb needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured). On average, \(\mathbb{E}\!\left[\dfrac{m_{L}}{m_{\text{libstb}}}\right]\): OpenCV 4.2×, Pillow 5.1×, imageio 6.5× as much.

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

load_file: path in, pixel array out

The same as decode but starting from a path, so file I/O is included: what someone actually types.

libstb is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{libstb}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): OpenCV 0.9×, Pillow 1.7×, imageio 2.7× as long.

libstb needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured). On average, \(\mathbb{E}\!\left[\dfrac{m_{L}}{m_{\text{libstb}}}\right]\): OpenCV 4.2×, Pillow 4.9×, imageio 6.0× as much.

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

Encoding

Writing images

Raw pixels to file bytes, at matched settings. Speed alone is not the whole story for an encoder, so each panel also shows how large the output is. Speed is the best of 7 runs; memory is the extra peak RSS of one isolated process per measurement.

encode PNG: raw pixels in, file bytes out

zlib level 6 on every side. stb bundles its own small deflate instead of zlib, so the same level does not produce the same bytes: read the speed together with the output-size table below.

libstb is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{libstb}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): OpenCV 1.7×, Pillow 2.1×, imageio 2.3× as long.

libstb needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured). On average, \(\mathbb{E}\!\left[\dfrac{m_{L}}{m_{\text{libstb}}}\right]\): Pillow 0.0×, OpenCV 0.0×, imageio 1.3× as much.

Output size\(\mathbb{E}\!\left[\dfrac{s_{L}}{s_{\text{ Pillow }}}\right]\)entries
libstb1.01×22
OpenCV1.02×22
imageio1.00×22

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

encode JPEG: raw pixels in, file bytes out

Quality 90 on every side, 3-channel images only. Encoders make different size/quality trades at the same setting, so the output-size table matters as much as the time.

OpenCV is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{OpenCV}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): Pillow 1.2×, imageio 2.1×, libstb 2.2× as long.

OpenCV needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured).

Output size\(\mathbb{E}\!\left[\dfrac{s_{L}}{s_{\text{ Pillow }}}\right]\)entries
libstb0.99×18
OpenCV1.00×18
imageio1.00×18

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

encode BMP: raw pixels in, file bytes out

Uncompressed, so this is mostly memory bandwidth. 3-channel images only.

OpenCV is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{OpenCV}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): Pillow 2.0×, libstb 2.3×, imageio 5.1× as long.

OpenCV needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured). On average, \(\mathbb{E}\!\left[\dfrac{m_{L}}{m_{\text{OpenCV}}}\right]\): Pillow 1.1×, libstb 13.7×, imageio 18.2× as much.

Output size\(\mathbb{E}\!\left[\dfrac{s_{L}}{s_{\text{ Pillow }}}\right]\)entries
libstb1.00×18
OpenCV1.00×18
imageio1.00×18

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

encode TGA: raw pixels in, file bytes out

Uncompressed on every side (libstb with rle=False, matching Pillow's default). OpenCV has no TGA encoder.

Pillow is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{Pillow}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): libstb 1.4×, imageio 2.3× as long.

Pillow needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured). On average, \(\mathbb{E}\!\left[\dfrac{m_{L}}{m_{\text{Pillow}}}\right]\): libstb 4.9×, imageio 5.8× as much.

Output size\(\mathbb{E}\!\left[\dfrac{s_{L}}{s_{\text{ Pillow }}}\right]\)entries
libstb1.00×22
imageio1.00×22

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

Resizing

Resizing images

Half size and double size, with linear, cubic and box filters. Speed is the best of 7 runs; memory is the extra peak RSS of one isolated process per measurement.

resize, half size, linear (triangle)

libstb, Pillow and scikit-image prefilter when shrinking; OpenCV's INTER_LINEAR and INTER_CUBIC do not (faster, and they alias), and INTER_AREA is its antialiasing counterpart. "libstb" is Resizer(filter, srgb=False), the like-for-like pairing; "libstb (sRGB-correct)" is the library's own default, which blends in linear light like a correct resizer should, listed separately so the price of that is visible. 3-channel images only. scikit-image is skipped above 1 MP (seconds per call).

OpenCV is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{OpenCV}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): libstb 2.8×, libstb (sRGB-correct) 6.7×, Pillow 10.6×, scikit-image 155.3× as long.

OpenCV needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured).

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

resize, half size, cubic (Catmull-Rom)

libstb, Pillow and scikit-image prefilter when shrinking; OpenCV's INTER_LINEAR and INTER_CUBIC do not (faster, and they alias), and INTER_AREA is its antialiasing counterpart. "libstb" is Resizer(filter, srgb=False), the like-for-like pairing; "libstb (sRGB-correct)" is the library's own default, which blends in linear light like a correct resizer should, listed separately so the price of that is visible. 3-channel images only. scikit-image is skipped above 1 MP (seconds per call).

OpenCV is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{OpenCV}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): libstb 1.8×, libstb (sRGB-correct) 3.6×, Pillow 7.9×, scikit-image 352.1× as long.

OpenCV needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured).

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

resize, half size, box / area

libstb, Pillow and scikit-image prefilter when shrinking; OpenCV's INTER_LINEAR and INTER_CUBIC do not (faster, and they alias), and INTER_AREA is its antialiasing counterpart. "libstb" is Resizer(filter, srgb=False), the like-for-like pairing; "libstb (sRGB-correct)" is the library's own default, which blends in linear light like a correct resizer should, listed separately so the price of that is visible. 3-channel images only. scikit-image is skipped above 1 MP (seconds per call). scikit-image has no box filter.

OpenCV is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{OpenCV}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): libstb 2.0×, libstb (sRGB-correct) 5.9×, Pillow 7.0× as long.

OpenCV needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured).

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

resize, double size, linear (triangle)

libstb, Pillow and scikit-image prefilter when shrinking; OpenCV's INTER_LINEAR and INTER_CUBIC do not (faster, and they alias), and INTER_AREA is its antialiasing counterpart. "libstb" is Resizer(filter, srgb=False), the like-for-like pairing; "libstb (sRGB-correct)" is the library's own default, which blends in linear light like a correct resizer should, listed separately so the price of that is visible. 3-channel images only. scikit-image is skipped above 1 MP (seconds per call).

OpenCV is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{OpenCV}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): libstb 1.5×, libstb (sRGB-correct) 3.4×, Pillow 5.5×, scikit-image 130.8× as long.

OpenCV needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured). On average, \(\mathbb{E}\!\left[\dfrac{m_{L}}{m_{\text{OpenCV}}}\right]\): libstb (sRGB-correct) 0.9×, libstb 0.9×, Pillow 2.6×, scikit-image 18.3× as much.

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

resize, double size, cubic (Catmull-Rom)

libstb, Pillow and scikit-image prefilter when shrinking; OpenCV's INTER_LINEAR and INTER_CUBIC do not (faster, and they alias), and INTER_AREA is its antialiasing counterpart. "libstb" is Resizer(filter, srgb=False), the like-for-like pairing; "libstb (sRGB-correct)" is the library's own default, which blends in linear light like a correct resizer should, listed separately so the price of that is visible. 3-channel images only. scikit-image is skipped above 1 MP (seconds per call).

OpenCV is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{OpenCV}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): libstb 2.1×, libstb (sRGB-correct) 3.9×, Pillow 8.0×, scikit-image 872.2× as long.

OpenCV needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured). On average, \(\mathbb{E}\!\left[\dfrac{m_{L}}{m_{\text{OpenCV}}}\right]\): libstb (sRGB-correct) 0.6×, libstb 0.6×, Pillow 1.8×, scikit-image 118.1× as much.

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

Editing

Editing images

Crop, quarter turn, flip, RGB to gray and alpha compositing: the operations libstb added for working on pixels without leaving the library, against Pillow and OpenCV where they have one. Speed is the best of 7 runs; memory is the extra peak RSS of one isolated process per measurement.

crop: the central half of each side

A new, independent image in every library (an OpenCV slice is a view, so it is copied). This is mostly memory bandwidth. 3-channel images only.

libstb is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{libstb}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): OpenCV 1.2×, Pillow 2.1× as long.

libstb needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured).

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

rotate: a counter-clockwise quarter turn

Pillow's transpose(ROTATE_90) and cv2.rotate against libstb's rotate90. A turn reads rows and writes columns, so it is about memory access patterns. 3-channel images only.

OpenCV is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{OpenCV}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): libstb 1.2×, Pillow 1.6× as long.

OpenCV needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured).

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

flip left to right

Pillow's transpose(FLIP_LEFT_RIGHT) and cv2.flip against libstb's flip_horizontal. 3-channel images only.

OpenCV is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{OpenCV}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): libstb 1.4×, Pillow 1.6× as long.

OpenCV needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured).

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

convert: RGB to gray

libstb and Pillow use the same integer formula and give identical pixels; OpenCV's BGR2GRAY rounds slightly differently. libstb runs the fastest SIMD backend of the CPU it is measured on (libstb.simd_name()). 3-channel images only.

OpenCV is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{OpenCV}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): libstb 1.2×, Pillow 1.7× as long.

OpenCV needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured).

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

composite: RGBA over RGBA, straight alpha

An RGBA image laid over its own upside-down copy, against Pillow's Image.alpha_composite (OpenCV has no equivalent). The alpha channel is identical; the colour is within one level where the pixel is visible. 4-channel images only, so only the graphics images take part.

libstb is the reference (fastest on the most corpus entries). On average (\(\mathbb{E}\!\left[\dfrac{t_{L}}{t_{\text{libstb}}}\right]\), the mean of each entry's own ratio, not a ratio of medians): Pillow 2.6× as long.

libstb needs the least extra memory on the most entries (extra peak RSS on top of what the process held before the operation started; images under 0.065 MP are not measured). On average, \(\mathbb{E}\!\left[\dfrac{m_{L}}{m_{\text{libstb}}}\right]\): Pillow 1.0× as much.

Throughput vs. image size (higher is better)

Time vs. image size (lower is better)

Extra peak memory vs. image size (lower is better)

Scaling

Does the slope change with image size?

One photo-like image at each side length from 128 to 4096 pixels, one representative operation of each kind, libstb against Pillow and OpenCV. A flat line means the cost per pixel stays constant; a falling line means it grows with the image.

decode PNG (throughput, higher is better)

decode JPEG (throughput, higher is better)

encode PNG (throughput, higher is better)

encode JPEG (throughput, higher is better)

resize, half size, cubic (throughput, higher is better)

Text

stb_truetype vs. FreeType

DejaVuSans.ttf, libstb's Font against Pillow's FreeType binding on its basic layout engine (so Pillow is not charged for shaping, which stb_truetype does not do). The two rasterize differently, so this compares speed only. stb_truetype has no security guarantee on untrusted fonts; FreeType is hardened. No speed number captures that.

open: load the font file, get a font object

On average (\(\mathbb{E}\!\left[\dfrac{t_{\text{libstb}}}{t_{\text{Pillow}}}\right]\), the mean of each case's own ratio): 0.48×; below 1 means libstb took less time.

Time per call (lower is better)

See the numbers
CaselibstbPillowlibstb / Pillow
16px24.1µs66.2µs0.36×
32px32.7µs67.4µs0.48×
64px32.2µs55.6µs0.58×

measure: how big would this text be

On average (\(\mathbb{E}\!\left[\dfrac{t_{\text{libstb}}}{t_{\text{Pillow}}}\right]\), the mean of each case's own ratio): 0.07×; below 1 means libstb took less time.

Time per call (lower is better)

See the numbers
CaselibstbPillowlibstb / Pillow
short 16px9.9µs83.1µs0.12×
short 32px9.7µs84.0µs0.12×
short 64px7.5µs85.4µs0.09×
paragraph 16px16.9µs814.8µs0.02×
paragraph 32px20.7µs775.3µs0.03×
paragraph 64px20.7µs788.0µs0.03×

render: text in, coverage bitmap out (measure + allocate + draw)

On average (\(\mathbb{E}\!\left[\dfrac{t_{\text{libstb}}}{t_{\text{Pillow}}}\right]\), the mean of each case's own ratio): 0.10×; below 1 means libstb took less time.

Time per call (lower is better)

See the numbers
CaselibstbPillowlibstb / Pillow
short 16px35.4µs277.8µs0.13×
short 32px41.5µs293.4µs0.14×
short 64px61.6µs366.1µs0.17×
paragraph 16px101.9µs2.57ms0.04×
paragraph 32px143.7µs2.91ms0.05×
paragraph 64px244.5µs3.70ms0.07×
Environment

Where these numbers came from

Before anything was timed, every operation and library was run on small non-square images and its output checked (exact pixels for lossless formats, close for JPEG and resizing): 225 checks passed, 0 failed, 11 library/operation pairs unavailable (formats a library cannot handle at all).

CPUIntel(R) Xeon(R) Processor
Logical cores1
RAM3.9 GiB
Architecturex86_64
OSLinux 6.18.44-fc-v114
PythonCPython 3.12.3