Skip to content

Commit 508612e

Browse files
committed
add im_from_matlab
1 parent 65e93c9 commit 508612e

File tree

5 files changed

+233
-1
lines changed

5 files changed

+233
-1
lines changed

Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ MosaicViews = "e94cdb99-869f-56ef-bcf0-1ae2bcbe0389"
1313
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
1414
PaddedViews = "5432bcbf-9aad-5242-b902-cca2824c8663"
1515
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
16+
StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a"
1617

1718
[compat]
1819
AbstractFFTs = "0.4, 0.5, 1.0"
@@ -25,6 +26,7 @@ MosaicViews = "0.3.3"
2526
OffsetArrays = "0.8, 0.9, 0.10, 0.11, 1.0.1"
2627
PaddedViews = "0.5.8"
2728
Reexport = "0.2, 1.0"
29+
StructArrays = "0.5, 0.6"
2830
julia = "1"
2931

3032
[extras]

src/ImageCore.jl

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ using OffsetArrays # for show.jl
1212
using .ColorTypes: colorant_string
1313
using Colors: Fractional
1414
using MappedArrays: AbstractMultiMappedArray
15+
@reexport using StructArrays: StructArray # for struct of array layout
1516

1617
using Base: tail, @pure, Indices
1718
import Base: float
@@ -91,7 +92,10 @@ export
9192
spacedirections,
9293
spatialorder,
9394
width,
94-
widthheight
95+
widthheight,
96+
# matlab compatibility
97+
im_from_matlab
98+
9599

96100
include("colorchannels.jl")
97101
include("stackedviews.jl")
@@ -100,6 +104,7 @@ include("traits.jl")
100104
include("map.jl")
101105
include("show.jl")
102106
include("functions.jl")
107+
include("matlab.jl")
103108
include("deprecations.jl")
104109

