Skip to content

Commit

Permalink
DiskTaggedImage -> LazyBufferedTiff
Browse files Browse the repository at this point in the history
  • Loading branch information
timholy committed May 26, 2022
1 parent 8abcb3d commit c7b5b8d
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 27 deletions.
3 changes: 2 additions & 1 deletion docs/src/lib/public.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ memmap

```@docs
TiffImages.DenseTaggedImage
TiffImages.DiskTaggedImage
TiffImages.LazyBufferedTIFF
TiffImages.MmappedTIFF
```
2 changes: 1 addition & 1 deletion src/load.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function load(tf::TiffFile; verbose=true, mmap = false, lazyio = false)
return MmappedTIFF(tf, ifds)
elseif lazyio || mmap
mmap && @warn "Compression and discontiguous planes are not supported by `mmap`, use `lazyio = true` instead"
loaded = DiskTaggedImage(tf, ifds)
loaded = LazyBufferedTIFF(tf, ifds)
else
if nplanes == 1
loaded = load(tf, ifds, nothing; verbose=verbose)
Expand Down
54 changes: 30 additions & 24 deletions src/types/lazy.jl
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
"""
$(TYPEDEF)
A type to represent memory-mapped TIFF data. Useful for opening and operating on
images too large to store in memory.
A type to represent lazily-loaded TIFF data, returned by `TiffImages.load(filepath; lazyio=true)`.
Useful for opening and operating on images too large to store in memory,
and for incrementally writing new TIFF files.
This works by buffering individual slices. This allows broad format support, including compressed
TIFFs, but with mixed performance depending on your specific access (indexing) patterns.
See discussion in the package documentation, and [`MmappedTIFF`](@ref) for an alternative
with different strengths and weaknesses.
```jldoctest
julia> using TiffImages, ColorTypes
julia> img = TiffImages.memmap(Gray{Float32}, joinpath(mktempdir(), "test.tif"))
32-bit DiskTaggedImage{Gray{Float32}} 0×0×0 (writable)
32-bit LazyBufferedTIFF{Gray{Float32}} 0×0×0 (writable)
Current file size on disk: 8 bytes
Addressable space remaining: 4.000 GiB
```
$(FIELDS)
"""
mutable struct DiskTaggedImage{T <: Colorant, O <: Unsigned, AA <: AbstractArray} <: AbstractDenseTIFF{T, 3}
mutable struct LazyBufferedTIFF{T <: Colorant, O <: Unsigned, AA <: AbstractArray} <: AbstractDenseTIFF{T, 3}

"""
Pointer to keep track of the backing file
Expand Down Expand Up @@ -48,33 +54,33 @@ mutable struct DiskTaggedImage{T <: Colorant, O <: Unsigned, AA <: AbstractArray
"""
readonly::Bool

function DiskTaggedImage(file::TiffFile{O}, ifds::Vector{IFD{O}}, dims, cache::AA, cache_index::Int, last_ifd_offset::O, readonly::Bool) where {O, AA <: AbstractArray}
function LazyBufferedTIFF(file::TiffFile{O}, ifds::Vector{IFD{O}}, dims, cache::AA, cache_index::Int, last_ifd_offset::O, readonly::Bool) where {O, AA <: AbstractArray}
new{eltype(cache), O, typeof(cache)}(file, ifds, dims, cache, cache_index, last_ifd_offset, readonly)
end
end

function DiskTaggedImage(file::TiffFile{O}, ifds::Vector{IFD{O}}) where {O}
function LazyBufferedTIFF(file::TiffFile{O}, ifds::Vector{IFD{O}}) where {O}
ifd = ifds[1]
dims = (nrows(ifd), ncols(ifd), length(ifds))
cache = getcache(ifd)
DiskTaggedImage(file, ifds, dims, cache, -1, zero(O), true)
LazyBufferedTIFF(file, ifds, dims, cache, -1, zero(O), true)
end

