Skip to main content

PyPI version fury.io

QOI

A simple Python wrapper around QOI, the "Quite OK Image" image format (via the qoi Rust crate). It's

  • Lossless with comparable compression to PNG, but fast! It encodes 4x+ faster and decodes 3x+ faster than PNG in OpenCV or PIL.
  • You can make it lossy with a simple trick (downscale before encoding), and then it's around 2.5x faster than JPEG, though the files are around 2.4x bigger for the same visual quality. (These numbers vary a lot depending on how "lossy" you make JPEG or QOI.)
  • Multi-threaded - no GIL hold-ups here. Encoding and decoding release the GIL, and free-threaded Python (e.g. 3.14t) is supported too.
  • Zero-copy where possible - arrays are encoded from, and decoded into, numpy memory directly.

Install

pip install qoi

Example

import numpy as np
import qoi

# Get your image as a numpy array (OpenCV, Pillow, etc. but here we just create a bunch of noise). Note: HWC ordering
rgb = np.random.randint(low=0, high=255, size=(224, 244, 3)).astype(np.uint8)

# Write it:
_ = qoi.write("/tmp/img.qoi", rgb)

# Read it and check it matches (it should, as we're lossless)
rgb_read = qoi.read("/tmp/img.qoi")
assert np.array_equal(rgb, rgb_read)

# Likewise for encode/decode to/from bytes:
bites = qoi.encode(rgb)
rgb_decoded = qoi.decode(bites)
assert np.array_equal(rgb, rgb_decoded)

# Benchmarking
from qoi.benchmark import benchmark
benchmark()  # Check out the arguments if you're interested

If you want to really max out your CPU:

from concurrent.futures import ThreadPoolExecutor, wait
import numpy as np
import qoi

RGB = np.random.randint(low=0, high=255, size=(224, 244, 3)).astype(np.uint8)

def worker():
    bites = bytearray(qoi.encode(RGB))
    img_decoded = qoi.decode(bites)

print("Go watch your CPU utilization ...")
with ThreadPoolExecutor(8) as pool:
    futures = [pool.submit(worker) for _ in range(10000)]
    wait(futures)

(Single-threaded) Benchmarks

If we consider lossless, then we're generally comparing with PNG. Yup, there are others, but they're not as common. Benchmarks:

Test image Method Format Input (kb) Encode (ms) Encode (kb) Decode (ms) SSIM
all black ('best' case) PIL png 6075.0 17.16 6.0 5.01 1.00
all black ('best' case) opencv png 6075.0 8.98 7.7 8.16 1.00
all black ('best' case) qoi qoi 6075.0 1.75 32.7 0.63 1.00
koi photo PIL png 6075.0 801.69 2821.5 50.21 1.00
koi photo opencv png 6075.0 51.01 3121.5 34.07 1.00
koi photo qoi qoi 6075.0 12.04 3489.0 8.83 1.00
random noise (worst case) PIL png 6075.0 142.37 6084.5 32.97 1.00
random noise (worst case) opencv png 6075.0 33.77 6086.9 8.77 1.00
random noise (worst case) qoi qoi 6075.0 8.95 8096.2 3.07 1.00

So qoi isn't far off PNG in terms of compression, but 4x-60x faster to encode and 3x-13x faster to decode.

NB:

  1. There's additional overhead here with PIL images being converted back to an array as the return type, to be consistent. In some sense, this isn't fair, as PIL will be faster if you're dealing with PIL images. On the other hand, if your common use case involves arrays (e.g. for computer vision) then it's reasonable.
  2. Produced with python -m qoi.benchmark --implementations=qoi,opencv,pil --formats=png,qoi --tests=20 on an i7-12700H (WSL2). Not going to the point of optimised OpenCV/PIL (e.g. SIMD, or pillow-simd) as the results are clear enough for this 'normal' scenario. If you want to dig further, go for it! You can easily run these tests yourself.

If we consider lossy compression, again, JPEG is usually what we're comparing with. Normally, it'd be unfair to compare QOI with JPEG as QOI is lossless, however we can do a slight trick to make QOI lossy - downscale the image, then encode it, then upsample it by the same amount after decoding. You can see we've implemented that below with a downscaling to 40% and JPEG quality of 80 (which results in them having the same visual compression i.e. SSIM). So, results (only on koi photo as the rest are less meaningful/fair for lossy):

Test image Method Format Input (kb) Encode (ms) Encode (kb) Decode (ms) SSIM
koi photo PIL jpg @ 80 6075.0 5.33 274.6 6.40 0.94
koi photo opencv jpg @ 80 6075.0 5.93 275.3 5.37 0.94
koi photo qoi qoi 6075.0 11.86 3489.0 8.62 1.00
koi photo qoi-lossy-0.40x0.40 qoi 6075.0 2.28 667.5 2.21 0.94

