From 39f60557b242143724b793a41628c283e7367a4c Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Mon, 11 Apr 2022 15:23:03 +0800 Subject: [PATCH 01/12] add im_from_matlab --- Project.toml | 2 + src/ImageCore.jl | 7 ++- src/matlab.jl | 97 ++++++++++++++++++++++++++++++++++++ test/matlab.jl | 127 +++++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 5 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 src/matlab.jl create mode 100644 test/matlab.jl diff --git a/Project.toml b/Project.toml index 69109a5..c7944eb 100644 --- a/Project.toml +++ b/Project.toml @@ -13,6 +13,7 @@ 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" @@ -25,6 +26,7 @@ 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..afe0ed9 100644 --- a/src/ImageCore.jl +++ b/src/ImageCore.jl @@ -12,6 +12,7 @@ using OffsetArrays # for show.jl using .ColorTypes: colorant_string using Colors: Fractional using MappedArrays: AbstractMultiMappedArray +@reexport using StructArrays: StructArray # for struct of array layout using Base: tail, @pure, Indices import Base: float @@ -91,7 +92,10 @@ export spacedirections, spatialorder, width, - widthheight + widthheight, + # matlab compatibility + im_from_matlab + include("colorchannels.jl") include("stackedviews.jl") @@ -100,6 +104,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..9533166 --- /dev/null +++ b/src/matlab.jl @@ -0,0 +1,97 @@ +# 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} + +Convert numerical array `X` to colorant array, using the MATLAB image layout convention. + +The input image `X` is assumed to be either grayscale image or RGB image. For other +colorspaces, the input `X` must be converted to RGB colorspace first. + +```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-alpha image +im_from_matlab(HSV, rand(4, 4, 3)) # 4×4 HSV image +``` + +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 first +im_from_matlab(UInt8.(img)) # convert to UInt8 first +``` + +!!! tip "lazy 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 +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=3) +end +_im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Gray, T<:Real} = of_eltype(CT, X) diff --git a/test/matlab.jl b/test/matlab.jl new file mode 100644 index 0000000..032ffbe --- /dev/null +++ b/test/matlab.jl @@ -0,0 +1,127 @@ +@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 + + 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) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 4d2d76d..29d4edd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -32,6 +32,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 From 95c5758b821d6114d92bc0c0d751c3bcde47dde6 Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Mon, 11 Apr 2022 18:08:30 +0800 Subject: [PATCH 02/12] add im_to_matlab --- src/ImageCore.jl | 3 +- src/matlab.jl | 69 +++++++++++++++++++++++++++++++++++++++++- test/matlab.jl | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/ImageCore.jl b/src/ImageCore.jl index afe0ed9..72eba6f 100644 --- a/src/ImageCore.jl +++ b/src/ImageCore.jl @@ -94,7 +94,8 @@ export width, widthheight, # matlab compatibility - im_from_matlab + im_from_matlab, + im_to_matlab include("colorchannels.jl") diff --git a/src/matlab.jl b/src/matlab.jl index 9533166..9082f55 100644 --- a/src/matlab.jl +++ b/src/matlab.jl @@ -94,4 +94,71 @@ function _im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Colorant, T # FIXME(johnnychen94): not type inferrable here return StructArray{_CT}(X; dims=3) end -_im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Gray, T<:Real} = of_eltype(CT, X) +_im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Gray, T<:Real} = colorview(CT, X) + + +""" + im_to_matlab([T], X::AbstractArray) -> AbstractArray{T} + +Convert colorant array `X` to numerical array, using MATLAB's image layout convention. + +```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 +``` + +!!! tip "lazy 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}) = 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{<:Gray}) where T = + of_eltype(T, channelview(img)) + +# for RGB, only 1d and 2d cases are supported as other cases are not well-defined in MATLAB. +im_to_matlab(::Type{T}, img::AbstractVector{<:RGB}) where T = + im_to_matlab(T, reshape(img, (length(img), 1))) +im_to_matlab(::Type{T}, img::AbstractMatrix{<:RGB}) where T = + PermutedDimsArray(of_eltype(T, channelview(img)), (2, 3, 1)) +im_to_matlab(::Type{T}, img::AbstractArray{<:RGB}) where T = + throw(ArgumentError("For $(ndims(img)) dimensional color image, manual conversion to MATLAB layout is required.")) + +# 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} = + img.parent diff --git a/test/matlab.jl b/test/matlab.jl index 032ffbe..d7fed52 100644 --- a/test/matlab.jl +++ b/test/matlab.jl @@ -124,4 +124,83 @@ msg = "Unrecognized MATLAB image layout." @test_throws ArgumentError(msg) im_from_matlab(data) 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 = @inferred im_to_matlab(img) + @test eltype(data) == N0f8 + @test size(data) == (4, 5, 3) + @test permutedims(channelview(img), (2, 3, 1)) == data + data = @inferred im_to_matlab(Float64, img) + @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 = @inferred im_to_matlab(img) + @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 = @inferred im_to_matlab(img) + @test eltype(data) == Float64 + @test size(data) == (4, 1, 3) # oh yes, we add one extra dimension for RGB but not for Gray + + img = rand(RGB{Float64}, 2, 3, 4) + msg = "For 3 dimensional color image, manual conversion to MATLAB layout is required." + @test_throws ArgumentError(msg) im_to_matlab(img) + end + + @testset "Color3" begin + img = Lab.(rand(RGB, 4, 5)) + @test @inferred(im_to_matlab(img)) ≈ @inferred(im_to_matlab(RGB.(img))) + end + @testset "transparent" begin + img = rand(AGray, 4, 5) + @test @inferred(im_to_matlab(img)) == @inferred(im_to_matlab(Gray.(img))) + img = rand(RGBA, 4, 5) + @test @inferred(im_to_matlab(img)) == @inferred(im_to_matlab(RGB.(img))) + 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 end From a8c98e47ccf9ed997580d3b8b2a8fd57145982cf Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Mon, 11 Apr 2022 18:25:48 +0800 Subject: [PATCH 03/12] compat fix for Julia < v1.6 --- src/matlab.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/matlab.jl b/src/matlab.jl index 9082f55..4da605e 100644 --- a/src/matlab.jl +++ b/src/matlab.jl @@ -159,6 +159,11 @@ im_to_matlab(::Type{T}, img::AbstractMatrix{<:RGB}) where T = im_to_matlab(::Type{T}, img::AbstractArray{<:RGB}) where T = throw(ArgumentError("For $(ndims(img)) dimensional color image, manual conversion to MATLAB layout is required.")) -# 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} = - img.parent +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} = + img.parent +else + im_to_matlab(::Type{T}, img::Base.ReinterpretArray{CT,N,T,<:AbstractArray{T,N}}) where {CT,N,T} = + img.parent +end From d46a180c19b3ebca3a0b6a7bfd40d6ed57da30a1 Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Mon, 11 Apr 2022 20:10:29 +0800 Subject: [PATCH 04/12] compat fix for Julia < v1.3 --- src/ImageCore.jl | 4 +++- src/matlab.jl | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/ImageCore.jl b/src/ImageCore.jl index 72eba6f..305bd9b 100644 --- a/src/ImageCore.jl +++ b/src/ImageCore.jl @@ -12,7 +12,9 @@ using OffsetArrays # for show.jl using .ColorTypes: colorant_string using Colors: Fractional using MappedArrays: AbstractMultiMappedArray -@reexport using StructArrays: StructArray # for struct of array layout +@static if VERSION >= v"1.3" + @reexport using StructArrays: StructArray # for struct of array layout +end using Base: tail, @pure, Indices import Base: float diff --git a/src/matlab.jl b/src/matlab.jl index 4da605e..935794d 100644 --- a/src/matlab.jl +++ b/src/matlab.jl @@ -89,10 +89,19 @@ end # Step 3: colorspace conversion _im_from_matlab(::Type{CT}, X::AbstractArray{CT}) where CT<:Colorant = X -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=3) +@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,3}) 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=3) + 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 end _im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Gray, T<:Real} = colorview(CT, X) From 98193108fe2c3c016420fe802ff76a34f8f0be30 Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Mon, 11 Apr 2022 23:27:31 +0800 Subject: [PATCH 05/12] let im_from_matlab error on suspicious dimention With some whitespace fixes --- src/matlab.jl | 22 +++++++++++++--------- test/matlab.jl | 11 +++++++++++ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/matlab.jl b/src/matlab.jl index 935794d..88e1990 100644 --- a/src/matlab.jl +++ b/src/matlab.jl @@ -59,7 +59,7 @@ im_from_matlab(X::AbstractArray) = throw(ArgumentError("Unrecognized MATLAB imag # Step 2: storage type conversion function im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT,T} - if T<:Union{Normed, AbstractFloat} + 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." @@ -68,11 +68,11 @@ function im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT,T} 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 +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) + _im2double(x) = (Float64(x) + Float64(32768)) / Float64(65535) return _im_from_matlab(CT, mappedarray(_im2double, X)) end @@ -88,22 +88,26 @@ function _matlab_type_hint(@nospecialize X) end # Step 3: colorspace conversion -_im_from_matlab(::Type{CT}, X::AbstractArray{CT}) where CT<:Colorant = X +_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,3}) where {CT<:Colorant, T<:Real} + 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 StructArray{_CT}(X; dims=3) end else - function _im_from_matlab(::Type{CT}, X::AbstractArray{T,3}) where {CT<:Colorant, T<:Real} + 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 end -_im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Gray, T<:Real} = colorview(CT, X) +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 +_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) """ diff --git a/test/matlab.jl b/test/matlab.jl index d7fed52..38cf5f5 100644 --- a/test/matlab.jl +++ b/test/matlab.jl @@ -116,6 +116,15 @@ @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) @@ -123,6 +132,8 @@ data = rand(4, 4, 3, 1) msg = "Unrecognized MATLAB image layout." @test_throws ArgumentError(msg) im_from_matlab(data) + msg = "For 4 dimensional numerical array, manual conversion from MATLAB layout is required." + @test_throws ArgumentError(msg) im_from_matlab(RGB, data) end @testset "im_to_matlab" begin From 19901d976c4fa065672ccf31c9f770ba0046490f Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Mon, 11 Apr 2022 23:27:45 +0800 Subject: [PATCH 06/12] whitespace fix --- src/matlab.jl | 14 +++++++------- test/matlab.jl | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/matlab.jl b/src/matlab.jl index 88e1990..9b930b9 100644 --- a/src/matlab.jl +++ b/src/matlab.jl @@ -155,26 +155,26 @@ See also: [`im_from_matlab`](@ref). function im_to_matlab end im_to_matlab(X::AbstractArray{<:Number}) = X -im_to_matlab(img::AbstractArray{CT}) where CT<:Colorant = im_to_matlab(eltype(CT), img) +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(::Type{T}, img::AbstractArray{<:Color}) where {T} = im_to_matlab(T, of_eltype(RGB{T}, img)) -im_to_matlab(::Type{T}, img::AbstractArray{<:Gray}) where T = +im_to_matlab(::Type{T}, img::AbstractArray{<:Gray}) where {T} = of_eltype(T, channelview(img)) # for RGB, only 1d and 2d cases are supported as other cases are not well-defined in MATLAB. -im_to_matlab(::Type{T}, img::AbstractVector{<:RGB}) where T = +im_to_matlab(::Type{T}, img::AbstractVector{<:RGB}) where {T} = im_to_matlab(T, reshape(img, (length(img), 1))) -im_to_matlab(::Type{T}, img::AbstractMatrix{<:RGB}) where T = +im_to_matlab(::Type{T}, img::AbstractMatrix{<:RGB}) where {T} = PermutedDimsArray(of_eltype(T, channelview(img)), (2, 3, 1)) -im_to_matlab(::Type{T}, img::AbstractArray{<:RGB}) where T = +im_to_matlab(::Type{T}, img::AbstractArray{<:RGB}) where {T} = throw(ArgumentError("For $(ndims(img)) dimensional color image, manual conversion to MATLAB layout is required.")) 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} = + im_to_matlab(::Type{T}, img::Base.ReinterpretArray{CT,N,T,<:AbstractArray{T,N},true}) where {CT,N,T} = img.parent else im_to_matlab(::Type{T}, img::Base.ReinterpretArray{CT,N,T,<:AbstractArray{T,N}}) where {CT,N,T} = diff --git a/test/matlab.jl b/test/matlab.jl index 38cf5f5..c3ffac2 100644 --- a/test/matlab.jl +++ b/test/matlab.jl @@ -159,7 +159,7 @@ img = rand(Gray{Float64}, 4) data = @inferred im_to_matlab(img) @test eltype(data) == Float64 - @test size(data) == (4, ) + @test size(data) == (4,) end @testset "RGB" begin From 6ad0c86eda89778e7211ec719686e2bd78a663d5 Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Mon, 11 Apr 2022 23:37:23 +0800 Subject: [PATCH 07/12] skip meta check for old Julia versions --- test/runtests.jl | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 29d4edd..d122444 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,18 +4,20 @@ using ImageCore 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 From a8d550d018efc97b710fafa9b5e1e6019bf6da01 Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Mon, 11 Apr 2022 23:58:51 +0800 Subject: [PATCH 08/12] skip inferred test for julia < 1.6 --- test/matlab.jl | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/test/matlab.jl b/test/matlab.jl index c3ffac2..d868e9f 100644 --- a/test/matlab.jl +++ b/test/matlab.jl @@ -164,17 +164,29 @@ @testset "RGB" begin img = rand(RGB{N0f8}, 4, 5) - data = @inferred im_to_matlab(img) + 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 = @inferred im_to_matlab(Float64, img) + 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 = @inferred im_to_matlab(img) + 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 @@ -183,7 +195,11 @@ @test img === @inferred im_to_matlab(img) img = rand(RGB{Float64}, 4) - data = @inferred im_to_matlab(img) + 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, 1, 3) # oh yes, we add one extra dimension for RGB but not for Gray @@ -194,13 +210,25 @@ @testset "Color3" begin img = Lab.(rand(RGB, 4, 5)) - @test @inferred(im_to_matlab(img)) ≈ @inferred(im_to_matlab(RGB.(img))) + 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) - @test @inferred(im_to_matlab(img)) == @inferred(im_to_matlab(Gray.(img))) + 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) - @test @inferred(im_to_matlab(img)) == @inferred(im_to_matlab(RGB.(img))) + 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 end From c630cdaf5d531d74cc4961de9fd2912bc69aae38 Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Fri, 15 Apr 2022 14:49:16 +0800 Subject: [PATCH 09/12] add indexed image support --- Project.toml | 2 + src/ImageCore.jl | 11 ++++- src/matlab.jl | 114 ++++++++++++++++++++++++++++++++++++----------- test/matlab.jl | 50 ++++++++++++++++++--- 4 files changed, 143 insertions(+), 34 deletions(-) diff --git a/Project.toml b/Project.toml index c7944eb..bee7a06 100644 --- a/Project.toml +++ b/Project.toml @@ -8,6 +8,7 @@ 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" @@ -21,6 +22,7 @@ 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" diff --git a/src/ImageCore.jl b/src/ImageCore.jl index 305bd9b..b65bbd4 100644 --- a/src/ImageCore.jl +++ b/src/ImageCore.jl @@ -13,8 +13,17 @@ using .ColorTypes: colorant_string using Colors: Fractional using MappedArrays: AbstractMultiMappedArray @static if VERSION >= v"1.3" - @reexport using StructArrays: StructArray # for struct of array layout + # 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 diff --git a/src/matlab.jl b/src/matlab.jl index 9b930b9..48a031c 100644 --- a/src/matlab.jl +++ b/src/matlab.jl @@ -6,31 +6,53 @@ """ im_from_matlab([CT], X::AbstractArray) -> AbstractArray{CT} + im_from_matlab([CT], index::AbstractArray, values::AbstractArray) -Convert numerical array `X` to colorant array, using the MATLAB image layout convention. +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. -The input image `X` is assumed to be either grayscale image or RGB image. For other -colorspaces, the input `X` must be converted to RGB colorspace first. +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-alpha 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 ``` -Integer values must be converted to float point numbers or fixed point numbers first. For +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 first -im_from_matlab(UInt8.(img)) # convert to UInt8 first +im_from_matlab(img ./ 255) # convert to Float64 +im_from_matlab(UInt8.(img)) # convert to UInt8 ``` -!!! tip "lazy conversion" +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(...))`. @@ -91,10 +113,10 @@ end _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,3}) where {CT<:Colorant,T<:Real} + 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=3) + return StructArray{_CT}(X; dims=ndims(X)) end else function _im_from_matlab(::Type{CT}, X::AbstractArray{T,3}) where {CT<:Colorant,T<:Real} @@ -102,18 +124,33 @@ else # FIXME(johnnychen94): not type inferrable here return colorview(_CT, PermutedDimsArray(X, (3, 1, 2))) end -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.")) + 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{T}, 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 + """ - im_to_matlab([T], X::AbstractArray) -> AbstractArray{T} + 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. +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) @@ -140,15 +177,35 @@ julia> im_to_matlab(img) ≈ im_to_matlab(gray.(img)) true ``` -!!! tip "lazy conversion" +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. + 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). """ @@ -164,13 +221,18 @@ im_to_matlab(::Type{T}, img::AbstractArray{<:Color}) where {T} = im_to_matlab(::Type{T}, img::AbstractArray{<:Gray}) where {T} = of_eltype(T, channelview(img)) -# for RGB, only 1d and 2d cases are supported as other cases are not well-defined in MATLAB. -im_to_matlab(::Type{T}, img::AbstractVector{<:RGB}) where {T} = - im_to_matlab(T, reshape(img, (length(img), 1))) -im_to_matlab(::Type{T}, img::AbstractMatrix{<:RGB}) where {T} = - PermutedDimsArray(of_eltype(T, channelview(img)), (2, 3, 1)) -im_to_matlab(::Type{T}, img::AbstractArray{<:RGB}) where {T} = - throw(ArgumentError("For $(ndims(img)) dimensional color image, manual conversion to MATLAB layout is required.")) +# for RGB, unroll the color channel in the last dimension +function im_to_matlab(::Type{T}, img::AbstractArray{<:RGB, N}) where {T, N} + v = of_eltype(T, channelview(img)) + perm = (ntuple(i->i+1, N)..., 1) + return PermutedDimsArray(v, perm) +end + +# indexed image +function im_to_matlab(::Type{T}, img::IndirectArray{CT}) where {T<:Real, CT<:Colorant} + return 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 diff --git a/test/matlab.jl b/test/matlab.jl index d868e9f..e5f2098 100644 --- a/test/matlab.jl +++ b/test/matlab.jl @@ -132,8 +132,24 @@ data = rand(4, 4, 3, 1) msg = "Unrecognized MATLAB image layout." @test_throws ArgumentError(msg) im_from_matlab(data) - msg = "For 4 dimensional numerical array, manual conversion from MATLAB layout is required." - @test_throws ArgumentError(msg) im_from_matlab(RGB, 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 + end end @testset "im_to_matlab" begin @@ -201,11 +217,7 @@ im_to_matlab(img) end @test eltype(data) == Float64 - @test size(data) == (4, 1, 3) # oh yes, we add one extra dimension for RGB but not for Gray - - img = rand(RGB{Float64}, 2, 3, 4) - msg = "For 3 dimensional color image, manual conversion to MATLAB layout is required." - @test_throws ArgumentError(msg) im_to_matlab(img) + @test size(data) == (4, 3) end @testset "Color3" begin @@ -230,6 +242,30 @@ @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 end # test `im_from_matlab` and `im_to_matlab` are inverses of each other. From 2fc256aa04ee205b79afbdda67150b2fd2432a42 Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Sun, 17 Apr 2022 15:40:02 +0800 Subject: [PATCH 10/12] force 1-based indexing for im_to_matlab output --- src/ImageCore.jl | 1 + src/matlab.jl | 12 ++++++------ test/matlab.jl | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/src/ImageCore.jl b/src/ImageCore.jl index b65bbd4..2c7a43c 100644 --- a/src/ImageCore.jl +++ b/src/ImageCore.jl @@ -9,6 +9,7 @@ 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 diff --git a/src/matlab.jl b/src/matlab.jl index 48a031c..4bba99a 100644 --- a/src/matlab.jl +++ b/src/matlab.jl @@ -211,7 +211,7 @@ See also: [`im_from_matlab`](@ref). """ function im_to_matlab end -im_to_matlab(X::AbstractArray{<:Number}) = X +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} = @@ -219,26 +219,26 @@ im_to_matlab(::Type{T}, img::AbstractArray{CT}) where {T,CT<:TransparentColor} = 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{<:Gray}) where {T} = - of_eltype(T, channelview(img)) + no_offset_view(of_eltype(T, channelview(img))) # for RGB, unroll the color channel in the last dimension function im_to_matlab(::Type{T}, img::AbstractArray{<:RGB, N}) where {T, N} - v = of_eltype(T, channelview(img)) + v = no_offset_view(of_eltype(T, channelview(img))) perm = (ntuple(i->i+1, N)..., 1) return PermutedDimsArray(v, perm) end # indexed image function im_to_matlab(::Type{T}, img::IndirectArray{CT}) where {T<:Real, CT<:Colorant} - return img.index, im_to_matlab(T, img.values) + 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} = - img.parent + no_offset_view(img.parent) else im_to_matlab(::Type{T}, img::Base.ReinterpretArray{CT,N,T,<:AbstractArray{T,N}}) where {CT,N,T} = - img.parent + no_offset_view(img.parent) end diff --git a/test/matlab.jl b/test/matlab.jl index e5f2098..527a097 100644 --- a/test/matlab.jl +++ b/test/matlab.jl @@ -278,4 +278,44 @@ 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 From 02c92140d016a0622ea71a8a29e8ea7eff438a38 Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Sun, 17 Apr 2022 17:20:49 +0800 Subject: [PATCH 11/12] support UInt8/UInt16 for im_to_matlab --- src/matlab.jl | 40 +++++++++++++++++++++++++++++++++++----- test/matlab.jl | 35 +++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/matlab.jl b/src/matlab.jl index 4bba99a..15361c7 100644 --- a/src/matlab.jl +++ b/src/matlab.jl @@ -218,18 +218,48 @@ 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{<:Gray}) where {T} = - no_offset_view(of_eltype(T, channelview(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 -function im_to_matlab(::Type{T}, img::AbstractArray{<:RGB, N}) where {T, N} +_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) + 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} +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 diff --git a/test/matlab.jl b/test/matlab.jl index 527a097..07890f8 100644 --- a/test/matlab.jl +++ b/test/matlab.jl @@ -266,6 +266,41 @@ @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. diff --git a/test/runtests.jl b/test/runtests.jl index d122444..6627608 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,7 @@ module ImageCoreTests using ImageCore +using OffsetArrays: OffsetArray using Test, ReferenceTests using Aqua, Documenter # for meta quality checks From 251fb2d648cd60f8ac70f7b6a39af4a65d04931f Mon Sep 17 00:00:00 2001 From: Johnny Chen Date: Thu, 21 Apr 2022 03:34:18 +0800 Subject: [PATCH 12/12] fix UInt8 indexed image for im_from_matlab --- src/matlab.jl | 2 +- test/matlab.jl | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/matlab.jl b/src/matlab.jl index 15361c7..d09d582 100644 --- a/src/matlab.jl +++ b/src/matlab.jl @@ -132,7 +132,7 @@ _im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Gray,T<:Real} = colo _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{T}, index, values) +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)) diff --git a/test/matlab.jl b/test/matlab.jl index 07890f8..39f8394 100644 --- a/test/matlab.jl +++ b/test/matlab.jl @@ -149,6 +149,12 @@ 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