"""
memmap(T, filepath; bigtiff)
Create a new memory-mapped file ready with element type `T` for appending future
slices. The `bigtiff` flag, if true, allows 64-bit offsets for data larger than
~4GB.
~4GB.
```jldoctest; setup=:(rm("test.tif", force=true))
julia> using ColorTypes, FixedPointNumbers # for Gray{N0f8} type
julia> img = memmap(Gray{N0f8}, "test.tif"); # make memory-mapped image
julia> push!(img, rand(Gray{N0f8}, 100, 100));
julia> push!(img, rand(Gray{N0f8}, 100, 100));
julia> push!(img, rand(Gray{N0f8}, 100, 100));
julia> push!(img, rand(Gray{N0f8}, 100, 100));
julia> size(img)
(100, 100, 2)
Expand All @@ -84,22 +90,22 @@ function memmap(::Type{T}, filepath; bigtiff=false) where {T <: Colorant}
if isfile(filepath)
error("This file already exists, please use `TiffImages.load` to open")
end
DiskTaggedImage(T, getstream(format"TIFF", open(filepath, "w+"), filepath); bigtiff = bigtiff)
LazyBufferedTIFF(T, getstream(format"TIFF", open(filepath, "w+"), filepath); bigtiff = bigtiff)
end

function DiskTaggedImage(::Type{T}, io::Stream; bigtiff = false) where {T}
function LazyBufferedTIFF(::Type{T}, io::Stream; bigtiff = false) where {T}
O = bigtiff ? UInt64 : UInt32
tf = TiffFile{O}(io)

last_ifd_offset = write(tf) # write out header

DiskTaggedImage(tf, IFD{O}[], (0, 0, 0), Array{T}(undef, 1, 1), -1, O(last_ifd_offset), false)
LazyBufferedTIFF(tf, IFD{O}[], (0, 0, 0), Array{T}(undef, 1, 1), -1, O(last_ifd_offset), false)
end

Base.size(A::DiskTaggedImage) = A.dims
offset(::DiskTaggedImage{T, O, AA}) where {T, O, AA} = O
Base.size(A::LazyBufferedTIFF) = A.dims
offset(::LazyBufferedTIFF{T, O, AA}) where {T, O, AA} = O

function Base.getindex(A::DiskTaggedImage{T, O, AA}, i1::Int, i2::Int, i::Int) where {T, O, AA}
function Base.getindex(A::LazyBufferedTIFF{T, O, AA}, i1::Int, i2::Int, i::Int) where {T, O, AA}
(size(A) == (0, 0, 0)) && error("This image has not been initialized, please `push!` data into it first")
# check the loaded cache is already the correct slice
if A.cache_index == i
Expand All @@ -121,23 +127,23 @@ function Base.getindex(A::DiskTaggedImage{T, O, AA}, i1::Int, i2::Int, i::Int) w
return A.cache[i2, i1]
end

function Base.setindex!(A::DiskTaggedImage, I...)
error("Unable to mutate inplace since this array is on disk. Convert to a mutable in-memory version by running "*
function Base.setindex!(A::LazyBufferedTIFF, I...)
error("Unable to mutate inplace since this array is on disk. Convert to a mutable in-memory version by running "*
"`copy(arr)`. \n\n𝗡𝗼𝘁𝗲: For large files this can be quite expensive. A future PR will add "*
"support for writing inplace to disk. See `push!` for appending to an array.")
end

"""
push!(img::DiskTaggedImage, slice::AbstractMatrix)
push!(img::LazyBufferedTIFF, slice::AbstractMatrix)
Push a 2D slice to a memory-mapped file. The slice must be the same `eltype` as the
target `img` and the `img` must be not be readonly.
"""
function Base.push!(A::DiskTaggedImage{T, O, AA}, data::AbstractMatrix{T}) where {T, O, AA}
function Base.push!(A::LazyBufferedTIFF{T, O, AA}, data::AbstractMatrix{T}) where {T, O, AA}
(A.readonly) && error("This image is read only")

if size(A) == (0, 0, 0) # if this is the initial slice pushed, initialize the size
A.dims = (size(data)..., 0)
A.dims = (size(data)..., 0)
end

@assert size(data) == (A.dims[1], A.dims[2]) "Pushed slices must have dimensions: $((A.dims[1], A.dims[2]))"
Expand All @@ -160,11 +166,11 @@ function Base.push!(A::DiskTaggedImage{T, O, AA}, data::AbstractMatrix{T}) where
A
end

Base.push!(A::DenseTaggedImage{T, N, O, AA}, data) where {T, N, O, AA <: DiskTaggedImage} = push!(A.data, data)
Base.push!(A::DenseTaggedImage{T, N, O, AA}, data) where {T, N, O, AA <: LazyBufferedTIFF} = push!(A.data, data)

function Base.show(io::IO, ::MIME"text/plain", A::DiskTaggedImage{T, O, AA}) where {T, O, AA}
function Base.show(io::IO, ::MIME"text/plain", A::LazyBufferedTIFF{T, O, AA}) where {T, O, AA}
printstyled(io, O == UInt32 ? "32-bit" : "64-bit"; color = :cyan)
print(io, " DiskTaggedImage{$(T)} ")
print(io, " LazyBufferedTIFF{$(T)} ")
printstyled(io, "$(size(A, 1))×$(size(A, 2))×$(size(A, 3))"; bold=true)
if A.readonly
printstyled(io, " (readonly)"; color=:red)
Expand Down
35 changes: 34 additions & 1 deletion src/types/mmapped.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
"""
$(TYPEDEF)
A type to represent memory-mapped TIFF data, returned by `TiffImages.load(filepath; mmap=true)`.
Useful for opening and operating on images too large to store in memory.
This works by exploiting the operating system's memory-mapping capabilities. This is not compatible
with certain TIFF options, including compression, but when applicable it gives good performance for
most access (indexing) patterns.
See discussion in the package documentation, and [`LazyBufferedTIFF`](@ref) for an alternative
with different strengths and weaknesses.
```
julia> using TiffImages
julia> img = TiffImages.load(filepath; mmap=true);
julia> print(summary(img))
200×541 TiffImages.MmappedTIFF{RGBA{N0f8}, 2}
```
Fields:
$(FIELDS)
"""
struct MmappedTIFF{T <: Colorant, N, O <: Unsigned, A <: AbstractMatrix{T}} <: AbstractTIFF{T, N}
"""
2d slices in the file"
2d slices in the file
"""
chunks::Vector{A}
"""
Expand Down Expand Up @@ -74,3 +99,11 @@ Base.@propagate_inbounds function Base.setindex!(img::MmappedTIFF{T, 3}, val, i:
chunk = img.chunks[k]
chunk[j, i] = val
end

function Base.summary(io::IO, X::MmappedTIFF)
print(io, Base.dims2string(size(X)), " TiffImages.MmappedTIFF{$(eltype(X)), $(ndims(X))}")
n = length(X.chunks)
if n > 1
print(io, " with $(length(X.chunks)) slice planes")
end
end

0 comments on commit c7b5b8d

Please sign in to comment.