I am trying to make a Makie
recipe for a sequence of images with the current image frame from the sequence being an Observable
(frame
). I would like to have the separate frames of the image load on demand, when the frame
changes.
Attempt 1
My first attempt was to create the desired image plot when the frame changes. Here it is:
using DataStructures, Images
using Makie
import Makie: convert_arguments
import MakieCore: argument_names
@recipe(ImageSequence) do scene
Attributes(
image_attributes = (; interpolate = false, inspectable = false)
)
end
argument_names(::Type{<:ImageSequence}, numargs::Integer) = numargs == 2 && (:frame, :sequence)
function Makie.plot!(
im_sequence::ImageSequence{<:Tuple{Integer,AbstractVector{<:AbstractMatrix{<:Colorant}}}})
imageframes = fill(nothing, length(im_sequence[:sequence][])) |> Vector{Union{Nothing, AbstractPlot}}
visitedframes = CircularBuffer(3)
on(im_sequence[:frame]) do frame
foreach(f -> begin # make the previous plots invisible
# NOTE: It is possible to notify frame without changing it
if f != frame
imageframes[f].visible[] = false
end
end, visitedframes)
if isnothing(imageframes[frame])
imageframes[frame] = image!(im_sequence, im_sequence[:sequence][][frame]; im_sequence.image_attributes..., visible = true)
end
push!(visitedframes, frame)
end
notify(im_sequence[:frame])
return im_sequence
end
And then I call it
l = imagesequence(1, img_vec)
Which shows the first frame correctly,
but on an attempt to change the frame,
l.plot[:frame][] = 2
an empty axis appears.
But the plot of image frame 2
is included…
julia> l.plot.plots
> 2-element Vector{AbstractPlot}:
Image{Tuple{StepRangeLen{Float32, Float64, Float64, Int64}, StepRangeLen{Float32, Float64, Float64, Int64}, Matrix{RGB
A{Float32}}}}
Image{Tuple{StepRangeLen{Float32, Float64, Float64, Int64}, StepRangeLen{Float32, Float64, Float64, Int64}, Matrix{RGB
A{Float32}}}}
And l.plot.plots[2].visible[] == true
so there is no issue on that front.
I thought that to associate the plot with the given recipe it has to be created in the Makie.plot!
function, so I tried to create them using @async
Attempt 2
using Distributed
using DataStructures, Images
using Makie
import Makie: convert_arguments
import MakieCore: argument_names
@recipe(ImageSequence) do scene
Attributes(
image_attributes = (; interpolate = false, inspectable = false)
)
end
argument_names(::Type{<:ImageSequence}, numargs::Integer) = numargs == 2 && (:frame, :sequence)
function Makie.plot!(
im_sequence::ImageSequence{<:Tuple{Integer,AbstractVector{<:AbstractMatrix{<:Colorant}}}})
imageframes = fill(nothing, length(im_sequence[:sequence][])) |> Vector{Union{Nothing,AbstractPlot}}
imageframetasks = Vector(undef, length(im_sequence[:sequence][]))
for i in 1:length(imageframes)
imageframetasks[i] = @async imageframes[i] = image!(im_sequence, im_sequence[:sequence][][i]; im_sequence.image_attributes..., visible = false)
end
visitedframes = CircularBuffer(3)
on(im_sequence[:frame]) do frame
wait(imageframetasks[frame])
foreach(f -> begin # make the previous plots invisible
# NOTE: It is possible to notify frame without changing it
if f != frame
imageframes[f].visible[] = false
end
end, visitedframes)
imageframes[frame].visible[] = true
push!(visitedframes, frame)
end
notify(im_sequence[:frame])
return im_sequence
end
which works correctly when using the frame, but it takes the same time as my original synchronous version for some reason.
julia> @time l = ANT.Recipes.imagesequence(1, img_vec)
44.776909 seconds (1.51 G allocations: 86.338 GiB, 46.70% gc time, 1.48% compilation time)
Synchronous version
using DataStructures, Images
using Makie
import Makie: convert_arguments
import MakieCore: argument_names
@recipe(ImageSequence) do scene
Attributes(
# Attributes to be used for the image plot. Including visible would break the mechanism for
# showing the correct frame, so do not include `visible = true`.
image_attributes = (; interpolate = false, inspectable = false)
)
end
argument_names(::Type{<:ImageSequence}, numargs::Integer) = numargs == 2 && (:frame, :sequence)
function Makie.plot!(
im_sequence::ImageSequence{<:Tuple{Integer,AbstractVector{<:AbstractMatrix{<:Colorant}}}})
imageframes = Vector(undef, length(im_sequence[:sequence][]))
for i in 1:length(imageframes)
imageframes[i] = image!(im_sequence, im_sequence[:sequence][][i]; im_sequence.image_attributes..., visible = false)
end
visitedframes = CircularBuffer(3)
on(im_sequence[:frame]) do frame
foreach(f -> begin # make the previous plots invisible
# NOTE: It is possible to notify frame without changing it
if f != frame
imageframes[f].visible[] = false
end
end, visitedframes)
imageframes[frame].visible[] = true
push!(visitedframes, frame)
end
notify(im_sequence[:frame])
return im_sequence
end
Which works as expected, but takes a long time (same as Attempt 2)
julia> @time l = imagesequence(1, img_vec)
41.936290 seconds (1.51 G allocations: 86.258 GiB, 46.83% gc time, 0.09% compilation time)
Also, do you have an idea, why there are so much allocations? The original image sequence size is
.rw-r--r-- 252M kunzaatko 21 Mar 2021 TIRF488_cam1_0_L.tiff
and when only showing one frame from the total of 60 the allocation size is much smaller:
julia> @time image(img[:,:,1])
0.026145 seconds (45.76 k allocations: 38.600 MiB)
(And also it is a mystery to me as to why the loading of the images takes so long, when
julia> 60 * 0.026145
> 1.5687
so why doesn’t it take only one this time to load them all?)