JuliaIO / JpegTurbo.jl

Julia interface to libjpeg-turbo
MIT License
15 stars 7 forks source link

initial implementation for `jpeg_encode` and `jpeg_decode` #3

Closed johnnychen94 closed 2 years ago

johnnychen94 commented 2 years ago

APIs

# encode pixels into JPEG bytes
jpeg_encode(filename::AbstractString, img; kwargs...) -> Int
jpeg_encode(io::IO, img; kwargs...) -> Int
jpeg_encode(img; kwargs...) -> Vector{UInt8}

# decode JPEG bytes into pixels
jpeg_decode([T,] filename::AbstractString; kwargs...) -> Matrix{T}

Feature set

function filename IOStream in-memory buffer pre-allocated output multi-threads
jpeg_encode x x x x
jpeg_decode x x
ImageMagick.save x x x x
ImageMagick.load x x x x
QuartzImageIO.save x x x (FileIO.Stream) x
QuartzImageIO.load x x x (FileIO.Stream) x

Notes:

Benchmark

I've compared JpegTurbo.jl with ImageMagick.jl, QuartzImageIO.jl, OpenCV(PyCall), Scikit-image(PyCall). Generally speaking, JpegTurbo.jl is the fastest version.

The default compress quality is set to 92, which is ImageMagick's default value. Except for a few cases (e.g., "mandril"), the PSNR results are similar to the ImageMagick version.

