Skip to content

Commit f262412

Browse files
committed
feat: initial jpeg_decode implementation
1 parent a0e5006 commit f262412

File tree

8 files changed

+245
-9
lines changed

8 files changed

+245
-9
lines changed

docs/src/reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CurrentModule = JpegTurbo
66

77
```@docs
88
jpeg_encode
9+
jpeg_decode
910
```
1011

1112
## Utilities

src/JpegTurbo.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ include("libjpeg_utils.jl")
88

99
include("common.jl")
1010
include("encode.jl")
11+
include("decode.jl")
1112

12-
export jpeg_encode
13+
export jpeg_encode, jpeg_decode
1314

1415
end

src/common.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,25 @@ jpeg_color_space(::Type{CT}) where CT<:RGBA = LibJpeg.JCS_EXT_RGBA
1010
jpeg_color_space(::Type{CT}) where CT<:BGRA = LibJpeg.JCS_EXT_BGRA
1111
jpeg_color_space(::Type{CT}) where CT<:ABGR = LibJpeg.JCS_EXT_ABGR
1212
jpeg_color_space(::Type{CT}) where CT<:ARGB = LibJpeg.JCS_EXT_ARGB
13+
14+
function jpeg_color_space(v::LibJpeg.J_COLOR_SPACE)::Type{<:Colorant}
15+
if v === LibJpeg.JCS_GRAYSCALE
16+
return Gray{N0f8}
17+
elseif v === LibJpeg.JCS_RGB
18+
return RGB{N0f8}
19+
elseif v === LibJpeg.JCS_EXT_RGB
20+
return RGB{N0f8}
21+
elseif v === LibJpeg.JCS_EXT_BGR
22+
return BGR{N0f8}
23+
elseif v === LibJpeg.JCS_EXT_RGBA
24+
return RGBA{N0f8}
25+
elseif v === LibJpeg.JCS_EXT_BGRA
26+
return BGRA{N0f8}
27+
elseif v === LibJpeg.JCS_EXT_ABGR
28+
return ABGR{N0f8}
29+
elseif v === LibJpeg.JCS_EXT_ARGB
30+
return ARGB{N0f8}
31+
else
32+
throw(ArgumentError("Unsupported colorspace $v"))
33+
end
34+
end