Here we see that lossless qoi is losing out considerably in compression, as expected for lossy vs lossless. Modern JPEG libraries (e.g. libjpeg-turbo, as now shipped with PIL and OpenCV) are also very fast, so lossless qoi is now around 2x slower to encode and 1.5x slower to decode than JPEG. Note this varies a lot depending on the jpeg quality specified - here it's 80 but the default for OpenCV is actually 95 which is 3x worse compression and a bit slower.

However, that's still lossy vs lossless! If you look at qoi-lossy-0.40x0.40 where we downscale as above, you can see that it can perform really well. The compression ratio is now only 2.4x that of JPEG (and 5x better than lossless QOI, and also the same as the default OpenCV JPEG encoding at a quality of 95), and it's faster - around 2.5x faster to encode and decode.

Anyway, there are definitely use cases where qoi may still make sense over JPEG. If you use the "lossy" QOI, you're getting "comparable" (depending on JPEG quality) compression but faster.

NB:

  1. See above re additional PIL overhead.
  2. Produced with python -m qoi.benchmark --images=koi --implementations=qoi,qoi-lossy,opencv,pil --formats=jpg,qoi --qoi-lossy-scale=0.4 --jpeg-quality=80 --tests=20 on an i7-12700H (WSL2). Not going to the point of optimised OpenCV/PIL (e.g. pillow-simd, different JPEG qualities, etc.) as the results are clear enough for this 'normal' scenario. If you want to dig further, go for it! You can easily run these tests yourself.

Developing

The extension is written in Rust (see src/lib.rs) using PyO3, and built with maturin. You'll need a Rust toolchain - see rustup.

git clone https://github.com/kodonnell/qoi/
cd qoi
pip install maturin
maturin develop --release --extras dev  # builds and installs (editable) into the current environment
pytest

Rerun maturin develop --release after changing the Rust code. (Without --release it builds much faster, but the result is a lot slower.)

We use maturin-action to build all the wheels in a GitHub action: one abi3 wheel per platform (covering CPython 3.11+) plus a free-threaded (3.14t) wheel. If you want to check a wheel builds locally, maturin build --release.

Finally, when you're happy, submit a PR.

Publishing

When you're on main on your local, bump the version in Cargo.toml (and commit it), then git tag vX.X.X (matching that version) and git push origin vX.X.X. This pushes the tag which triggers the full GitHub Action and:

  • Checks the tag matches the version in Cargo.toml
  • Builds source distribution and wheels (for various platforms), and tests them
  • Pushes to PyPI
  • Creates a new release with the appropriate artifacts attached.

TODO

  • Make benchmark.py a CLI entry point
  • Create a qoi CLI
  • Benchmarks - add real images, and also compare performance with QOI to see overhead of python wrapper.

Discussion

Wrap or rewrite?

For now, this is just a simple wrapper. We'll leave the underlying implementation to do all the hard work on performance etc., and also maintaining (or not) compatibility or adding new features etc. We make no claims to do any more than that - we're basically just porting that functionality to Python.

Up to v0.7 we wrapped the reference C implementation with Cython. From v0.8 we wrap the qoi Rust crate with PyO3, which is 1.1x-2x faster (except encoding noisy images, which is about the same), and much simpler to build and package. Things to be aware of if upgrading:

  • Python 3.11+ is required.
  • Files are still fully compatible both ways, but the encoded bytes for an image aren't always identical to the reference encoder's - the QOI format allows a few different (equally valid, and same size) encodings of the same pixels.
  • Invalid inputs consistently raise ValueError (e.g. wrong dtype/shape, non-contiguous arrays, bad colorspace), and failures to encode/decode/read/write raise RuntimeError with more detail than before.

On the name

For now, let's rock with qoi because

  • We're already in python, and the py in pyqoi seems redundant. For what it's worth, 3 < 5.
  • pyqoi seems like a good name for a python-only version of QOI (useful for pypy etc.), which this isn't.
  • qoi is generally new so let's not overthink it for now. We can always rename later if needed.

What's up with ./src?

See here and here. I didn't read all of it, but yeh, import qoi is annoying when there's also a folder called qoi.

Metadata

Release files for qoi 0.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for qoi 0.8.0
File Size Uploaded
qoi-0.8.0.tar.gz 3.2 MB Details

Built distributions (wheels)

Table of built distributions (wheels) for qoi 0.8.0
File
qoi-0.8.0-cp314-cp314t-win_amd64.whl CPython 3.14 CPython 3.14 free-threading Windows x86-64 Details
qoi-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.14 CPython 3.14 free-threading Linux glibc 2.17+ x86-64 Details
qoi-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl CPython 3.14 CPython 3.14 free-threading macOS 11.0+ ARM64 Details
qoi-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl CPython 3.14 CPython 3.14 free-threading macOS 10.12+ x86-64 Details
qoi-0.8.0-cp311-abi3-win_amd64.whl CPython 3.11 abi3 Windows x86-64 Details
qoi-0.8.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.11 abi3 Linux glibc 2.17+ x86-64 Details
qoi-0.8.0-cp311-abi3-macosx_11_0_arm64.whl CPython 3.11 abi3 macOS 11.0+ ARM64 Details
qoi-0.8.0-cp311-abi3-macosx_10_12_x86_64.whl CPython 3.11 abi3 macOS 10.12+ x86-64 Details

