Skip to content

Commit b6ba7ce

Browse files
authored
Merge pull request #17 from tlnagy/tn/eager-interpretation
Various improvements to eager realization
2 parents f45bf93 + 484d841 commit b6ba7ce

File tree

11 files changed

+177
-72
lines changed

11 files changed

+177
-72
lines changed

src/TIFF.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ include("files.jl")
1919
include("compression.jl")
2020
include("tags.jl")
2121
include("ifds.jl")
22+
include("layout.jl")
2223
include(joinpath("types", "common.jl"))
2324
include(joinpath("types", "dense.jl"))
2425
include("load.jl")

src/compression.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ Base.read!(tf::TiffFile, arr::AbstractArray, comp::CompressionType) = read!(tf,
99

1010
Base.read!(tf::TiffFile, arr::AbstractArray, ::Val{COMPRESSION_NONE}) = read!(tf, arr)
1111

12-
function Base.read!(tf::TiffFile, arr::AbstractArray, ::Val{COMPRESSION_PACKBITS})
12+
function Base.read!(tf::TiffFile, arr::AbstractArray{T, N}, ::Val{COMPRESSION_PACKBITS}) where {T, N}
1313
pos = 1
1414
nbit = Array{Int8}(undef, 1)
15-
nxt = Array{UInt8}(undef, 1)
15+
nxt = Array{T}(undef, 1)
1616
while pos < length(arr)
1717
read!(tf, nbit)
1818
n = nbit[1]
@@ -38,7 +38,7 @@ julia> TIFF.get_inflator(first(methods(read!, [TIFF.TiffFile, AbstractArray, Val
3838
COMPRESSION_NONE::CompressionType = 1
3939
```
4040
"""
41-
get_inflator(::Type{Tuple{F, T, A, Val{C}}}) where {F, T, A, C} = C
41+
get_inflator(::Type{Tuple{typeof(read!), TiffFile, AbstractArray{T, N} where {T, N}, Val{C}}}) where C = C
4242

4343
# autogenerate nice error messages for all non-implemented inflation methods
4444
implemented = map(x->get_inflator(x.sig), methods(read!, [TiffFile, AbstractArray, Val], ))

src/enum.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,4 +503,10 @@ end
503503
COMPRESSION_SGILOG24 = 34677 # SGI Log 24-bit packed
504504
COMPRESSION_JP2000 = 34712 # Leadtools JPEG2000
505505
COMPRESSION_LZMA = 34925 # LZMA2
506+
end
507+
508+
@enum ExtraSamples begin
509+
EXTRASAMPLE_UNSPECIFIED = 0 # unspecified data
510+
EXTRASAMPLE_ASSOCALPHA = 1 # associated alpha data
511+
EXTRASAMPLE_UNASSALPHA = 2 # unassociated alpha data
506512
end

src/files.jl

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,10 @@ function Base.read!(io::IOStream, arr::SubArray{T,N,P,I,L}) where {T, N, P <: Bi
8888
error("Strided bilevel TIFFs are not yet supported. Please open an issue against TIFF.jl.")
8989
end
9090

91-
function Base.read!(io::IOStream, arr::SubArray{T,N,P,I,L}) where {T, N, P <: BitArray, I <: Tuple{Base.Slice, Int64}, L}
92-
rng = arr.offset1 .+ arr.indices[1]
93-
n = length(rng)
94-
Bc = view(parent(arr).chunks, (Base.get_chunks_id(rng.start)[1]):(Base.get_chunks_id(rng.stop)[1]))
95-
nc = length(read!(io, Bc))
91+
function Base.read!(file::TiffFile, arr::BitArray)
92+
Bc = arr.chunks
93+
n = length(arr)
94+
nc = length(read!(file.io, Bc))
9695
if length(Bc) > 0 && Bc[end] & Base._msk_end(n) Bc[end]
9796
Bc[end] &= Base._msk_end(n) # ensure that the BitArray is not broken
9897
end

src/ifds.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Base.keys(ifd::IFD) = keys(ifd.tags)
99
Base.iterate(ifd::IFD) = iterate(ifd.tags)
1010
Base.iterate(ifd::IFD, n::Integer) = iterate(ifd.tags, n)
1111
Base.getindex(ifd::IFD, key::TiffTag) = getindex(ifd, UInt16(key))
12+
Base.getindex(ifd::IFD{O}, key::UInt16) where {O} = getindex(ifd.tags, key)
1213
Base.in(key::TiffTag, v::IFD) = in(UInt16(key), v)
1314
Base.in(key::UInt16, v::IFD) = in(key, keys(v))
1415
Base.delete!(ifd::IFD, key::TiffTag) = delete!(ifd, UInt16(key))
@@ -22,7 +23,6 @@ function Base.setindex!(ifd::IFD{O}, value, key::UInt16) where {O <: Unsigned}
2223
setindex!(ifd, Tag{O}(key, value), UInt16(key))
2324
end
2425

25-
Base.getindex(ifd::IFD{O}, key::UInt16) where {O} = getindex(ifd.tags, key)
2626

2727
function load!(tf::TiffFile, ifd::IFD)
2828
for idx in keys(ifd)

src/layout.jl

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
nrows(ifd::IFD) = Int(first(ifd[IMAGELENGTH].data))
2+
ncols(ifd::IFD) = Int(first(ifd[IMAGEWIDTH].data))
3+
nsamples(ifd::IFD) = Int(first(ifd[SAMPLESPERPIXEL].data))
4+
5+
"""
6+
interpretation(ifd)
7+
8+
For a given IFD, determine the proper colorimetric interpretation of the data.
9+
It returns subtypes of `Colorant` depending on the values of the tiff tags and
10+
whether there are extrasamples it doesn't know how to deal with.
11+
"""
12+
function interpretation(ifd::IFD)
13+
interp = PhotometricInterpretations(first(ifd[PHOTOMETRIC].data))
14+
extras = EXTRASAMPLE_UNSPECIFIED
15+
if EXTRASAMPLES in ifd
16+
try
17+
extras = ExtraSamples(first(ifd[EXTRASAMPLES].data))
18+
catch
19+
extras = EXTRASAMPLE_ASSOCALPHA
20+
end
21+
end
22+
interpretation(interp, extras, nsamples(ifd))
23+
end
24+
25+
# dummy color type for palette colored images to dispatch on
26+
struct Palette{T} <: Colorant{T, 1}
27+
i::T
28+
end
29+
Base.reinterpret(::Type{Palette{T}}, arr::A) where {T, N, S, A <: AbstractArray{S, N}} = arr
30+
31+
interpretation(p::PhotometricInterpretations) = interpretation(Val(p))
32+
interpretation(::Val{PHOTOMETRIC_RGB}) = RGB
33+
interpretation(::Val{PHOTOMETRIC_MINISBLACK}) = Gray
34+
interpretation(::Val{PHOTOMETRIC_PALETTE}) = Palette
35+
interpretation(::Val{PHOTOMETRIC_YCBCR}) = YCbCr
36+
interpretation(::Val{PHOTOMETRIC_CIELAB}) = Lab
37+
38+
function interpretation(p::PhotometricInterpretations, extrasamples::ExtraSamples, samplesperpixel::Int)
39+
interp = interpretation(p)
40+
len = length(interp)
41+
if len + 1 == samplesperpixel
42+
return interpretation(p, extrasamples, Val(samplesperpixel))
43+
elseif len == samplesperpixel
44+
return interp, false
45+
elseif len < samplesperpixel
46+
return interp, true
47+
else
48+
error("TIFF file says it contains $interp values, but only has $samplesperpixel samples per pixel instead of the minimum required $(length(interp))")
49+
end
50+
end
51+
_pad(::Type{RGB}) = RGBX
52+
_pad(::Type{T}) where {T} = T
53+
54+
interpretation(p::PhotometricInterpretations, extrasamples::ExtraSamples, nsamples::Val) = interpretation(p, Val(extrasamples), nsamples)
55+
interpretation(p::PhotometricInterpretations, ::Val{EXTRASAMPLE_UNSPECIFIED}, ::Val) = interpretation(p), true
56+
interpretation(p::PhotometricInterpretations, ::Val{EXTRASAMPLE_UNSPECIFIED}, ::Val{4}) = _pad(interpretation(p)), false
57+
interpretation(p::PhotometricInterpretations, ::Val{EXTRASAMPLE_ASSOCALPHA}, ::Val) = coloralpha(interpretation(p)), false
58+
interpretation(p::PhotometricInterpretations, ::Val{EXTRASAMPLE_UNASSALPHA}, nsamples::Val) = interpretation(p, Val(EXTRASAMPLE_ASSOCALPHA), nsamples)
59+
60+
_mappedtype(::Type{T}) where {T} = T
61+
_mappedtype(::Type{T}) where {T <: Unsigned} = Normed{T, sizeof(T) * 8}
62+
_mappedtype(::Type{T}) where {T <: Signed} = Fixed{T, sizeof(T) * 8 - 1}
63+
64+
function rawtype(ifd::IFD)
65+
samplesperpixel = nsamples(ifd)
66+
bitsperpixel = ifd[BITSPERSAMPLE].data
67+
sampleformats = fill(UInt16(0x01), samplesperpixel)
68+
if SAMPLEFORMAT in ifd
69+
sampleformats = ifd[SAMPLEFORMAT].data
70+
end
71+
rawtype_mapping[SampleFormats(first(sampleformats)), first(bitsperpixel)]
72+
end
73+
74+

src/load.jl

Lines changed: 41 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
function load(filepath::String; verbose=false)
1+
function load(filepath::String; verbose=true)
22
open(filepath) do io
33
load(io; verbose=verbose)
44
end
@@ -29,79 +29,61 @@ function load(io::IOStream; verbose=true)
2929

3030
nplanes += 1
3131
end
32-
if layout.rawtype == Bool
33-
slice = view(BitArray(undef, layout.nbytes*8), :, 1)
34-
else
35-
slice = Array{layout.readtype}(undef, layout.nbytes÷sizeof(layout.readtype))
36-
end
37-
38-
ifds_iter = Iterators.Stateful(ifds)
3932

40-
# load first slice
41-
ifd = popfirst!(ifds_iter)
42-
trans = load(slice, tf, layout, ifd)
43-
44-
# construct the final realized array from the lazy wrappers
45-
data = Array{eltype(trans)}(undef, size(trans)..., nplanes)
46-
plane_dim = length(size(data))
47-
# set the first plane to the data in trans
48-
selectdim(data, plane_dim, 1) .= trans
49-
50-
freq = verbose ? 1 : Inf
51-
@showprogress freq for (idx, ifd) in enumerate(ifds_iter)
52-
trans = load(slice, tf, layout, ifd)
53-
selectdim(data, plane_dim, idx+1) .= trans
54-
end
55-
56-
if layout.interpretation == PHOTOMETRIC_PALETTE
33+
loaded = load(tf, layout, ifds, Val(nplanes); verbose=verbose)
34+
35+
if eltype(loaded) <: Palette
36+
ifd = ifds[1]
37+
raw = rawtype(ifd)
38+
loadedr = reinterpret(raw, loaded)
5739
maxdepth = 2^(first(ifd[BITSPERSAMPLE].data))-1
5840
colors = ifd[COLORMAP].data
5941
color_map = vec(reinterpret(RGB{N0f16}, reshape(colors, :, 3)'))
60-
data = IndirectArray(data, OffsetArray(color_map, 0:maxdepth))
42+
data = IndirectArray(loadedr, OffsetArray(color_map, 0:maxdepth))
43+
elseif eltype(loaded) <: Bool
44+
data = Gray.(loaded)
45+
else
46+
data = loaded
6147
end
6248

6349
close(tf.io)
64-
todrop = tuple(findall(size(data) .== 1)...)
65-
DenseTaggedImage(dropdims(data, dims=todrop), ifds)
50+
return DenseTaggedImage(data, ifds)
6651
end
6752

68-
"""
69-
load(prealloc, tf, layout, ifd)
70-
71-
Read the raw data from `tf` into `prealloc` and then lazily transform the latter
72-
based on the information in `layout` and `ifd`. The returned array can later be
73-
realized to unwrap the lazy transforms.
74-
"""
75-
function load(prealloc::AbstractVector, tf::TiffFile, layout::IFDLayout, ifd::IFD)
76-
read!(prealloc, tf, ifd)
77-
78-
data = reinterpret(layout.rawtype, prealloc)
53+
function load(tf::TiffFile, layout::IFDLayout, ifds, ::Val{1}; verbose = true)
54+
ifd = ifds[1]
7955

80-
trans = reshape(data, :, layout.nrows)
56+
colortype, extras = interpretation(ifd)
57+
8158
if layout.rawtype == Bool
82-
trans = view(trans, 1:layout.ncols, 1:layout.nrows)
59+
cache = BitArray(undef, ncols(ifd), nrows(ifd))
60+
else
61+
cache = Array{colortype{layout.mappedtype}}(undef, ncols(ifd), nrows(ifd))
8362
end
63+
64+
read!(cache, tf, ifd)
65+
66+
return Array(cache')
67+
end
68+
69+
function load(tf::TiffFile, layout::IFDLayout, ifds, ::Val{N}; verbose = true) where {N}
70+
ifd = ifds[1]
8471

85-
if layout.nsamples > 1
86-
trans = PermutedDimsArray(reshape(trans, layout.nsamples, layout.ncols, layout.nrows), [1, 3, 2])
72+
colortype, extras = interpretation(ifd)
73+
74+
if layout.rawtype == Bool
75+
cache = BitArray(undef, ncols(ifd), nrows(ifd))
8776
else
88-
trans = PermutedDimsArray(trans, [2, 1])
77+
cache = Array{colortype{layout.mappedtype}}(undef, ncols(ifd), nrows(ifd))
8978
end
9079

91-
colortype = nothing
92-
if layout.interpretation != PHOTOMETRIC_PALETTE
93-
if layout.interpretation == PHOTOMETRIC_MINISBLACK
94-
colortype = Gray{layout.mappedtype}
95-
if layout.nsamples > 1
96-
trans = view(trans, 1, :, :)
97-
end
98-
elseif layout.interpretation == PHOTOMETRIC_RGB
99-
colortype = RGB{layout.mappedtype}
100-
trans = view(trans, 1:3, :, :)
101-
else
102-
error("Given TIFF requests $(layout.interpretation) interpretation, but that's not yet supported")
103-
end
104-
trans = reinterpret(colortype, trans)
80+
data = similar(cache, nrows(ifd), ncols(ifd), N)
81+
82+
freq = verbose ? 1 : Inf
83+
@showprogress freq for (idx, ifd) in enumerate(ifds)
84+
read!(cache, tf, ifd)
85+
data[:, :, idx] .= cache'
10586
end
106-
trans
87+
88+
return data
10789
end

src/utils.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,7 @@ const rawtype_mapping = Dict(
136136
(SAMPLEFORMAT_IEEEFP, 32) => Float32,
137137
(SAMPLEFORMAT_IEEEFP, 64) => Float64,
138138
)
139+
140+
Base.bswap(d::FixedPoint{T, N}) where {T, N} = FixedPointNumber.rawone(FixedPoint{T, N}(bswap(d)))
141+
Base.bswap(c::Colorant{T, N}) where {T, N} = mapc(bswap, c)
142+
Base.bswap(c::Colorant{<: FixedPoint{UInt8, N1}, N2}) where {N1, N2} = c

test/layouts.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
@testset "Interpretations" begin
2+
ifd = TIFF.IFD(UInt32)
3+
ifd[TIFF.PHOTOMETRIC] = TIFF.PHOTOMETRIC_MINISBLACK
4+
ifd[TIFF.SAMPLESPERPIXEL] = 1
5+
6+
@test TIFF.interpretation(ifd) == (Gray, false)
7+
8+
ifd[TIFF.SAMPLESPERPIXEL] = 2
9+
10+
@test TIFF.interpretation(ifd) == (Gray, true)
11+
12+
ifd[TIFF.EXTRASAMPLES] = TIFF.EXTRASAMPLE_ASSOCALPHA
13+
14+
@test TIFF.interpretation(ifd) == (GrayA, false)
15+
16+
ifd[TIFF.EXTRASAMPLES] = TIFF.EXTRASAMPLE_UNSPECIFIED
17+
18+
@test TIFF.interpretation(ifd) == (Gray, true)
19+
20+
ifd[TIFF.SAMPLESPERPIXEL] = 3
21+
22+
@test TIFF.interpretation(ifd) == (Gray, true)
23+
24+
ifd[TIFF.PHOTOMETRIC] = TIFF.PHOTOMETRIC_RGB
25+
26+
@test TIFF.interpretation(ifd) == (RGB, false)
27+
28+
ifd[TIFF.SAMPLESPERPIXEL] = 4
29+
30+
@test TIFF.interpretation(ifd) == (RGBX, false)
31+
32+
ifd[TIFF.EXTRASAMPLES] = TIFF.EXTRASAMPLE_ASSOCALPHA
33+
34+
@test TIFF.interpretation(ifd) == (RGBA, false)
35+
end

test/runtests.jl

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ get_example(name) = download("https://github.com/tlnagy/exampletiffs/blob/master
1515
filepath = get_example("house.tif")
1616
img = TIFF.load(filepath)
1717
@test size(img) == (512, 512)
18-
@test eltype(img) == Gray{N0f8}
19-
@test img[50,50] == Gray{N0f8}(0.804) # value from ImageMagick.jl
18+
@test eltype(img) == GrayA{N0f8}
19+
@test img[50,50] == GrayA{N0f8}(0.804, 1.0) # value from ImageMagick.jl
2020
img[50:300, 50:150] .= 0.0
21-
@test img[50, 50] == Gray{N0f8}(0.0)
21+
@test img[50, 50] == GrayA{N0f8}(0.0, 1.0)
2222
end
2323

2424
@testset "MRI stack" begin
@@ -92,4 +92,8 @@ end
9292

9393
@testset "Writing" begin
9494
include("writer.jl")
95+
end
96+
97+
@testset "Interpreting IFD layouts" begin
98+
include("layouts.jl")
9599
end

0 commit comments

Comments
 (0)