Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Added findaxis, finddim, and hasdim #89

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/ImageCore.jl
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export
# traits
assert_timedim_last,
coords_spatial,
findaxis,
finddim,
hasdim,
height,
indices_spatial,
namedaxes,
Expand Down
73 changes: 65 additions & 8 deletions src/traits.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,83 @@ is the corresponding dimensions's axis. If `HasDimNames` is not defined for `x`
default names are returned. `x` should have an `axes` method.

```jldoctest
julia> using ImagesCore
julia> using ImageCore

julia> img = reshape(1:24, 2,3,4);

julia> namedaxes(img)
(dim_1 = Base.OneTo(2), dim_2 = Base.OneTo(3), dim_3 = Base.OneTo(4))
(:row = Base.OneTo(2), :col = Base.OneTo(3), :page = Base.OneTo(4))
```
"""
namedaxes(img::T) where T = namedaxes(HasDimNames(T), img)
namedaxes(img::T) where T = _namedaxes(HasDimNames(T), img)
_namedaxes(::HasDimNames{true}, img) = NamedTuple{names(img)}(axes(img))
_namedaxes(::HasDimNames{false}, img) = NamedTuple{default_names(img)}(axes(img))

default_names(img::AbstractArray{T,N}) where {T,N} = ntuple(i->default_name(i), N)

@inline function default_name(i::Integer)
if i == 1
return :row
elseif i == 2
return :col
elseif i == 3
return :page
else
return Symbol(:dim_, i)
end
end

namedaxes(::HasDimNames{true}, x::T) where T = NamedTuple{names(x)}(axes(x))
"""
findaxis(img, name) -> axis

function namedaxes(::HasDimNames{false}, img::AbstractArray{T,N}) where {T,N}
NamedTuple{default_names(Val(N))}(axes(img))
Returns the axis (as in `axes(img, dim)`) that corresponds to the `name`. If no
matching name is found any empy axis (i.e. `0:0`) is returned.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
matching name is found any empy axis (i.e. `0:0`) is returned.
matching name is found any empty axis (i.e. `0:-1`) is returned.

I almost wonder if axisrange is a better name for this operation?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this case would you be thinking of the axis as the names and range as the tuple of axes?

Copy link
Member

Choose a reason for hiding this comment

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

I'm just trying to ask myself, "what would I expect findaxis to do?" And I am not sure. findmin returns both the value and location of the minimum. So maybe findaxis should return the value (i.e., range) and dimension index (like finddim)? Or maybe axisvalue, which is again back to the names chosen by AxisArrays.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess I'm trying to imitate the behavior of axes(img, i) where i would be a symbol. The trick is that whatever type img is may not wrap axes that are also directly linked to their names (e.g., one array type stores names wraps another type that stores axes). So if we can access the axes and names independently then we can use namedaxes to connect these otherwise independent features.

I think the rationale of findaxis returning something similar to findmin makes a lot of sense though. In this case maybe findaxis would return something like NamedTuple{(:dim_1)}((1:10,)) and axisvalue would return the individual axis/range.

Copy link
Member

Choose a reason for hiding this comment

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

I could go with that latter proposal.

"""
@inline findaxis(A, name::Symbol) = _findaxis(namedaxes(A), name)
function _findaxis(nt::NamedTuple{syms}, name::Symbol) where {syms}
if __hasdim(syms, name)
return getfield(nt, name)
else
return 0:-1
end
end

@generated function default_names(img::Val{N}) where {N}
:($(ntuple(i -> Symbol(:dim_, i), N)))
"""
finddim(img, name) -> Int

Returns the dimension that has the corresponding `name`. If `name` doesn't
match any of the dimension names `0` is returned. If `img` doesn't have `names`
then the default set of names is searched (e.g., dim_1, dim_2, ...).
"""
@inline finddim(A::T, name::Symbol) where {T} = _finddim(HasDimNames(T), A, name)
_finddim(::HasDimNames{false}, A::T, name::Symbol) where {T} = __finddim(default_names(A), name)
_finddim(::HasDimNames{true}, A::T, name::Symbol) where {T} = __finddim(names(A), name)

Base.@pure function __finddim(dimnames::NTuple{N,Symbol}, name::Symbol) where {N}
for i in 1:N
getfield(dimnames, i) === name && return i
end
return 0
end


"""
hasdim(x, name) -> Bool

Returns `true` if `name` is present.
"""
hasdim(x::T, name::Symbol) where {T} = _hasdim(HasDimNames(T), x, name)
_hasdim(::HasDimNames{true}, x::T, name::Symbol) where {T} = __hasdim(names(x), name)
_hasdim(::HasDimNames{false}, x::T, name::Symbol) where {T} = __hasdim(default_names(x), name)

Base.@pure function __hasdim(dimnames::Tuple{Vararg{Symbol,N}}, name::Symbol) where {N}
for ii in 1:N
getfield(dimnames, ii) === name && return true
end
return false
end


"""
pixelspacing(img) -> (sx, sy, ...)

Expand Down
25 changes: 21 additions & 4 deletions test/traits.jl
Original file line number Diff line number Diff line change
Expand Up @@ -169,21 +169,38 @@ ImageCore.HasProperties(::Type{<:RowVector}) = HasProperties{true}()

Base.names(::RowVector) = (:row,)
Base.axes(rv::RowVector) = axes(rv.v)

Base.size(rv::RowVector) = size(rv.v)

@testset "Trait Interface" begin
img = reshape(1:24, 2,3,4)
@test @inferred(namedaxes(img)) == NamedTuple{(:dim_1, :dim_2, :dim_3)}(axes(img))
@test @inferred(namedaxes(img)) == NamedTuple{(:row, :col, :page)}(axes(img))
@test @inferred(HasDimNames(img)) == HasDimNames{false}()
@test @inferred(HasProperties(img)) == HasProperties{false}()

rv = RowVector([1:10...], Dict{String,Any}())
rv = RowVector([1:10...], Dict{String,Any}());
@test @inferred(HasDimNames(rv)) == HasDimNames{true}()
@test @inferred(HasProperties(rv)) == HasProperties{true}()
@test @inferred(namedaxes(rv)) == NamedTuple{(:row,)}((Base.OneTo(10),))

@test @inferred(hasdim(rv, :row)) == true
@test @inferred(hasdim(rv, :time)) == false
@test @inferred(hasdim(img, :time)) == false

@test @inferred(finddim(rv, :time)) == 0
@test @inferred(finddim(rv, :row)) == 1
@test @inferred(finddim(img, :page)) == 3

@test findaxis(rv, :time) == 0:-1
@test findaxis(rv, :row) == Base.OneTo(10)
@test findaxis(img, :page) == Base.OneTo(4)

# default names
@test @inferred(default_names(Val(3))) == (:dim_1, :dim_2, :dim_3)
@test @inferred(default_names(img)) == (:row, :col, :page)
@test @inferred(ImageCore.default_name(1)) == :row
@test @inferred(ImageCore.default_name(2)) == :col
@test @inferred(ImageCore.default_name(3)) == :page
@test @inferred(ImageCore.default_name(4)) == :dim_4

end

nothing