compare against other backends # JPEG backends comparison ``` Julia Version 1.8.0-DEV.1434 Commit 4abf26eec8 (2022-01-30 20:04 UTC) Platform Info: OS: macOS (x86_64-apple-darwin18.7.0) CPU: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz WORD_SIZE: 64 LIBM: libopenlibm LLVM: libLLVM-13.0.0 (ORCJIT, skylake) Environment: JULIA_NUM_THREADS = 8 OpenCV version: 4.5.5 Scikit-image version: 0.19.1 ``` ## moonsurface Gray{N0f8} (256, 256) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 0.40 | 0.33 | 22.66 | 39.0516 | | ImageMagick.jl | 1.18 | 1.44 | 22.24 | 39.0533 | | QuartzImageIO.jl | 1.17 | 0.60 | 25.01 | 39.5761 | | OpenCV (PyCall) | 1.02 | 0.53 | 29.47 | 42.3855 | | Scikit-image (PyCall) | 1.84 | 0.69 | 10.83 | 34.0346 | ## cameraman Gray{N0f8} (512, 512) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 1.04 | 0.81 | 50.19 | 47.1206 | | ImageMagick.jl | 3.09 | 4.54 | 49.30 | 47.1297 | | QuartzImageIO.jl | 2.18 | 1.22 | 50.82 | 47.0878 | | OpenCV (PyCall) | 2.78 | 1.45 | 65.63 | 49.2061 | | Scikit-image (PyCall) | 5.25 | 1.53 | 27.55 | 41.9095 | ## pirate Gray{N0f8} (512, 512) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 1.15 | 1.01 | 79.84 | 40.9099 | | ImageMagick.jl | 3.64 | 4.78 | 78.51 | 40.9127 | | QuartzImageIO.jl | 2.62 | 1.48 | 81.77 | 41.3253 | | OpenCV (PyCall) | 3.18 | 1.86 | 104.68 | 43.5602 | | Scikit-image (PyCall) | 6.57 | 1.72 | 42.09 | 35.6561 | ## house Gray{N0f8} (512, 512) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 0.97 | 0.68 | 35.70 | 50.0640 | | ImageMagick.jl | 2.88 | 4.25 | 35.16 | 50.0741 | | QuartzImageIO.jl | 2.00 | 1.13 | 36.61 | 49.6511 | | OpenCV (PyCall) | 2.30 | 1.23 | 46.67 | 51.8188 | | Scikit-image (PyCall) | 4.85 | 1.38 | 20.61 | 45.5563 | ## rand Gray{Float64} (512, 512) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 2.18 | 1.83 | 215.79 | 38.3101 | | ImageMagick.jl | 4.18 | 5.37 | 189.28 | 38.3115 | | QuartzImageIO.jl | 4.35 | 2.43 | 218.84 | 39.1215 | | OpenCV (PyCall) | 3.58 | 2.50 | 257.45 | 42.3429 | | Scikit-image (PyCall) | 10.06 | 2.67 | 142.33 | 28.5171 | ## rand Gray{Float64} (4096, 4096) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 274.02 | 208.91 | 13794.18 | 38.3060 | | ImageMagick.jl | 365.29 | 556.16 | 12103.64 | 38.3060 | | QuartzImageIO.jl | 329.87 | 252.93 | 13850.71 | 39.1137 | | OpenCV (PyCall) | 307.79 | 278.67 | 16463.29 | 42.3242 | | Scikit-image (PyCall) | 820.44 | 280.53 | 9090.66 | 28.5389 | ## fabio RGB{N0f8} (512, 512) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 1.34 | 3.97 | 55.91 | 42.7003 | | ImageMagick.jl | 5.83 | 6.10 | 72.76 | 45.5593 | | QuartzImageIO.jl | 4.62 | 6.85 | 55.38 | 42.0154 | | OpenCV (PyCall) | 5.87 | 4.50 | 72.57 | 44.0539 | | Scikit-image (PyCall) | 15.75 | 4.78 | 31.68 | 37.8229 | ## barbara RGB{N0f8} (576, 720) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 2.44 | 6.86 | 140.21 | 36.1151 | | ImageMagick.jl | 9.34 | 9.78 | 179.70 | 38.1910 | | QuartzImageIO.jl | 8.02 | 11.82 | 139.84 | 36.0860 | | OpenCV (PyCall) | 11.11 | 8.68 | 185.88 | 37.2003 | | Scikit-image (PyCall) | 28.19 | 7.85 | 74.79 | 32.7438 | ## mandril RGB{N0f8} (512, 512) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 1.90 | 4.86 | 149.28 | 27.7261 | | ImageMagick.jl | 8.49 | 7.57 | 241.40 | 32.2466 | | QuartzImageIO.jl | 6.10 | 8.36 | 150.35 | 27.7677 | | OpenCV (PyCall) | 7.48 | 6.17 | 190.93 | 28.2401 | | Scikit-image (PyCall) | 20.33 | 6.13 | 76.89 | 25.6526 | ## coffee RGB{N0f8} (400, 600) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 1.42 | 3.92 | 78.10 | 36.1796 | | ImageMagick.jl | 5.98 | 6.00 | 100.48 | 38.2192 | | QuartzImageIO.jl | 4.25 | 6.02 | 78.63 | 36.1604 | | OpenCV (PyCall) | 5.53 | 4.55 | 102.26 | 37.4588 | | Scikit-image (PyCall) | 15.36 | 4.76 | 40.71 | 32.2566 | ## lighthouse RGB{N0f8} (512, 768) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 2.65 | 6.99 | 125.39 | 38.6723 | | ImageMagick.jl | 9.68 | 9.92 | 147.12 | 39.6910 | | QuartzImageIO.jl | 7.20 | 10.40 | 125.60 | 38.8406 | | OpenCV (PyCall) | 9.54 | 7.35 | 165.09 | 40.4860 | | Scikit-image (PyCall) | 45.30 | 7.28 | 63.88 | 33.8235 | ## earth_apollo RGB{N0f8} (3002, 3000) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 68.99 | 186.33 | 1779.18 | 39.5714 | | ImageMagick.jl | 212.79 | 230.55 | 2463.45 | 42.1515 | | QuartzImageIO.jl | 179.41 | 337.31 | 1734.75 | 39.5452 | | OpenCV (PyCall) | 246.82 | 232.78 | 2428.32 | 40.6026 | | Scikit-image (PyCall) | 619.76 | 246.03 | 906.01 | 37.6173 | ## rand RGB{Float64} (512, 512) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 4.39 | 5.57 | 248.64 | 12.7318 | | ImageMagick.jl | 10.47 | 9.22 | 446.42 | 31.8193 | | QuartzImageIO.jl | 7.62 | 8.09 | 249.95 | 12.7788 | | OpenCV (PyCall) | 7.49 | 6.32 | 300.34 | 12.7464 | | Scikit-image (PyCall) | 22.67 | 6.11 | 154.14 | 12.6452 | ## rand RGB{Float64} (4096, 4096) | Backend | encode time(ms) | decode time(ms) | encoded size(KB) | PSNR(dB) | | ------- | --------------- | --------------- | ---------------- | -------- | | JpegTurbo.jl | 776.43 | 482.70 | 15873.58 | 12.7283 | | ImageMagick.jl | 814.72 | 757.56 | 28545.15 | 31.8188 | | QuartzImageIO.jl | 562.66 | 695.01 | 15946.29 | 12.8189 | | OpenCV (PyCall) | 874.32 | 848.83 | 19184.72 | 12.7812 | | Scikit-image (PyCall) | 2566.55 | 849.31 | 9827.60 | 12.6801 |
codecov[bot] commented 2 years ago

Codecov Report

Merging #3 (5798d88) into master (013210c) will decrease coverage by 13.19%. The diff coverage is 86.33%.

Impacted file tree graph

@@             Coverage Diff              @@
##            master       #3       +/-   ##
============================================
- Coverage   100.00%   86.80%   -13.20%     
============================================
  Files            1        4        +3     
  Lines            5      144      +139     
============================================
+ Hits             5      125      +120     
- Misses           0       19       +19     
Impacted Files Coverage Δ
src/common.jl 51.72% <51.72%> (ø)
src/decode.jl 95.38% <95.38%> (ø)
src/encode.jl 95.55% <95.55%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more Δ = absolute <relative> (impact), ø = not affected, ? = missing data Powered by Codecov. Last update 013210c...5798d88. Read the comment docs.

johnnychen94 commented 2 years ago

This becomes a big PR, so I will merge this once the test passes. Even though the benchmark is shown to be quite promising in the sense that we already get the best JPEG loading performance in the Julia world.

I still have some more ideas to tweak and I'll leave it to future PRs.

@Gnimuc thank you so much for helping me build this! It runs so smoothly with guidance from expert like you 😍