diff --git a/Project.toml b/Project.toml index 69109a5..bee7a06 100644 --- a/Project.toml +++ b/Project.toml @@ -8,11 +8,13 @@ ColorVectorSpace = "c3611d14-8923-5661-9e6a-0046d554d3a4" Colors = "5ae59095-9a9b-59fe-a467-6f913c188581" FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" Graphics = "a2bd30eb-e257-5431-a919-1863eab51364" +IndirectArrays = "9b13fd28-a010-5f03-acff-a1bbcff69959" MappedArrays = "dbb5928d-eab1-5f90-85c2-b9b0edb7c900" MosaicViews = "e94cdb99-869f-56ef-bcf0-1ae2bcbe0389" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" PaddedViews = "5432bcbf-9aad-5242-b902-cca2824c8663" Reexport = "189a3867-3050-52da-a836-e630ba90ab69" +StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" [compat] AbstractFFTs = "0.4, 0.5, 1.0" @@ -20,11 +22,13 @@ ColorVectorSpace = "0.9.7" Colors = "0.12" FixedPointNumbers = "0.8" Graphics = "0.4, 1.0" +IndirectArrays = "0.5, 1" MappedArrays = "0.2, 0.3, 0.4" MosaicViews = "0.3.3" OffsetArrays = "0.8, 0.9, 0.10, 0.11, 1.0.1" PaddedViews = "0.5.8" Reexport = "0.2, 1.0" +StructArrays = "0.5, 0.6" julia = "1" [extras] diff --git a/src/ImageCore.jl b/src/ImageCore.jl index f835c9c..2c7a43c 100644 --- a/src/ImageCore.jl +++ b/src/ImageCore.jl @@ -9,9 +9,22 @@ using Reexport @reexport using PaddedViews using MappedArrays, Graphics using OffsetArrays # for show.jl +using OffsetArrays: no_offset_view using .ColorTypes: colorant_string using Colors: Fractional using MappedArrays: AbstractMultiMappedArray +@static if VERSION >= v"1.3" + # There are two common ways to convert from struct of array (SOA) layout to array of + # struct (AOS) layout without copying the data. Take 2D RGB image as an example: + # - `colorview(RGB, PermutedDimsArray(img, (3, 1, 2)))` + # - `StructArray{RGB{eltype(img)}}(img; dims=3)` + # Using `StructArray` preserves the information that original data is stored as SOA + # layout, while `ReinterpretArray` cannot. For newer Julia versions, we interpret it as + # `StructArray` and thus provides room for operator optimization, e.g., `imfilter` on + # SOA layout can be implemented much easier and efficiently. + @reexport using StructArrays: StructArray +end +@reexport using IndirectArrays: IndirectArray # for indexed image using Base: tail, @pure, Indices import Base: float @@ -91,7 +104,11 @@ export spacedirections, spatialorder, width, - widthheight + widthheight, + # matlab compatibility + im_from_matlab, + im_to_matlab + include("colorchannels.jl") include("stackedviews.jl") @@ -100,6 +117,7 @@ include("traits.jl") include("map.jl") include("show.jl") include("functions.jl") +include("matlab.jl") include("deprecations.jl") """ diff --git a/src/matlab.jl b/src/matlab.jl new file mode 100644 index 0000000..d09d582 --- /dev/null +++ b/src/matlab.jl @@ -0,0 +1,274 @@ +# Convenient utilities for MATLAB image layout: the color channel is stored as the last dimension. +# +# These function do not intent to cover all use cases +# because numerical arrays do not contain colorspace information. + + +""" + im_from_matlab([CT], X::AbstractArray) -> AbstractArray{CT} + im_from_matlab([CT], index::AbstractArray, values::AbstractArray) + +Convert numerical array image `X` to colorant array, using the MATLAB image layout +convention. The image can also be an indexed image by passing the `index`, `values` pair. + +By default, the input image `X` is assumed to be either grayscale image or RGB image. For +other colorspaces, explicit colorspace `CT` must be specified. Note that `CT` is only used +to interpret the values without numerical changes, thus using it incorrectly would produce +unexpected results, e.g., `im_from_matlab(Lab, rgb_values)` would be terribly wrong. + +```julia +im_from_matlab(rand(4, 4)) # 4×4 Gray image +im_from_matlab(rand(4, 4, 3)) # 4×4 RGB image + +im_from_matlab(GrayA, rand(4, 4, 2)) # 4×4 Gray image with alpha channel +im_from_matlab(HSV, rand(4, 4, 3)) # 4×4 HSV image +``` + +Except for special types `UInt8` and `UInt16`, the value range is typically \$[0, 1]\$. Thus +integer values must be converted to float point numbers or fixed point numbers first. For +instance: + +```julia +img = rand(1:255, 16, 16) # 16×16 Int array + +im_from_matlab(img ./ 255) # convert to Float64 +im_from_matlab(UInt8.(img)) # convert to UInt8 +``` + +Indexd image in MATLAB convention consists of the `index`-`values` pair. `values` is a +two-dimensional N×3 numerical array, and `index` is a integer-valued array in range \$[1, +N]\$. + +```julia +# a 4×4 random indexed image using five colors +index = rand(1:5, 4, 4) +values = [0.0 0.0 0.0 # black + 1.0 0.0 0.0 # red + 0.0 1.0 0.0 # green + 0.0 0.0 1.0 # blue + 1.0 1.0 1.0] # white + +# 4×4 matrix with eltype RGB{Float64} +im_from_matlab(index, values) +``` + +!!! tip "eager conversion" + To save memory allocation, the conversion is done in lazy mode. In some cases, this + could introduce performance overhead due to the repeat computation. This can be easily + solved by converting eagerly via, e.g., `collect(im_from_matlab(...))`. + +See also: [`im_to_matlab`](@ref). +""" +function im_from_matlab end + +# Step 1: convenient conventions +# - 1d numerical vector is Gray image +# - 2d numerical array is Gray image +# - 3d numerical array of size (m, n, 3) is RGB image +# For other cases, users must specify `CT` explicitly; otherwise it is not type-stable +im_from_matlab(X::AbstractVector) = vec(im_from_matlab(reshape(X, (length(X), 1)))) +im_from_matlab(X::AbstractMatrix{T}) where {T<:Real} = im_from_matlab(Gray, X) +function im_from_matlab(X::AbstractArray{T,3}) where {T<:Real} + if size(X, 3) != 3 + msg = "Unrecognized MATLAB image layout." + hint = size(X, 3) == 1 ? "Do you mean `im_from_matlab(reshape(X, ($(size(X)[1:2]...))))`?" : "" + msg = isempty(hint) ? msg : "$msg $hint" + throw(ArgumentError(msg)) + end + return im_from_matlab(RGB, X) +end +im_from_matlab(X::AbstractArray) = throw(ArgumentError("Unrecognized MATLAB image layout.")) + +# Step 2: storage type conversion +function im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT,T} + if T <: Union{Normed,AbstractFloat} + return _im_from_matlab(CT, X) + else + msg = "Unrecognized element type $T, manual conversion to float point number or fixed point number is needed." + hint = _matlab_type_hint(X) + msg = isempty(hint) ? msg : "$msg $hint" + throw(ArgumentError(msg)) + end +end +im_from_matlab(::Type{CT}, X::AbstractArray{UInt8}) where {CT} = _im_from_matlab(CT, reinterpret(N0f8, X)) +im_from_matlab(::Type{CT}, X::AbstractArray{UInt16}) where {CT} = _im_from_matlab(CT, reinterpret(N0f16, X)) +function im_from_matlab(::Type{CT}, X::AbstractArray{Int16}) where {CT} + # MALTAB compat + _im2double(x) = (Float64(x) + Float64(32768)) / Float64(65535) + return _im_from_matlab(CT, mappedarray(_im2double, X)) +end + +function _matlab_type_hint(@nospecialize X) + mn, mx = extrema(X) + if mn >= typemin(UInt8) && mx <= typemax(UInt8) + return "For instance: `UInt8.(X)` or `X./$(typemax(UInt8))`" + elseif mn >= typemin(UInt16) && mx <= typemax(UInt16) + return "For instance: `UInt16.(X)` or `X./$(typemax(UInt16))`" + else + return "" + end +end + +# Step 3: colorspace conversion +_im_from_matlab(::Type{CT}, X::AbstractArray{CT}) where {CT<:Colorant} = X +@static if VERSION >= v"1.3" + # use StructArray to inform that this is a struct of array layout + function _im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Colorant,T<:Real} + _CT = isconcretetype(CT) ? CT : base_colorant_type(CT){T} + # FIXME(johnnychen94): not type inferrable here + return StructArray{_CT}(X; dims=ndims(X)) + end +else + function _im_from_matlab(::Type{CT}, X::AbstractArray{T,3}) where {CT<:Colorant,T<:Real} + _CT = isconcretetype(CT) ? CT : base_colorant_type(CT){T} + # FIXME(johnnychen94): not type inferrable here + return colorview(_CT, PermutedDimsArray(X, (3, 1, 2))) + end + function _im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Colorant,T<:Real} + throw(ArgumentError("For $(ndims(X)) dimensional numerical array, manual conversion from MATLAB layout is required.")) + end +end +_im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Gray,T<:Real} = colorview(CT, X) +_im_from_matlab(::Type{CT}, X::AbstractArray{T,3}) where {CT<:Gray,T<:Real} = colorview(CT, X) + +# index image support +im_from_matlab(index::AbstractArray, values::AbstractMatrix{T}) where T<:Real = im_from_matlab(RGB, index, values) +@static if VERSION >= v"1.3" + function im_from_matlab(::Type{CT}, index::AbstractArray, values::AbstractMatrix{T}) where {CT<:Colorant, T<:Real} + return IndirectArray(index, im_from_matlab(CT, values)) + end +else + function im_from_matlab(::Type{CT}, index::AbstractArray, values::AbstractMatrix{T}) where {CT<:Colorant, T<:Real} + return IndirectArray(index, colorview(CT, PermutedDimsArray(values, (2, 1)))) + end +end + + +""" + I = im_to_matlab([T], X::AbstractArray) + (index, values) = im_to_matlab([T], X::IndirectArray) + +Convert colorant array `X` to numerical array, using MATLAB's image layout convention. If +`X` is an indexed image `IndirectArray`, then the output is a tuple of `index`-`values` +pair. + +```julia +img = rand(Gray{N0f8}, 4, 4) +im_to_matlab(img) # 4×4 array with element type N0f8 +im_to_matlab(Float64, img) # 4×4 array with element type Float64 + +img = rand(RGB{N0f8}, 4, 4) +im_to_matlab(img) # 4×4×3 array with element type N0f8 +im_to_matlab(Float64, img) # 4×4×3 array with element type Float64 +``` + +For color image `X`, it will be converted to RGB colorspace first. The alpha channel, if +presented, will be removed. + +```jldoctest; setup = :(using ImageCore, Random; Random.seed!(1234)) +julia> img = Lab.(rand(RGB, 4, 4)); + +julia> im_to_matlab(img) ≈ im_to_matlab(RGB.(img)) +true + +julia> img = rand(AGray{N0f8}, 4, 4); + +julia> im_to_matlab(img) ≈ im_to_matlab(gray.(img)) +true +``` + +For indexed image represented as `IndirectArray` provided by +[IndirectArrays.jl](https://github.com/JuliaArrays/IndirectArrays.jl), a tuple of +`index`-`values` pair will be returned: + +```julia +# 4×4 indexed image with 5 color +jl_index = rand(1:5, 4, 4) +jl_values = [ + RGB(0.0,0.0,0.0), # black + RGB(1.0,0.0,0.0), # red + RGB(0.0,1.0,0.0), # green + RGB(0.0,0.0,1.0), # blue + RGB(1.0,1.0,1.0) # white +] +jl_img = IndirectArray(jl_index, jl_values) + +# m_values is 5×3 matrix with eltype Float64 +m_index, m_values = im_to_matlab(jl_img) +``` + +!!! tip "eager conversion" + To save memory allocation, the conversion is done in lazy mode. In some cases, this + could introduce performance overhead due to the repeat computation. This can be easily + solved by converting eagerly via, e.g., `collect(im_to_matlab(...))`. + +!!! info "value range" + The output value is always in range \$[0, 1]\$. Thus the equality `data ≈ + im_to_matlab(im_from_matlab(data))` only holds when `data` is in also range \$[0, 1]\$. + For example, if `eltype(data) == UInt8`, this equality will not hold. + +See also: [`im_from_matlab`](@ref). +""" +function im_to_matlab end + +im_to_matlab(X::AbstractArray{<:Number}) = no_offset_view(X) +im_to_matlab(img::AbstractArray{CT}) where {CT<:Colorant} = im_to_matlab(eltype(CT), img) + +im_to_matlab(::Type{T}, img::AbstractArray{CT}) where {T,CT<:TransparentColor} = + im_to_matlab(T, of_eltype(base_color_type(CT), img)) +im_to_matlab(::Type{T}, img::AbstractArray{<:Color}) where {T} = + im_to_matlab(T, of_eltype(RGB{T}, img)) +im_to_matlab(::Type{T}, img::AbstractArray{CT}) where {T,CT<:Union{Gray,RGB,Number}} = + _im_to_matlab_try_reinterpret(T, img) + +# eltype conversion doesn't work in general, e.g., `UInt8(N0f8(0.3))` would fail. For special +# types that we know solution, directly reinterpret them via `rawview`. +function _im_to_matlab_try_reinterpret(::Type{T}, img::AbstractArray{CT}) where {T,CT<:Union{Gray,Real}} + throw(ArgumentError("Can not convert to MATLAB format: invalid conversion from `$(CT)` to `$T`.")) +end +function _im_to_matlab_try_reinterpret(::Type{T}, img::AbstractArray{CT}) where {T<:Union{AbstractFloat, Normed},CT<:Union{Gray,Real}} + return no_offset_view(of_eltype(T, channelview(img))) +end +for (T, NT) in ((:UInt8, :N0f8), (:UInt16, :N0f16)) + @eval function _im_to_matlab_try_reinterpret(::Type{$T}, img::AbstractArray{CT}) where {CT<:Union{Gray,Real}} + if eltype(CT) != $NT + nt_str = string($NT) + throw(ArgumentError("Can not convert to MATLAB format: invalid conversion from `$(CT)` to `$nt_str`.")) + end + return no_offset_view(rawview(channelview(img))) + end +end +# for RGB, unroll the color channel in the last dimension +_im_to_matlab_try_reinterpret(::Type{T}, img::AbstractArray{CT}) where {T,CT<:RGB} = + throw(ArgumentError("Can not convert to MATLAB format: invalid conversion from `$(CT)` to `$T`.")) +function _im_to_matlab_try_reinterpret(::Type{T}, img::AbstractArray{<:RGB,N}) where {T<:Union{AbstractFloat, Normed},N} + v = no_offset_view(of_eltype(T, channelview(img))) + perm = (ntuple(i -> i + 1, N)..., 1) + return PermutedDimsArray(v, perm) +end +for (T, NT) in ((:UInt8, :N0f8), (:UInt16, :N0f16)) + @eval function _im_to_matlab_try_reinterpret(::Type{$T}, img::AbstractArray{CT,N}) where {CT<:RGB,N} + if eltype(CT) != $NT + nt_str = string($NT) + throw(ArgumentError("Can not convert to MATLAB format: invalid conversion from `$(CT)` to `$nt_str`.")) + end + v = no_offset_view(rawview(channelview(img))) + perm = (ntuple(i -> i + 1, N)..., 1) + return PermutedDimsArray(v, perm) + end +end + +# indexed image +function im_to_matlab(::Type{T}, img::IndirectArray{CT}) where {T<:Real,CT<:Colorant} + return no_offset_view(img.index), im_to_matlab(T, img.values) +end + + +if VERSION >= v"1.6.0-DEV.1083" + # this method allows `data === im_to_matlab(im_from_matlab(data))` for gray image + im_to_matlab(::Type{T}, img::Base.ReinterpretArray{CT,N,T,<:AbstractArray{T,N},true}) where {CT,N,T} = + no_offset_view(img.parent) +else + im_to_matlab(::Type{T}, img::Base.ReinterpretArray{CT,N,T,<:AbstractArray{T,N}}) where {CT,N,T} = + no_offset_view(img.parent) +end diff --git a/test/matlab.jl b/test/matlab.jl new file mode 100644 index 0000000..39f8394 --- /dev/null +++ b/test/matlab.jl @@ -0,0 +1,362 @@ +@testset "MATLAB" begin + @testset "im_from_matlab" begin + @testset "Gray" begin + # Float64 + data = rand(4, 5) + img = @inferred im_from_matlab(data) + @test eltype(img) == Gray{Float64} + @test size(img) == (4, 5) + @test channelview(img) == data + + # N0f8 + data = rand(N0f8, 4, 5) + img = @inferred im_from_matlab(data) + mn, mx = extrema(img) + @test eltype(img) == Gray{N0f8} + @test size(img) == (4, 5) + @test 0.0 <= mn <= mx <= 1.0 + + # UInt8 + data = rand(UInt8, 4, 5) + img = @inferred im_from_matlab(data) + mn, mx = extrema(img) + @test eltype(img) == Gray{N0f8} + @test size(img) == (4, 5) + @test 0.0 <= mn <= mx <= 1.0 + + # UInt16 + data = rand(UInt16, 4, 5) + img = @inferred im_from_matlab(data) + mn, mx = extrema(img) + @test eltype(img) == Gray{N0f16} + @test size(img) == (4, 5) + @test 0.0 <= mn <= mx <= 1.0 + + # Int16 -- MATLAB's im2double supports Int16 + data = rand(Int16, 4, 5) + img = @inferred im_from_matlab(data) + mn, mx = extrema(img) + @test eltype(img) == Gray{Float64} + @test size(img) == (4, 5) + @test 0.0 <= mn <= mx <= 1.0 + data = Int16[-32768 0; 0 32767] + @test isapprox([0.0 0.5; 0.5 1.0], @inferred im_from_matlab(data); atol=1e-4) + + # Int is ambiguious -- manual conversion is required but we provide some basic hints + data = rand(1:255, 4, 5) + msg = "Unrecognized element type $(Int), manual conversion to float point number or fixed point number is needed. For instance: `UInt8.(X)` or `X./255`" + @test_throws ArgumentError(msg) im_from_matlab(data) + data = rand(256:65535, 4, 5) + msg = "Unrecognized element type $(Int), manual conversion to float point number or fixed point number is needed. For instance: `UInt16.(X)` or `X./65535`" + @test_throws ArgumentError(msg) im_from_matlab(data) + + # vector + data = rand(UInt8, 4) + img = @inferred im_from_matlab(data) + @test eltype(img) == Gray{N0f8} + @test size(img) == (4,) + end + + @testset "RGB" begin + # Float64 + data = rand(4, 5, 3) + img = im_from_matlab(data) + @test_broken @inferred im_from_matlab(data) + @test_nowarn @inferred collect(im_from_matlab(data)) # type inference issue only occurs in lazy mode + @test eltype(img) == RGB{Float64} + @test size(img) == (4, 5) + @test permutedims(channelview(img), (2, 3, 1)) == data + + # N0f8 + data = rand(N0f8, 4, 5, 3) + img = im_from_matlab(data) + @test_broken @inferred im_from_matlab(data) + @test_nowarn @inferred collect(im_from_matlab(data)) # type inference issue only occurs in lazy mode + mn, mx = extrema(channelview(img)) + @test eltype(img) == RGB{N0f8} + @test size(img) == (4, 5) + @test 0.0 <= mn <= mx <= 1.0 + + # UInt8 + data = rand(UInt8, 4, 5, 3) + img = im_from_matlab(data) + @test_broken @inferred im_from_matlab(data) + @test_nowarn @inferred collect(im_from_matlab(data)) # type inference issue only occurs in lazy mode + mn, mx = extrema(channelview(img)) + @test eltype(img) == RGB{N0f8} + @test size(img) == (4, 5) + @test 0.0 <= mn <= mx <= 1.0 + + # UInt16 + data = rand(UInt16, 4, 5, 3) + img = im_from_matlab(data) + @test_broken @inferred im_from_matlab(data) + @test_nowarn @inferred collect(im_from_matlab(data)) # type inference issue only occurs in lazy mode + mn, mx = extrema(channelview(img)) + @test eltype(img) == RGB{N0f16} + @test size(img) == (4, 5) + @test 0.0 <= mn <= mx <= 1.0 + + # Int16 -- MATLAB's im2double supports Int16 + data = rand(Int16, 4, 5, 3) + img = im_from_matlab(data) + @test_broken @inferred im_from_matlab(data) + @test_nowarn @inferred collect(im_from_matlab(data)) # type inference issue only occurs in lazy mode + mn, mx = extrema(channelview(img)) + @test eltype(img) == RGB{Float64} + @test size(img) == (4, 5) + @test 0.0 <= mn <= mx <= 1.0 + + # Int is ambiguious -- manual conversion is required but we provide some basic hints + data = rand(1:255, 4, 5, 3) + msg = "Unrecognized element type $(Int), manual conversion to float point number or fixed point number is needed. For instance: `UInt8.(X)` or `X./255`" + @test_throws ArgumentError(msg) im_from_matlab(data) + data = rand(256:65535, 4, 5, 3) + msg = "Unrecognized element type $(Int), manual conversion to float point number or fixed point number is needed. For instance: `UInt16.(X)` or `X./65535`" + @test_throws ArgumentError(msg) im_from_matlab(data) + end + + @testset "Color3" begin + img = Lab.(rand(RGB{Float64}, 4, 5)) + data = permutedims(channelview(img), (2, 3, 1)) + img1 = im_from_matlab(Lab, data) + @test eltype(img1) == Lab{Float64} + @test size(img1) == (4, 5) + @test RGB.(img) ≈ RGB.(img1) + end + + data = rand(4, 4, 2) + msg = "Unrecognized MATLAB image layout." + @test_throws ArgumentError(msg) im_from_matlab(data) + + data = rand(4, 4, 3, 1) + msg = "Unrecognized MATLAB image layout." + @test_throws ArgumentError(msg) im_from_matlab(data) + + @testset "indexed image" begin + index = [1 2 3 4 5 + 2 3 4 5 1] + values = [0.0 0.0 0.0 # black + 1.0 0.0 0.0 # red + 0.0 1.0 0.0 # green + 0.0 0.0 1.0 # blue + 1.0 1.0 1.0] # white + img = im_from_matlab(index, values) + @test size(img) == (2, 5) + @test eltype(img) == RGB{Float64} + @test img[2, 3] == RGB(0.0, 0.0, 1.0) + + lab_values = permutedims(channelview(Lab.(img.values)), (2, 1)) + lab_img = im_from_matlab(Lab, index, lab_values) + @test sum(abs2, channelview(RGB.(lab_img) - img)) < 1e-10 + + values = UInt8.(values .* 255) + img = im_from_matlab(index, values) + @test size(img) == (2, 5) + @test eltype(img) == RGB{N0f8} + @test img[2, 3] == RGB(0.0, 0.0, 1.0) + end + end + + @testset "im_to_matlab" begin + @testset "Gray" begin + img = rand(Gray{N0f8}, 4, 5) + data = @inferred im_to_matlab(img) + @test eltype(data) == N0f8 + @test size(data) == (4, 5) + @test img == data + data = @inferred im_to_matlab(Float64, img) + @test eltype(data) == Float64 + @test img == data + + img = rand(Gray{Float64}, 4, 5) + data = @inferred im_to_matlab(img) + @test eltype(data) == Float64 + @test size(data) == (4, 5) + @test img == data + + img = rand(UInt8, 4, 5) + @test img === @inferred im_to_matlab(img) + + img = rand(Gray{Float64}, 4) + data = @inferred im_to_matlab(img) + @test eltype(data) == Float64 + @test size(data) == (4,) + end + + @testset "RGB" begin + img = rand(RGB{N0f8}, 4, 5) + data = if VERSION >= v"1.6" + @inferred im_to_matlab(img) + else + im_to_matlab(img) + end + @test eltype(data) == N0f8 + @test size(data) == (4, 5, 3) + @test permutedims(channelview(img), (2, 3, 1)) == data + data = if VERSION >= v"1.6" + @inferred im_to_matlab(Float64, img) + else + im_to_matlab(Float64, img) + end + @test eltype(data) == Float64 + @test size(data) == (4, 5, 3) + @test permutedims(channelview(img), (2, 3, 1)) == data + + img = rand(RGB{Float64}, 4, 5) + data = if VERSION >= v"1.6" + @inferred im_to_matlab(img) + else + im_to_matlab(img) + end + @test eltype(data) == Float64 + @test size(data) == (4, 5, 3) + @test permutedims(channelview(img), (2, 3, 1)) == data + + img = rand(UInt8, 4, 5, 3) + @test img === @inferred im_to_matlab(img) + + img = rand(RGB{Float64}, 4) + data = if VERSION >= v"1.6" + @inferred im_to_matlab(img) + else + im_to_matlab(img) + end + @test eltype(data) == Float64 + @test size(data) == (4, 3) + end + + @testset "Color3" begin + img = Lab.(rand(RGB, 4, 5)) + if VERSION >= v"1.6" + @test @inferred(im_to_matlab(img)) ≈ @inferred(im_to_matlab(RGB.(img))) + else + @test im_to_matlab(img) ≈ im_to_matlab(RGB.(img)) + end + end + @testset "transparent" begin + img = rand(AGray, 4, 5) + if VERSION >= v"1.6" + @test @inferred(im_to_matlab(img)) == @inferred(im_to_matlab(Gray.(img))) + else + @test im_to_matlab(img) == im_to_matlab(Gray.(img)) + end + img = rand(RGBA, 4, 5) + if VERSION >= v"1.6" + @test @inferred(im_to_matlab(img)) == @inferred(im_to_matlab(RGB.(img))) + else + @test im_to_matlab(img) == im_to_matlab(RGB.(img)) + end + end + + @testset "indexed image" begin + index = [1 2 3 4 5 + 2 3 4 5 1] + values = [ + RGB(0.0,0.0,0.0), # black + RGB(1.0,0.0,0.0), # red + RGB(0.0,1.0,0.0), # green + RGB(0.0,0.0,1.0), # blue + RGB(1.0,1.0,1.0) # white + ] + img = IndirectArray(index, values) + m_index, m_values = im_to_matlab(img) + @test size(m_index) == (2, 5) + @test eltype(m_index) == eltype(index) + @test size(m_values) == (5, 3) + @test eltype(m_values) == Float64 + @test index == m_index + @test m_values == permutedims(channelview(values), (2, 1)) + + m_index, m_values = im_to_matlab(N0f8, img) + @test eltype(m_values) == N0f8 + @test m_values == permutedims(channelview(values), (2, 1)) + end + + @testset "UInt8/UInt16" begin + # directly doing eltype conversion doesn't work for UInt8/UInt16 + # thus we can reinterpret, aka, `rawview`. + for (T, NT) in ((UInt8, N0f8), (UInt16, N0f16)) + img = rand(Gray{NT}, 4, 5) + img_m_normed = im_to_matlab(NT, img) + img_m = im_to_matlab(T, img) + @test eltype(img_m_normed) == NT + @test eltype(img_m) == T + @test img_m_normed != img_m + @test img_m_normed == channelview(img) + @test img_m == rawview(channelview(img)) + + img = rand(RGB{NT}, 4, 5) + img_m_normed = im_to_matlab(NT, img) + img_m = im_to_matlab(T, img) + @test eltype(img_m_normed) == NT + @test eltype(img_m) == T + @test img_m_normed != img_m + @test img_m_normed == permutedims(channelview(img), (2, 3, 1)) + @test img_m == permutedims(rawview(channelview(img)), (2, 3, 1)) + end + + # We only patch for special types that MATLAB expects, for anything that is + # non-standard MATLAB layout, manual conversions or other tools are needed. Here + # we test that we have informative error messages. + for CT in (Gray, RGB) + img = rand(CT{N0f8}, 4, 5) + msg = "Can not convert to MATLAB format: invalid conversion from `$CT{$N0f8}` to `$N0f16`." + @test_throws ArgumentError(msg) im_to_matlab(UInt16, img) + msg = "Can not convert to MATLAB format: invalid conversion from `$CT{$N0f8}` to `$Int`." + @test_throws ArgumentError(msg) im_to_matlab(Int, img) + end + end + end + + # test `im_from_matlab` and `im_to_matlab` are inverses of each other. + data = rand(4, 5) + @test data === im_to_matlab(im_from_matlab(data)) + # For RGB, ideally we would want to ensure this === equality, but it's not possible at the moment. + data = rand(4, 5, 3) + @test data == im_to_matlab(im_from_matlab(data)) + # the output range are always in [0, 1]; in this case they're not inverse of each other. + data = rand(UInt8, 4, 5) + img = im_from_matlab(data) + @test im_to_matlab(img) == data ./ 255 + + @testset "offset array" begin + # JuliaImages accepts arbitrary offsets thus there's no need to force 1-based indexing, + # MATLAB, on the other hand, generally requires 1-based indexing to properly work. + + # Gray + x = rand(4, 5) + xo = OffsetArray(x, (-2, -3)) + img = im_from_matlab(xo) + @test axes(img) == (-1:2, -2:2) + @test eltype(img) == Gray{Float64} + + m_img = im_to_matlab(img) + @test axes(m_img) == (1:4, 1:5) + @test m_img == x + + # RGB + x = rand(4, 5, 3) + xo = OffsetArray(x, (-2, -3, 0)) + img = im_from_matlab(xo) + @test axes(img) == (-1:2, -2:2) + @test eltype(img) == RGB{Float64} + + m_img = im_to_matlab(img) + @test axes(m_img) == (1:4, 1:5, 1:3) + @test m_img == x + + # indexed image + index = rand(1:5, 4, 5) + index_offset = OffsetArray(index, (-1, -1)) + values = rand(5, 3) + img = im_from_matlab(index_offset, values) + @test axes(img) == (0:3, 0:4) + @test eltype(img) == RGB{Float64} + + m_index, m_values = im_to_matlab(img) + @test axes(m_index) == (1:4, 1:5) + @test m_index == index + @test m_values == values + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 4d2d76d..6627608 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,21 +1,24 @@ module ImageCoreTests using ImageCore +using OffsetArrays: OffsetArray using Test, ReferenceTests using Aqua, Documenter # for meta quality checks -@testset "Project meta quality checks" begin - # Not checking compat section for test-only dependencies - Aqua.test_ambiguities(ImageCore) - Aqua.test_all(ImageCore; - ambiguities=false, - project_extras=true, - deps_compat=true, - stale_deps=true, - project_toml_formatting=true, - unbound_args=false, # FIXME: it fails when this is true - ) - DocMeta.setdocmeta!(ImageCore, :DocTestSetup, :(using ImageCore); recursive=true) +@static if VERSION >= v"1.3" + @testset "Project meta quality checks" begin + # Not checking compat section for test-only dependencies + Aqua.test_ambiguities(ImageCore) + Aqua.test_all(ImageCore; + ambiguities=false, + project_extras=true, + deps_compat=true, + stale_deps=true, + project_toml_formatting=true, + unbound_args=false, # FIXME: it fails when this is true + ) + DocMeta.setdocmeta!(ImageCore, :DocTestSetup, :(using ImageCore); recursive=true) + end end # ReferenceTests uses ImageInTerminal as a default image rendering backend, we need to @@ -32,6 +35,7 @@ include("convert_reinterpret.jl") include("traits.jl") include("map.jl") include("functions.jl") +include("matlab.jl") include("show.jl") # To ensure our deprecations work and don't break code