Image decoding, encoding, resizing and text, each measured on its own against the libraries people actually reach for: Pillow, OpenCV, imageio and scikit-image.
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.
pip install pulls inDownload 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.
| Package | Version | Total | Deps | What it is |
|---|---|---|---|---|
| libstb ★ | 0.5.0 | 2.1 MB | 0 | image decode, png/jpg/bmp/tga encode, resize, TrueType text (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 |
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.
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.
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.
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.
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.
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 |
|---|---|---|
| libstb | 1.01× | 22 |
| OpenCV | 1.02× | 22 |
| imageio | 1.00× | 22 |
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 |
|---|---|---|
| libstb | 0.99× | 18 |
| OpenCV | 1.00× | 18 |
| imageio | 1.00× | 18 |
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 |
|---|---|---|
| libstb | 1.00× | 18 |
| OpenCV | 1.00× | 18 |
| imageio | 1.00× | 18 |
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 |
|---|---|---|
| libstb | 1.00× | 22 |
| imageio | 1.00× | 22 |
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.
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).
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).
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).
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.
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.
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.
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).
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).
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).
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).
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.
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.
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.
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.
| Case | libstb | Pillow | libstb / Pillow |
|---|---|---|---|
| 16px | 24.1µs | 66.2µs | 0.36× |
| 32px | 32.7µs | 67.4µs | 0.48× |
| 64px | 32.2µs | 55.6µs | 0.58× |
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.
| Case | libstb | Pillow | libstb / Pillow |
|---|---|---|---|
| short 16px | 9.9µs | 83.1µs | 0.12× |
| short 32px | 9.7µs | 84.0µs | 0.12× |
| short 64px | 7.5µs | 85.4µs | 0.09× |
| paragraph 16px | 16.9µs | 814.8µs | 0.02× |
| paragraph 32px | 20.7µs | 775.3µs | 0.03× |
| paragraph 64px | 20.7µs | 788.0µs | 0.03× |
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.
| Case | libstb | Pillow | libstb / Pillow |
|---|---|---|---|
| short 16px | 35.4µs | 277.8µs | 0.13× |
| short 32px | 41.5µs | 293.4µs | 0.14× |
| short 64px | 61.6µs | 366.1µs | 0.17× |
| paragraph 16px | 101.9µs | 2.57ms | 0.04× |
| paragraph 32px | 143.7µs | 2.91ms | 0.05× |
| paragraph 64px | 244.5µs | 3.70ms | 0.07× |
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).
| CPU | Intel(R) Xeon(R) Processor |
| Logical cores | 1 |
| RAM | 3.9 GiB |
| Architecture | x86_64 |
| OS | Linux 6.18.44-fc-v114 |
| Python | CPython 3.12.3 |