105110
"""

src/matlab.jl

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Convenient utilities for MATLAB image layout: the color channel is stored as the last dimension.
2+
#
3+
# These function do not intent to cover all use cases
4+
# because numerical arrays do not contain colorspace information.
5+
6+
7+
"""
8+
im_from_matlab([CT], X::AbstractArray) -> AbstractArray{CT}
9+
10+
Convert numerical array `X` to colorant array, using the MATLAB image layout convention.
11+
12+
The input image `X` is assumed to be either grayscale image or RGB image. For other
13+
colorspaces, the input `X` must be converted to RGB colorspace first.
14+
15+
```julia
16+
im_from_matlab(rand(4, 4)) # 4×4 Gray image
17+
im_from_matlab(rand(4, 4, 3)) # 4×4 RGB image
18+
19+
im_from_matlab(GrayA, rand(4, 4, 2)) # 4×4 Gray-alpha image
20+
im_from_matlab(HSV, rand(4, 4, 3)) # 4×4 HSV image
21+
```
22+
23+
Integer values must be converted to float point numbers or fixed point numbers first. For
24+
instance:
25+
26+
```julia
27+
img = rand(1:255, 16, 16) # 16×16 Int array
28+
29+
im_from_matlab(img ./ 255) # convert to Float64 first
30+
im_from_matlab(UInt8.(img)) # convert to UInt8 first
31+
```
32+
33+
!!! tip "lazy conversion"
34+
To save memory allocation, the conversion is done in lazy mode. In some cases, this
35+
could introduce performance overhead due to the repeat computation. This can be easily
36+
solved by converting eagerly via, e.g., `collect(im_from_matlab(...))`.
37+
38+
See also: [`im_to_matlab`](@ref).
39+
"""
40+
function im_from_matlab end
41+
42+
# Step 1: convenient conventions
43+
# - 1d numerical vector is Gray image
44+
# - 2d numerical array is Gray image
45+
# - 3d numerical array of size (m, n, 3) is RGB image
46+
# For other cases, users must specify `CT` explicitly; otherwise it is not type-stable
47+
im_from_matlab(X::AbstractVector) = vec(im_from_matlab(reshape(X, (length(X), 1))))
48+
im_from_matlab(X::AbstractMatrix{T}) where {T<:Real} = im_from_matlab(Gray, X)
49+
function im_from_matlab(X::AbstractArray{T,3}) where {T<:Real}
50+
if size(X, 3) != 3
51+
msg = "Unrecognized MATLAB image layout."
52+
hint = size(X, 3) == 1 ? "Do you mean `im_from_matlab(reshape(X, ($(size(X)[1:2]...))))`?" : ""
53+
msg = isempty(hint) ? msg : "$msg $hint"
54+
throw(ArgumentError(msg))
55+
end
56+
return im_from_matlab(RGB, X)
57+
end
58+
im_from_matlab(X::AbstractArray) = throw(ArgumentError("Unrecognized MATLAB image layout."))
59+
60+
# Step 2: storage type conversion
61+
function im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT,T}
62+
if T<:Union{Normed, AbstractFloat}
63+
return _im_from_matlab(CT, X)
64+
else
65+
msg = "Unrecognized element type $T, manual conversion to float point number or fixed point number is needed."
66+
hint = _matlab_type_hint(X)
67+
msg = isempty(hint) ? msg : "$msg $hint"
68+
throw(ArgumentError(msg))
69+
end
70+
end
71+
im_from_matlab(::Type{CT}, X::AbstractArray{UInt8}) where CT = _im_from_matlab(CT, reinterpret(N0f8, X))
72+
im_from_matlab(::Type{CT}, X::AbstractArray{UInt16}) where CT = _im_from_matlab(CT, reinterpret(N0f16, X))
73+
function im_from_matlab(::Type{CT}, X::AbstractArray{Int16}) where CT
74+
# MALTAB compat
75+
_im2double(x) = (Float64(x)+Float64(32768))/Float64(65535)
76+
return _im_from_matlab(CT, mappedarray(_im2double, X))
77+
end
78+
79+
function _matlab_type_hint(@nospecialize X)
80+
mn, mx = extrema(X)
81+
if mn >= typemin(UInt8) && mx <= typemax(UInt8)
82+
return "For instance: `UInt8.(X)` or `X./$(typemax(UInt8))`"
83+
elseif mn >= typemin(UInt16) && mx <= typemax(UInt16)
84+
return "For instance: `UInt16.(X)` or `X./$(typemax(UInt16))`"
85+
else
86+
return ""
87+
end
88+
end
89+
90+
# Step 3: colorspace conversion
91+
_im_from_matlab(::Type{CT}, X::AbstractArray{CT}) where CT<:Colorant = X
92+
function _im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Colorant, T<:Real}
93+
_CT = isconcretetype(CT) ? CT : base_colorant_type(CT){T}
94+
# FIXME(johnnychen94): not type inferrable here
95+
return StructArray{_CT}(X; dims=3)
96+
end
97+
_im_from_matlab(::Type{CT}, X::AbstractArray{T}) where {CT<:Gray, T<:Real} = of_eltype(CT, X)

test/matlab.jl

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
@testset "MATLAB" begin
2+
@testset "im_from_matlab" begin
3+
@testset "Gray" begin
4+
# Float64
5+
data = rand(4, 5)
6+
img = @inferred im_from_matlab(data)
7+
@test eltype(img) == Gray{Float64}
8+
@test size(img) == (4, 5)
9+
@test channelview(img) == data
10+
11+
# N0f8
12+
data = rand(N0f8, 4, 5)
13+
img = @inferred im_from_matlab(data)
14+
mn, mx = extrema(img)
15+
@test eltype(img) == Gray{N0f8}
16+
@test size(img) == (4, 5)
17+
@test 0.0 <= mn <= mx <= 1.0
18+
19+
# UInt8
20+
data = rand(UInt8, 4, 5)
21+
img = @inferred im_from_matlab(data)
22+
mn, mx = extrema(img)
23+
@test eltype(img) == Gray{N0f8}
24+
@test size(img) == (4, 5)
25+
@test 0.0 <= mn <= mx <= 1.0
26+
27+
# UInt16
28+
data = rand(UInt16, 4, 5)
29+
img = @inferred im_from_matlab(data)
30+
mn, mx = extrema(img)
31+
@test eltype(img) == Gray{N0f16}
32+
@test size(img) == (4, 5)
33+
@test 0.0 <= mn <= mx <= 1.0
34+
35+
# Int16 -- MATLAB's im2double supports Int16
36+
data = rand(Int16, 4, 5)
37+
img = @inferred im_from_matlab(data)
38+
mn, mx = extrema(img)
39+
@test eltype(img) == Gray{Float64}
40+
@test size(img) == (4, 5)
41+
@test 0.0 <= mn <= mx <= 1.0
42+
data = Int16[-32768 0; 0 32767]
43+
@test isapprox([0.0 0.5; 0.5 1.0], @inferred im_from_matlab(data); atol=1e-4)
44+
45+
# Int is ambiguious -- manual conversion is required but we provide some basic hints
46+
data = rand(1:255, 4, 5)
47+
msg = "Unrecognized element type $(Int), manual conversion to float point number or fixed point number is needed. For instance: `UInt8.(X)` or `X./255`"
48+
@test_throws ArgumentError(msg) im_from_matlab(data)
49+
data = rand(256:65535, 4, 5)
50+
msg = "Unrecognized element type $(Int), manual conversion to float point number or fixed point number is needed. For instance: `UInt16.(X)` or `X./65535`"
51+
@test_throws ArgumentError(msg) im_from_matlab(data)
52+
53+
# vector
54+
data = rand(UInt8, 4)
55+
img = @inferred im_from_matlab(data)
56+
@test eltype(img) == Gray{N0f8}
57+
@test size(img) == (4,)
58+
end
59+
60+
@testset "RGB" begin
61+
# Float64
62+
data = rand(4, 5, 3)
63+
img = im_from_matlab(data)
64+
@test_broken @inferred im_from_matlab(data)
65+
@test_nowarn @inferred collect(im_from_matlab(data)) # type inference issue only occurs in lazy mode
66+
@test eltype(img) == RGB{Float64}
67+
@test size(img) == (4, 5)
68+
@test permutedims(channelview(img), (2, 3, 1)) == data
69+
70+
# N0f8
71+
data = rand(N0f8, 4, 5, 3)
72+
img = im_from_matlab(data)
73+
@test_broken @inferred im_from_matlab(data)
74+
@test_nowarn @inferred collect(im_from_matlab(data)) # type inference issue only occurs in lazy mode
75+
mn, mx = extrema(channelview(img))
76+
@test eltype(img) == RGB{N0f8}
77+
@test size(img) == (4, 5)
78+
@test 0.0 <= mn <= mx <= 1.0
79+
80+
# UInt8
81+
data = rand(UInt8, 4, 5, 3)
82+
img = im_from_matlab(data)
83+
@test_broken @inferred im_from_matlab(data)
84+
@test_nowarn @inferred collect(im_from_matlab(data)) # type inference issue only occurs in lazy mode
85+
mn, mx = extrema(channelview(img))
86+
@test eltype(img) == RGB{N0f8}
87+
@test size(img) == (4, 5)
88+
@test 0.0 <= mn <= mx <= 1.0
89+
90+
# UInt16
91+
data = rand(UInt16, 4, 5, 3)
92+
img = im_from_matlab(data)
93+
@test_broken @inferred im_from_matlab(data)
94+
@test_nowarn @inferred collect(im_from_matlab(data)) # type inference issue only occurs in lazy mode
95+
mn, mx = extrema(channelview(img))
96+
@test eltype(img) == RGB{N0f16}
97+
@test size(img) == (4, 5)
98+
@test 0.0 <= mn <= mx <= 1.0
99+
100+
# Int16 -- MATLAB's im2double supports Int16
101+
data = rand(Int16, 4, 5, 3)
102+
img = im_from_matlab(data)
103+
@test_broken @inferred im_from_matlab(data)
104+
@test_nowarn @inferred collect(im_from_matlab(data)) # type inference issue only occurs in lazy mode
105+
mn, mx = extrema(channelview(img))
106+
@test eltype(img) == RGB{Float64}
107+
@test size(img) == (4, 5)
108+
@test 0.0 <= mn <= mx <= 1.0
109+
110+
# Int is ambiguious -- manual conversion is required but we provide some basic hints
111+
data = rand(1:255, 4, 5, 3)
112+
msg = "Unrecognized element type $(Int), manual conversion to float point number or fixed point number is needed. For instance: `UInt8.(X)` or `X./255`"
113+
@test_throws ArgumentError(msg) im_from_matlab(data)
114+
data = rand(256:65535, 4, 5, 3)
115+
msg = "Unrecognized element type $(Int), manual conversion to float point number or fixed point number is needed. For instance: `UInt16.(X)` or `X./65535`"
116+
@test_throws ArgumentError(msg) im_from_matlab(data)
117+
end
118+
119+
data = rand(4, 4, 2)
120+
msg = "Unrecognized MATLAB image layout."
121+
@test_throws ArgumentError(msg) im_from_matlab(data)
122+
123+
data = rand(4, 4, 3, 1)
124+
msg = "Unrecognized MATLAB image layout."
125+
@test_throws ArgumentError(msg) im_from_matlab(data)
126+
end
127+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ include("convert_reinterpret.jl")
3232
include("traits.jl")
3333
include("map.jl")
3434
include("functions.jl")
35+
include("matlab.jl")
3536
include("show.jl")
3637

3738
# To ensure our deprecations work and don't break code

0 commit comments

Comments
 (0)