src/decode.jl

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""
2+
jpeg_decode([T,] filename::AbstractString; kwargs...) -> Matrix{T}
3+
4+
Decode the JPEG image from given I/O stream as colorant matrix.
5+
6+
# parameters
7+
8+
- `transpose::Bool`: whether we need to permute the image's width and height dimension
9+
before encoding. The default value is `false`.
10+
- `scale_ratio::Real`: scale the image by ratio `scale_ratio` in `M/8` with `M ∈ 1:16`. The
11+
default value is `1`. For values are not in the range, they will be mapped to the nearest
12+
value, e.g., `0.3 => 2/8` and `0.35 => 3/8`.
13+
14+
# Examples
15+
16+
```jldoctest
17+
julia> using JpegTurbo, TestImages, ImageCore
18+
19+
julia> filename = testimage("earth", download_only=true);
20+
21+
julia> img = jpeg_decode(filename); summary(img)
22+
"3002×3000 Array{RGB{N0f8},2} with eltype RGB{N0f8}"
23+
24+
julia> img = jpeg_decode(Gray, filename; scale_ratio=0.25); summary(img)
25+
"751×750 Array{Gray{N0f8},2} with eltype Gray{N0f8}"
26+
```
27+
28+
For image preview and similar purposes, `T` and `scale_ratio` are useful parameters to
29+
accelerate the JPEG decoding process. For color JPEG image, `jpeg_decode(Gray, filename)` is
30+
faster than `jpeg_decode(filename)` since the color components need not be processed.
31+
Smaller `scale_ratio` permits significantly faster decoding since fewer pixels need be
32+
processed and a simpler IDCT method can be used.
33+
34+
```julia
35+
using BenchmarkTools, TestImages, JpegTurbo
36+
filename = testimage("earth", download_only=true)
37+
# full decompression
38+
@btime jpeg_decode(filename); # 224.760 ms (7 allocations: 51.54 MiB)
39+
# only decompress luminance component
40+
@btime jpeg_decode(Gray, filename); # 91.157 ms (6 allocations: 17.18 MiB)
41+
# process only a few pixels
42+
@btime jpeg_decode(filename; scale_ratio=0.25); # 77.254 ms (8 allocations: 3.23 MiB)
43+
# process only a few pixels for luminance component
44+
@btime jpeg_decode(Gray, filename; scale_ratio=0.25); # 63.119 ms (6 allocations: 1.08 MiB)
45+
```
46+
47+
"""
48+
function jpeg_decode(
49+
::Type{CT},
50+
filename::AbstractString;
51+
transpose=false,
52+
scale_ratio=1) where CT<:Colorant
53+
infile = ccall(:fopen, Libc.FILE, (Cstring, Cstring), filename, "rb")
54+
@assert infile.ptr != C_NULL
55+
out_CT, jpeg_cls = _jpeg_out_color_space(CT)
56+
57+
local cinfo, out
58+
try
59+
cinfo = LibJpeg.jpeg_decompress_struct()
60+
cinfo_ref = Ref(cinfo)
61+
jerr = Ref{LibJpeg.jpeg_error_mgr}()
62+
cinfo.err = LibJpeg.jpeg_std_error(jerr)
63+
LibJpeg.jpeg_create_decompress(cinfo_ref)
64+
LibJpeg.jpeg_stdio_src(cinfo_ref, infile)
65+
LibJpeg.jpeg_read_header(cinfo_ref, true)
66+
67+
# set decompression parameters, if given
68+
r = _cal_scale_ratio(scale_ratio)
69+
cinfo.scale_num, cinfo.scale_denom = r.num, r.den
70+
cinfo.out_color_space = jpeg_cls
71+
72+
# eagerly calculate dimension information so that `output_XXX` fields are valid.
73+
LibJpeg.jpeg_calc_output_dimensions(cinfo_ref)
74+
out_size = (Int(cinfo.output_width), Int(cinfo.output_height))
75+
if !all(x -> x<=65535, out_size)
76+
error("Suspicious inferred image size $out_size: each dimension is expected to have at most 65535 pixels.")
77+
end
78+
out_ndims = Int(cinfo.output_components)
79+
@assert out_ndims == length(out_CT) "Suspicous output color space: $cinfo.out_color_space"
80+
81+
out = Matrix{out_CT}(undef, out_size)
82+
_jpeg_decode!(out, cinfo)
83+
finally
84+
LibJpeg.jpeg_destroy_decompress(Ref(cinfo))
85+
ccall(:fclose, Cint, (Ptr{Libc.FILE},), infile)
86+
end
87+
88+
if out_CT <: CT
89+
return transpose ? out : permutedims(out, (2, 1))
90+
else
91+
return transpose ? CT.(out) : CT.(PermutedDimsArray(out, (2, 1)))
92+
end
93+
end
94+
function jpeg_decode(filename::AbstractString; kwargs...)
95+
return jpeg_decode(_default_out_color_space(filename), filename; kwargs...)
96+
end
97+
98+
function _jpeg_decode!(out::Matrix{<:Colorant}, cinfo::LibJpeg.jpeg_decompress_struct)
99+
cinfo_ref = Ref(cinfo)
100+
101+
row_stride = size(out, 1) * length(eltype(out))
102+
buf = Vector{UInt8}(undef, row_stride)
103+
out_uint8 = reinterpret(UInt8, out)
104+
105+
LibJpeg.jpeg_start_decompress(cinfo_ref)
106+
while cinfo.output_scanline < cinfo.output_height
107+
# TODO(johnnychen94): try if we can directly write to `out` without using `buf`
108+
GC.@preserve buf LibJpeg.jpeg_read_scanlines(cinfo_ref, Ref(pointer(buf)), 1)
109+
copyto!(out_uint8, (cinfo.output_scanline-1) * row_stride + 1, buf, 1, row_stride)
110+
end
111+
LibJpeg.jpeg_finish_decompress(cinfo_ref)
112+
113+
return out
114+
end
115+
116+
# libjpeg-turbo only supports ratio M/8 with M from 1 to 16
117+
const _allowed_scale_ratios = ntuple(i->i//8, 16)
118+
_cal_scale_ratio(r::Real) = _allowed_scale_ratios[findmin(x->abs(x-r), _allowed_scale_ratios)[2]]
119+
120+
function _default_out_color_space(filename::AbstractString)
121+
infile = ccall(:fopen, Libc.FILE, (Cstring, Cstring), filename, "rb")
122+
@assert infile.ptr != C_NULL
123+
local cinfo
124+
try
125+
cinfo = LibJpeg.jpeg_decompress_struct()
126+
cinfo_ref = Ref(cinfo)
127+
jerr = Ref{LibJpeg.jpeg_error_mgr}()
128+
cinfo.err = LibJpeg.jpeg_std_error(jerr)
129+
LibJpeg.jpeg_create_decompress(cinfo_ref)
130+
LibJpeg.jpeg_stdio_src(cinfo_ref, infile)
131+
LibJpeg.jpeg_read_header(cinfo_ref, true)
132+
LibJpeg.jpeg_calc_output_dimensions(cinfo_ref)
133+
return jpeg_color_space(cinfo.out_color_space)
134+
finally
135+
LibJpeg.jpeg_destroy_decompress(Ref(cinfo))
136+
ccall(:fclose, Cint, (Ptr{Libc.FILE},), infile)
137+
end
138+
end
139+
140+
function _jpeg_out_color_space(::Type{CT}) where CT
141+
try
142+
n0f8(CT), jpeg_color_space(n0f8(CT))
143+
catch e
144+
@debug "Unsupported libjpeg-turbo color space, fallback to RGB{N0f8}" e
145+
RGB{N0f8}, jpeg_color_space(RGB{N0f8})
146+
end
147+
end

test/runtests.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ function decode_encode(img; kwargs...)
1313
tmpfile = joinpath(tmpdir, "tmp.jpg")
1414
buf = @inferred jpeg_encode(img; kwargs...)
1515
write(tmpfile, buf)
16-
# TODO(johnnychen94): load back with `JpegTurbo.decode`
17-
return ImageMagick.load(tmpfile)
16+
return jpeg_decode(tmpfile)
1817
end
1918

2019
@testset "JpegTurbo.jl" begin
@@ -47,6 +46,7 @@ end
4746
end
4847

4948
include("tst_encode.jl")
49+
include("tst_decode.jl")
5050
if Threads.nthreads() > 1
5151
@info "Multi-threads test: enabled"
5252
include("tst_multithreads.jl")

test/tst_decode.jl

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
@testset "jpeg_decode" begin
2+
img_rgb = testimage("lighthouse")
3+
4+
tmpfile = joinpath(tmpdir, "tmp.jpg")
5+
jpeg_encode(tmpfile, img_rgb)
6+
7+
data = jpeg_decode(tmpfile)
8+
9+
@test jpeg_decode(tmpfile; transpose=true) == data'
10+
11+
# ensure default keyword values are not changed by accident
12+
@test jpeg_decode(tmpfile)
13+
jpeg_decode(RGB, tmpfile; transpose=false, scale_ratio=1)
14+
jpeg_decode(tmpfile; transpose=false, scale_ratio=1)
15+
16+
17+
# TODO(johnnychen94): support IO and in-memory buffer
18+
@test_broken jpeg_decode(jpeg_encode(img_rgb))
19+
@test_broken open(jpeg_decode, tmpfile, "r")
20+
21+
@testset "colorspace" begin
22+
native_color_spaces = [Gray, RGB, BGR, RGBA, BGRA, ABGR, ARGB]
23+
ext_color_spaces = [YCbCr, RGBX, XRGB, Lab, YIQ] # supported by Colors.jl
24+
for CT in [native_color_spaces..., ext_color_spaces...]
25+
data = jpeg_decode(CT, tmpfile)
26+
@test eltype(data) <: CT
27+
if CT == Gray
28+
@test assess_psnr(data, Gray.(img_rgb)) > 34.92
29+
else
30+
@test assess_psnr(RGB.(data), img_rgb) > 33.87
31+
end
32+
end
33+
end
34+
35+
@testset "scale_ratio" begin
36+
data = jpeg_decode(tmpfile; scale_ratio=0.25)
37+
@test size(data) == (128, 192) == 0.25 .* size(img_rgb)
38+
39+
# `jpeg_decode` will map input `scale_ratio` to allowed values.
40+
data = jpeg_decode(tmpfile; scale_ratio=0.3)
41+
@test size(data) == (128, 192) != 0.3 .* size(img_rgb)
42+
end
43+
44+
@testset "transpose" begin
45+
jpeg_encode(tmpfile, img_rgb; transpose=true)
46+
data = jpeg_decode(tmpfile; transpose=true)
47+
@test assess_psnr(data, img_rgb) > 33.95
48+
end
49+
end

test/tst_encode.jl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ img_rgb = testimage("lighthouse")
66
for CT in [Gray, RGB, #=YCbCr,=# #=RGBX,=# BGR, #=XRGB,=# RGBA, BGRA, ABGR, ARGB]
77
img = CT.(img_rgb)
88
data = decode_encode(img)
9+
@test eltype(data) <: Union{Gray, RGB}
10+
@test size(data) == size(img)
911
@test data decode_encode(float32.(img))
1012

1113
# ensure default keyword values are not changed by accident
@@ -17,12 +19,12 @@ end
1719
# keyword checks
1820
@testset "quality" begin
1921
img = testimage("cameraman")
20-
psnr_refs = Dict(
21-
1 => 24.5647,
22-
10 => 31.3434,
23-
50 => 38.8765,
24-
100 => 59.3770,
25-
)
22+
psnr_refs = [
23+
1 => 24.63,
24+
10 => 31.34,
25+
50 => 38.87,
26+
100 => 59.31,
27+
]
2628
for (q, r) in psnr_refs
2729
v = assess_psnr(img, decode_encode(img, quality=q))
2830
@test v >= r

test/tst_multithreads.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,18 @@
1111
end
1212
@test all(out .== Ref(ref))
1313
end
14+
15+
@testset "jpeg_decode" begin
16+
out = [similar(img) for _ in 1:Threads.nthreads()]
17+
tmpdir = tempdir()
18+
19+
for i in 1:Threads.nthreads()
20+
jpeg_encode(joinpath(tmpdir, "$i.jpg"), img)
21+
end
22+
ref = jpeg_decode(joinpath(tmpdir, "1.jpg"))
23+
Threads.@threads for i in 1:Threads.nthreads()
24+
out[i] = jpeg_decode(joinpath(tmpdir, "$i.jpg"))
25+
end
26+
@test all(out .== Ref(ref))
27+
end
1428
end

0 commit comments

Comments
 (0)