Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnychen94 committed Apr 15, 2021
1 parent 1e04c64 commit 91c1b5e
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 78 deletions.
61 changes: 61 additions & 0 deletions src/keyboard.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# minimal keyboard event support
"""
read_key() -> control_value
read control key from keyboard input.
# Reference table
| value | control_value | effect |
| ------------------- | ----------------- | ------------------- |
| UP, LEFT, b | :CONTROL_BACKWARD | show previous frame |
| DOWN, RIGHT, f | :CONTROL_FORWARD | show next frame |
| SPACE, p | :CONTROL_PAUSE | pause/resume play |
| CTRL-c, q | :CONTROL_EXIT | exit current play |
| others... | :CONTROL_VOID | no effect |
"""
function read_key(io=stdin)
control_value = :CONTROL_VOID
try
_setraw!(io, true)
keyin = read(io, Char)
if keyin == '\e'
# some special keys are more than one byte, e.g., left key is `\e[D`
# reference: https://en.wikipedia.org/wiki/ANSI_escape_code
keyin = read(io, Char)
if keyin == '['
keyin = read(io, Char)
if keyin in ['A', 'D'] # up, left
control_value = :CONTROL_BACKWARD
elseif keyin in ['B', 'C'] # down, right
control_value = :CONTROL_FORWARD
end
end
elseif 'A' <= keyin <= 'Z' || 'a' <= keyin <= 'z'
keyin = lowercase(keyin)
if keyin == 'p'
control_value = :CONTROL_PAUSE
elseif keyin == 'q'
control_value = :CONTROL_EXIT
elseif keyin == 'f'
control_value = :CONTROL_FORWARD
elseif keyin == 'b'
control_value = :CONTROL_BACKWARD
end
elseif keyin == ' '
control_value = :CONTROL_PAUSE
end
catch e
if e isa InterruptException # Ctrl-C
control_value = :CONTROL_EXIT
else
rethrow(e)
end
finally
_setraw!(io, false)
end
return control_value
end

_setraw!(io::Base.TTY, raw) = ccall(:jl_tty_set_mode, Int32, (Ptr{Cvoid},Int32), io.handle, raw)
_setraw!(::IO, raw) = nothing
120 changes: 42 additions & 78 deletions src/multipage.jl
Original file line number Diff line number Diff line change
@@ -1,37 +1,51 @@
include("keyboard.jl")

ansi_moveup(n::Int) = string("\e[", n, "A")
const ansi_movecol1 = "\e[1G"

"""
play(framestack::AbstractVector{T}; kwargs...) where {T<:AbstractArray}
play(arr::T, dim::Int; kwargs...)
play(arr::T, dim=3; kwargs...)
Play a video of a framestack of image arrays, or 3D array along dimension `dim`.
Control keys:
- `p` or `space-bar`: pause/resume
- `f`, `←`(left arrow), or `↑`(up arrow): step backward
- `b`, `→`(right arrow), or `↓`(down arrow): step forward
- `b`, `←`(left arrow), or `↑`(up arrow): step backward
- `f`, `→`(right arrow), or `↓`(down arrow): step forward
- `ctrl-c` or `q`: exit
kwargs:
- `fps::Real=30`
- `fps`: frame per second.
# Examples
```julia
julia> using TestImages, ImageShow
julia> img3d = testimage("mri-stack") |> collect;
julia> ImageShow.play(img3d, 3)
using TestImages, ImageShow
julia> framestack = [img3d[:, :, i] for i in axes(img3d, 3)];
img3d = RGB.(testimage("mri-stack"))
ImageShow.play(img3d)
julia> ImageShow.play(framestack)
framestack = [img3d[:, :, i] for i in axes(img3d, 3)];
ImageShow.play(framestack)
```
"""
function play(framestack::AbstractVector{<:AbstractArray}; fps::Real=30, paused=false)
function play(framestack::AbstractVector{<:AbstractMatrix}; fps::Real=min(10, length(framestack)÷2))
# NOTE: the default fps is chosen purely by experience and may be changed in the future
_play(framestack; fps=fps, paused=false, quit_after_play=true)
end
play(img::AbstractArray{<:Colorant, 3}, dim=3; kwargs...) = play(map(i->selectdim(img, dim, i), axes(img, dim)); kwargs...)

