Package: qoi
QOIA simple Python wrapper aroundqoi, the "Quite OK Image" image format. It'sLossless with comparable compression to PNG, but fast! It encodes 10x faster and decodes around 5x faster than PNG in OpenCV or PIL.You can make it lossy with a simple trick, and then you can get similar compression to JPEG but a whole lot faster. (These number vary a lot depending on how "lossy" you make JPEG or QOI). That's cool.Multi-threaded - no GIL hold-ups here.InstallpipinstallqoiExampleimportnumpyasnpimportqoi# Get your image as a numpy array (OpenCV, Pillow, etc. but here we just create a bunch of noise)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")assertnp.array_equal(rgb,rgb_read)# Likewise for encode/decode to/from bytes:bites=qoi.encode(rgb)rgb_decoded=qoi.decode(bites)assertnp.array_equal(rgb,rgb_decoded)# Benchmarkingfromqoi.benchmarkimportbenchmarkbenchmark()# Check out the arguments if you're interestedIf you want to really max out your CPU:fromconcurrent.futuresimportThreadPoolExecutor,waitimportnumpyasnpimportqoiRGB=np.random.randint(low=0,high=255,size=(224,244,3)).astype(np.uint8)defworker():bites=bytearray(qoi.encode(RGB))img_decoded=qoi.decode(bites)print("Go watch your CPU utilization ...")withThreadPoolExecutor(8)aspool:futures=[pool.submit(worker)for_inrange(10000)]wait(futures)(Single-threaded) BenchmarksIf we consider lossless, then we're generally comparing with PNG. Yup, there are others, but they're not as common. Benchmarks:Test imageMethodFormatInput (kb)Encode (ms)Encode (kb)Decode (ms)SSIMall black ('best' case)PILpng6075.037.756.016.041.00all black ('best' case)opencvpng6075.023.827.717.931.00all black ('best' case)qoiqoi6075.04.1332.72.671.00koi photoPILpng6075.0849.072821.585.461.00koi photoopencvpng6075.095.243121.544.341.00koi photoqoiqoi6075.028.373489.017.191.00random noise (worst case)PILpng6075.0300.376084.546.301.00random noise (worst case)opencvpng6075.063.726086.914.011.00random noise (worst case)qoiqoi6075.016.168096.17.671.00Soqoiisn't far off PNG in terms of compression, but 4x-20x faster to encode and 1.5x-6x faster to decode.NB: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.Produced withpython src/qoi/benchmark.py --implementations=qoi,opencv,pil --formats=png,qoion an i7-9750H. Not going to the point of optimised OpenCV/PIL (e.g. SIMD, orpillow-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 onkoi photoas the rest are less meaningful/fair for lossy):Test imageMethodFormatInput (kb)Encode (ms)Encode (kb)Decode (ms)SSIMkoi photoPILjpg @ 806075.047.67275.224.010.94koi photoopencvjpg @ 806075.024.03275.319.580.94koi photoqoiqoi6075.023.173489.012.941.00koi photoqoi-lossy-0.40x0.40qoi6075.04.38667.52.960.94Here we see that losslessqoiis losing out considerably in compression, as expected for lossy vs lossless. Also,qoiis only 1x-2x faster of encoding, and 1.5x-2x faster for decoding. However, it's important to note that 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 atqoi-lossy-0.40x0.40where we downscale as above, you can see that it can perform really well. The compression ratio is now only 3x that of JPEG (and 5x better than lossless QOI, and also the same as the default OpenCV JPEG encoding at a quality of 95), but it's so fast - 5x-10x faster encoding, and 7x-8x faster decoding.Anyway, there are definitely use cases whereqoimay still make sense over JPEG. Even lossless QOI can be worth it if size isn't an issue, as it's a bit faster. But if you use the "lossy" QOI, you're getting "comparable" (depending on JPEG quality) compression but much faster.NB:See above re additional PIL overhead.Produced withpython src/qoi/benchmark.py --images=koi --implementations=qoi,qoi-lossy,opencv,pil --formats=jpg,qoi --qoi-lossy-scale=0.4 --jpeg-quality=0.8on an i7-9750H. Not going to the point of optimised OpenCV/PIL (e.g. SIMD, orpillow-simd,libjpeg-turbo, 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.Developinggitclone--recursivehttps://github.com/kodonnell/qoi/USE_CYTHON=1pipinstall-e.[dev]pytest.We usecibuildwheelto build all the wheels, which runs in a Github action. If you want to check this succeeds locally, you can try (untested):cibuildwheel--platformlinux.Finally, when you're happy, submit a PR.PublishingWhen you're onmainon your local,git tag vX.X.Xthengit push origin vX.X.X. This pushes the tag which triggers the full GitHub Action and:Builds source distribution and wheels (for various platforms)Pushes to PyPICreates a new release with the appropriate artifacts attached.TODOMakebenchmark.pya CLI in setup.pyCreate aqoiCLIBenchmarks - add real images, and also compare performance with QOI to see overhead of python wrapper.setuptools_scm_git_archive?Code completion?DiscussionWrap or rewrite?For now, this is just a simple wrapper. We'll leave the original project 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 C functionality to (C)Python.On the nameFor now, let's rock withqoibecauseWe're already in python, and thepyinpyqoiseems redundant. For what it's worth,3 < 5.pyqoiseems like a good name for a python-only version of QOI (useful for pypy etc.), which this isn't.qoiis generally new so let's not overthink it for now. We can always rename later if needed.What's up with./src?Seehereandhere. I didn't read all of it, but yeh,import qoiis annoying when there's also a folder calledqoi.USE_CYTHON=1?Seehere. Fair point.