Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add play and explore for 3D and framestack image playback/interaction #43

Closed
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,31 @@ If you want to temporarily disable this package, you can call `ImageInTerminal.d
restore the encoding functionality with `ImageInTerminal.enable_encoding()`. `ImageInTerminal.use_24bit()`
and `ImageInTerminal.use_256()` will also enable encodings, too.

### Exploring framestacks and 3D image arrays

The `play` and `explore` functions allow playback and exploration of stacks of images, or to move along a given
dimension in a 3D image array.

```julia
using ImageInTerminal, TestImages
img3D = testimage("mri-stack")
explore(img, 3) # explore along dimension 3
```
![explore mri](https://user-images.githubusercontent.com/1694067/109374814-1351a280-7886-11eb-956e-c08403ae9afb.png)

```julia
using ImageInTerminal, ImageCore
framestack = map(_->rand(Gray{N0f8}, 100, 100), 1:200)
play(framestack, fps = 24) # play framestack back at a target of 24 fps
```

Control keys are available in both modes:

- `p` or `space-bar`: pause
- `left` or `up-arrow`: step backward
- `right` or `down-arrow`: step forward
- `ctrl-c` or `q`: exit

## Troubleshooting

If you see out of place horizontal lines in your Image it means
Expand Down
8 changes: 5 additions & 3 deletions src/ImageInTerminal.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ using ImageCore
using ImageTransformations

export

play,
explore,
colorant2ansi,
imshow,
imshow256,
Expand All @@ -14,6 +15,7 @@ export
include("colorant2ansi.jl")
include("encodeimg.jl")
include("imshow.jl")
include("multipage.jl")

# -------------------------------------------------------------------
# overload default show in the REPL for colorant (arrays)
Expand Down Expand Up @@ -62,7 +64,7 @@ enable_encoding() = (should_render_image[1] = true)
function Base.show(
io::IO, mime::MIME"text/plain",
img::AbstractArray{<:Colorant})
if should_render_image[1]
if should_render_image[1]
println(io, summary(img), ":")
ImageInTerminal.imshow(io, img, colormode[1])
else
Expand All @@ -87,7 +89,7 @@ function __init__()
# use 24bit if the terminal supports it
lowercase(get(ENV, "COLORTERM", "")) in ("24bit", "truecolor") && use_24bit()
enable_encoding()

if VERSION < v"1.6.0-DEV.888" && Sys.iswindows()
# https://discourse.julialang.org/t/image-in-repl-does-not-correct/46359
@warn "ImageInTerminal is not supported for Windows platform: Julia at least v1.6.0 is required."
Expand Down
117 changes: 117 additions & 0 deletions src/multipage.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@

ansi_moveup(n::Int) = string("\e[", n, "A")
ansi_movecol1 = "\e[1G"
ansi_cleartoend = "\e[0J"
ansi_enablecursor = "\e[?25h"
ansi_disablecursor = "\e[?25l"

setraw!(io, raw) = ccall(:jl_tty_set_mode, Int32, (Ptr{Cvoid},Int32), io.handle, raw)

"""
play(io::IO, arr::T, dim::Int; kwargs...)
play(io::IO, framestack::Vector{T}; kwargs...) where {T<:AbstractArray}

Play a video of a framestack of image arrays, or 3D array along dimension `dim`.

Control keys:
- `p` or `space-bar`: pause
- `left` or `up-arrow`: step backward
- `right` or `down-arrow`: step forward
- `ctrl-c` or `q`: exit

kwargs:
- `fps::Real=30`
- `maxsize::Tuple = displaysize(io)`
"""
function play(io::IO, arr::T, dim::Int; fps::Real=30, maxsize::Tuple = displaysize(io), paused = false) where {T<:AbstractArray}
@assert dim <= ndims(arr) "Requested dimension $dim, but source array only has $(ndims(arr)) dimensions"
@assert ndims(arr) <= 3 "Source array dimensions cannot exceed 3"
firstframe = T <: Vector ? first(selectdim(arr, dim, 1)) : selectdim(arr, dim, 1)
Copy link
Member

@johnnychen94 johnnychen94 Mar 7, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little bit dirty here to put the framestack support here. Indeed, a better strategy IMO is to put a lazy-view to convert Vector{<:AbstractArray{T, 2}} to Array{T, 3}.

- play(io::IO, framestack::Vector{T}; kwargs...) where {T<:AbstractArray} = play(io, framestack, 1; kwargs...)
+ play(io::IO, framestack::Vector{T}; kwargs...) where {T<:AbstractArray} = play(io, 3dview(framestack), 3; kwargs...)

Do you know if there's such a 3dview tool in Julia? Or I can make one for this.

@assert eltype(firstframe) <: Colorant "Element type $(eltype(firstframe)) not supported"
# sizing
img_w, img_h = size(firstframe)
io_h, io_w = maxsize
blocks = 3img_w <= io_w ? BigBlocks() : SmallBlocks()

# fixed
nframes = size(arr, dim)
c = ImageInTerminal.colormode[]

# vars
frame = 1
finished = false
first_print = true
actual_fps = 0

println(summary(firstframe))
keytask = @async begin
try
setraw!(stdin, true)
while !finished
keyin = read(stdin, Char)
if UInt8(keyin) == 27
johnnychen94 marked this conversation as resolved.
Show resolved Hide resolved
keyin = read(stdin, Char)
if UInt8(keyin) == 91
johnnychen94 marked this conversation as resolved.
Show resolved Hide resolved
keyin = read(stdin, Char)
UInt8(keyin) in [68,65] && (frame = frame <= 1 ? 1 : frame - 1) # left & up arrows
UInt8(keyin) in [67,66] && (frame = frame >= nframes ? nframes : frame + 1) # right & down arrows
end
end
keyin in ['p',' '] && (paused = !paused)
keyin in ['\x03','q'] && (finished = true)
end
catch
finally
setraw!(stdin, false)
end
end
try
print(ansi_disablecursor)
setraw!(stdin, true)
while !finished
tim = Timer(1/fps)
t = @elapsed begin
img = T <: Vector ? collect(first(selectdim(arr, dim, frame))) : selectdim(arr, dim, frame)
lines, rows, cols = encodeimg(blocks, c, img, io_h, io_w)
str = sprint() do ios
println.((ios,), lines)
if paused
println(ios, "Preview: $(cols)x$(rows) Frame: $frame/$nframes", " "^15)
else
println(ios, "Preview: $(cols)x$(rows) Frame: $frame/$nframes FPS: $(round(actual_fps, digits=1))", " "^5)
end
println(ios, "exit: ctrl-c. play/pause: space-bar. seek: arrow keys")
end
first_print ? print(str) : print(ansi_moveup(rows+2), ansi_movecol1, str)
first_print = false
(!paused && frame == nframes) && break
!paused && (frame += 1)
wait(tim)
end
actual_fps = 1 / t
end
catch e
isa(e,InterruptException) || rethrow()
finally
print(ansi_enablecursor)
finished = true
@async Base.throwto(keytask, InterruptException())
wait(keytask)
end
return
end
play(arr::T, dim::Int; kwargs...) where {T<:AbstractArray} = play(stdout, arr, dim; kwargs...)
play(io::IO, framestack::Vector{T}; kwargs...) where {T<:AbstractArray} = play(io, framestack, 1; kwargs...)

"""
explore(io::IO, arr::T, dim::Int; kwargs...) where {T<:AbstractArray}
explore(arr::T, dim::Int; kwargs...) where {T<:AbstractArray}
explore(io::IO, framestack::Vector{T}; kwargs...) where {T<:AbstractArray}
explore(framestack::Vector{T}; kwargs...) where {T<:AbstractArray}

Like `play`, but starts paused
"""
explore(io::IO, arr::T, dim::Int; kwargs...) where {T<:AbstractArray} = play(io, arr, dim; paused=true, kwargs...)
explore(arr::T, dim::Int; kwargs...) where {T<:AbstractArray} = play(stdout, arr, dim; paused=true, kwargs...)
explore(io::IO, framestack::Vector{T}; kwargs...) where {T<:AbstractArray} = explore(io, framestack, 1; kwargs...)
explore(framestack::Vector{T}; kwargs...) where {T<:AbstractArray} = explore(stdout, framestack, 1; kwargs...)