function _play(
framestack::AbstractVector{<:AbstractMatrix};
fps, paused, quit_after_play,
# The following keywords are for advanced usages(e.g., test), common users are not expected
# to use them directly.
display_io::Union{Nothing, IO}=nothing,
summary_io::IO=stdout,
keyboard_io::IO=stdin
)
nframes = length(framestack)

# vars
Expand All @@ -44,18 +58,24 @@ function play(framestack::AbstractVector{<:AbstractArray}; fps::Real=30, paused=
cols, rows = size(frame)

if !first_frame
print(ansi_moveup(2), ansi_movecol1)
print(summary_io, ansi_moveup(2), ansi_movecol1)
end
println("Frame: $frame_idx/$nframes FPS: $(round(actual_fps, digits=1))", " "^5)
println("exit: ctrl-c. play/pause: space-bar. seek: arrow keys")

display(frame)
println(summary_io, "Frame: $frame_idx/$nframes FPS: $(round(actual_fps, digits=1))", " "^5)
println(summary_io, "exit: ctrl-c. play/pause: space-bar. seek: arrow keys")

# When calling `display(MIME"image/png"(), img)`, VSCode/IJulia/Atom will eventually
# create an `IOBuffer` to get the Base64+PNG encoded data, and send the encoded data to
# the outside display pipeline, e.g., as JSON message.
# For test purpose, we could directly show it to our manually created IO.
isnothing(display_io) ? display(frame) : show(display_io, MIME"image/png"(), frame)
end
# These codes live in ImageShow and thus MIME"image/png" is always showable
@assert showable(MIME"image/png"(), framestack[frame_idx])
render_frame(frame_idx, actual_fps; first_frame=true)

keytask = @async begin
while !should_exit
control_value = read_key()
control_value = read_key(keyboard_io)

if control_value == :CONTROL_BACKWARD
frame_idx = max(frame_idx-1, 1)
Expand Down Expand Up @@ -86,6 +106,10 @@ function play(framestack::AbstractVector{<:AbstractArray}; fps::Real=30, paused=
end
last_frame_idx = frame_idx
end
if !quit_after_play && frame_idx == nframes
# don't immediately quit the play
paused = true
end
paused || (frame_idx += 1)
paused && sleep(0.001)
end
Expand All @@ -98,67 +122,7 @@ function play(framestack::AbstractVector{<:AbstractArray}; fps::Real=30, paused=
end
return nothing
end
play(img::AbstractArray{<:Colorant}, dim; kwargs...) = play(map(i->selectdim(img, dim, i), axes(img, dim)); kwargs...)

# minimal keyboard event support
"""
read_key() -> control_value
read control key from keyboard input.
# Reference table
| value | control_value | effect |
| ------------------- | ----------------- | ------------------- |
| UP, LEFT, f, F | :CONTROL_BACKWARD | show previous frame |
| DOWN, RIGHT, b, B | :CONTROL_FORWARD | show next frame |
| SPACE, p, P | :CONTROL_PAUSE | pause/resume play |
| CTRL-c, q, Q | :CONTROL_EXIT | exit current play |
| others... | :CONTROL_VOID | no effect |
"""
function read_key()
setraw!(io, raw) = ccall(:jl_tty_set_mode, Int32, (Ptr{Cvoid},Int32), io.handle, raw)
control_value = :CONTROL_VOID
try
setraw!(stdin, true)
keyin = read(stdin, Char)
if keyin == '\e'
# some special keys are more than one byte, e.g., left key is `\e[D`
# reference: https://en.wikipedia.org/wiki/ANSI_escape_code
keyin = read(stdin, Char)
if keyin == '['
keyin = read(stdin, Char)
if keyin in ['A', 'D'] # up, left
control_value = :CONTROL_BACKWARD
elseif keyin in ['B', 'C'] # down, right
control_value = :CONTROL_FORWARD
end
end
elseif 'A' <= keyin <= 'Z' || 'a' <= keyin <= 'z'
keyin = lowercase(keyin)
if keyin == 'p'
control_value = :CONTROL_PAUSE
elseif keyin == 'q'
control_value = :CONTROL_EXIT
elseif keyin == 'f'
control_value = :CONTROL_FORWARD
elseif keyin == 'b'
control_value = :CONTROL_BACKWARD
end
elseif keyin == ' '
control_value = :CONTROL_PAUSE
end
catch e
if e isa InterruptException # Ctrl-C
control_value = :CONTROL_EXIT
else
rethrow(e)
end
finally
setraw!(stdin, false)
end
return control_value
end

"""
fixed_fps(f::Function, fps::Real)
Expand Down
36 changes: 36 additions & 0 deletions test/keyboard.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using ImageShow
using ImageShow: read_key

@testset "keyboard" begin
# TODO: Ctrl-C (InterruptException) is not tested
@testset "read_key" begin
inputs = [
(UInt8['\e', '[', 'A'], :CONTROL_BACKWARD), # UP
(UInt8['\e', '[', 'D'], :CONTROL_BACKWARD), # LEFT
(UInt8['\e', '[', 'B'], :CONTROL_FORWARD), # DOWN
(UInt8['\e', '[', 'C'], :CONTROL_FORWARD), # RIGHT

(UInt8[' '], :CONTROL_PAUSE), # SPACE
(UInt8['p'], :CONTROL_PAUSE),
(UInt8['P'], :CONTROL_PAUSE),
(UInt8['q'], :CONTROL_EXIT),
(UInt8['Q'], :CONTROL_EXIT),
(UInt8['f'], :CONTROL_FORWARD),
(UInt8['F'], :CONTROL_FORWARD),
(UInt8['b'], :CONTROL_BACKWARD),
(UInt8['B'], :CONTROL_BACKWARD),

# key events that currenctly has no effect

# although VIM users might want this :)
(UInt8['j'], :CONTROL_VOID),
(UInt8['k'], :CONTROL_VOID),
(UInt8['h'], :CONTROL_VOID),
(UInt8['l'], :CONTROL_VOID),
]
for (chs, ref) in inputs
io = IOBuffer(chs)
@test ref == read_key(io)
end
end
end
61 changes: 61 additions & 0 deletions test/multipage.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using ImageShow, ImageCore
using TestImages, FileIO
using Test

function check_summary(n, msg)
function generate_summary_regex(n)
summary_regex = raw"Frame: \d+/\d+ FPS: \d+\.\d+\s+\nexit: ctrl-c\. play\/pause: space-bar\. seek: arrow keys\n"
Regex(mapreduce(i->summary_regex, (x,y)->x*raw".*"*y, 1:n))
end
function _check_summary(n, msg)
return !isnothing(match(generate_summary_regex(n), msg))
end

return _check_summary(n, msg) && !_check_summary(n+1, msg)
end

@testset "multipage" begin
@testset "play" begin
img = RGB.(testimage("mri-stack"))
framestack = [img[:, :, i] for i in 1:size(img, 3)]

workdir = "tmp"
mktempdir() do workdir
filename = joinpath(workdir, "multipage.png")
fn = open(filename, "w")
summary_output_io = IOBuffer()

save(filename, framestack[1])
frame_size = stat(filename).size

# Case: quit immediately
key_input_io = IOBuffer(UInt8['q'])
ImageShow._play(framestack; fps=1, paused=false, quit_after_play=true, display_io=fn, summary_io=summary_output_io, keyboard_io=key_input_io)
summary_msg = String(take!(summary_output_io))
@test check_summary(2, summary_msg) # If paused=false, it will print summary twice
@test RGB.(load(filename)) == framestack[1]
@test 1 == stat(filename).size/frame_size

# Case: quit after all play
fn = open(filename, "w")
summary_output_io = IOBuffer()
# use a small fps to make sure each frame are actually written
ImageShow._play(framestack; fps=15, paused=false, quit_after_play=true, display_io=fn, summary_io=summary_output_io, keyboard_io=stdin)
summary_msg = String(take!(summary_output_io))
@test check_summary(length(framestack), summary_msg)
# FIXME: show method will append to the given file handler, however, PNG reader will only
# read the first valid png data; all extra data block are discarded.
# This somehow still proves that we're writing more than one image to the display_io
@test RGB.(load(filename)) == framestack[1]
@test 20 < stat(filename).size/frame_size < length(framestack)
end
end

@testset "utils" begin
# Although it's a no-op, it gets blocked at fps=2, which is about 0.5 second
t = @elapsed ImageShow.fixed_fps(2) do
nothing
end
@test 0.4 < t < 0.6
end
end
4 changes: 4 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ using Test
if VERSION >= v"1.3.0"
include("gif.jl")
end

@info "There are some keyboard IO tests. To make sure test passes as expected, please don't press any key until test finishes."
include("keyboard.jl")
include("multipage.jl")
end

0 comments on commit 91c1b5e

Please sign in to comment.