Total release size: 29.8 MB

Release files / qoi-0.8.0.tar.gz

Download URL qoi-0.8.0.tar.gz
Size 3.2 MB
Tags Source
SHA-256 checksum
How to use checksums
922a9833a190b173e1cd7da0544f4a5d25d65231e402e6bc3182ce2f22bccbcb
BLAKE2b-256 checksum
How to use checksums
3c877cb8d8b5c4172282d1e529a0a430c2ae67c3b52599011512f5a0aefc6725
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / qoi-0.8.0-cp314-cp314t-win_amd64.whl

Download URL qoi-0.8.0-cp314-cp314t-win_amd64.whl
Size 3.2 MB
Tags CPython 3.14 CPython 3.14 free-threading Windows x86-64
SHA-256 checksum
How to use checksums
5251190eb98282661ede6bea3c8eabf9a928e5626d9dd63f80f0996fd9c694b3
BLAKE2b-256 checksum
How to use checksums
2f44728783a346fb9f0955677288c0c702f48061f5cf37954d36bea7d3331f95
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / qoi-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL qoi-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 3.3 MB
Tags CPython 3.14 CPython 3.14 free-threading Linux glibc 2.17+ x86-64
SHA-256 checksum
How to use checksums
b11701bfc59034094b70a3b53fe531ab6a65a5e0ad35ff68605ab8c66e0b89b8
BLAKE2b-256 checksum
How to use checksums
3e692ba46dd43b90be9380acf1b103e2bf859b2fbc514f298cc2042a8015734d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / qoi-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl

Download URL qoi-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl
Size 3.3 MB
Tags CPython 3.14 CPython 3.14 free-threading macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
8acc98583a705b9a631318eb1e6dada29d1127c52533b5d735b5aab9652de07c
BLAKE2b-256 checksum
How to use checksums
dee857d156ca69c4d678cb8f5ddfa746912a3a219c24c01730ac636968a92183
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / qoi-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl

Download URL qoi-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl
Size 3.3 MB
Tags CPython 3.14 CPython 3.14 free-threading macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
d4f216c41cd518a132f30d1208cf9d6ca4751887a46dda6d9f0d0940217c9c28
BLAKE2b-256 checksum
How to use checksums
471306382b5d519a1a7bd239fb4297f435d555af505f8738f3c9a5a51926fec6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / qoi-0.8.0-cp311-abi3-win_amd64.whl

Download URL qoi-0.8.0-cp311-abi3-win_amd64.whl
Size 3.2 MB
Tags CPython 3.11 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
7f2f4f92f61419d7b659a9bf6e0b2764e8e0af9ac5b4a481153d0c7f85d9dd7a
BLAKE2b-256 checksum
How to use checksums
505d610efa9d2c8bc20321c960eb0466cb4ab5bcdc6dfa05551a1bd74b532177
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / qoi-0.8.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL qoi-0.8.0-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 3.4 MB
Tags CPython 3.11 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
258ef0d81bdf7d7f575aff802dcf1e0047570f765e226e3353663c2c8d9ebb19
BLAKE2b-256 checksum
How to use checksums
7acb0074051e2b8f0ff61aa0a2f6f6cb37227758b47af187730da279c146d927
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / qoi-0.8.0-cp311-abi3-macosx_11_0_arm64.whl

Download URL qoi-0.8.0-cp311-abi3-macosx_11_0_arm64.whl
Size 3.3 MB
Tags CPython 3.11 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
4590f214844eef89004e26edaba7ea7946caf76c985deaa8bf7a3f642839c71d
BLAKE2b-256 checksum
How to use checksums
9d19814ab1ae50e332fbcd083c498ba75963d48c257f1d2f62ce5f81c977c41d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / qoi-0.8.0-cp311-abi3-macosx_10_12_x86_64.whl

Download URL qoi-0.8.0-cp311-abi3-macosx_10_12_x86_64.whl
Size 3.3 MB
Tags CPython 3.11 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
2f772d4faad1a8a5e4259b9e95993b52d46bbfc30677f2ed62fa522d82e0105a
BLAKE2b-256 checksum
How to use checksums
7305948570acafa4d547b63ccc127098551b814a7252cdff3f1cbc3dfb7e76a6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

This release

0.8.0 This release

9 release files

0.7.2

26 release files

0.7.1

1 release file

0.7.0

1 release file

0.6.0

1 release file

0.5.0

26 release files

0.4.0

26 release files

0.3.1

21 release files

0.3.0

21 release files

0.2.0

16 release files

0.1.2

16 release files

0.0.9

20 release files

0.0.8

20 release files

0.0.6

1 release file

0.0.5

1 release file

0.0.4

1 release file

0.0.3

1 release file

0.0.2

1 release file

0.0.1

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page