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

Create a Complement type #189

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

mkitti
Copy link

@mkitti mkitti commented Nov 29, 2023

This adds a Complement{C,T,N} type.

The complement is to be interpreted as a lazy complement.

Math on Complement affects its underlying Colorant.

@mkitti
Copy link
Author

mkitti commented Nov 29, 2023

I will probably move this into its file that is included near the top of the file.

Copy link

codecov bot commented Nov 29, 2023

Codecov Report

Attention: Patch coverage is 0% with 46 lines in your changes are missing coverage. Please review.

Project coverage is 77.63%. Comparing base (4d8eaaa) to head (7245c8b).

❗ Current head 7245c8b differs from pull request most recent head 070de6b. Consider uploading reports for the commit 070de6b to get more accurate results

Files Patch % Lines
src/ColorVectorSpace.jl 0.00% 46 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master     #189       +/-   ##
===========================================
- Coverage   91.04%   77.63%   -13.41%     
===========================================
  Files           3        3               
  Lines         268      313       +45     
===========================================
- Hits          244      243        -1     
- Misses         24       70       +46     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@mkitti
Copy link
Author

mkitti commented Nov 29, 2023

@tlnagy @timholy could you review?

@tlnagy
Copy link
Member

tlnagy commented Dec 6, 2023

I don't have a ton of experience with ColorVectorSpace, but this seems like a reasonable implementation.

Copy link
Member

@timholy timholy left a comment

Choose a reason for hiding this comment

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

I like introducing Complement but I worry the interpretation here isn't consistent. Perhaps you can explain more about why you think it should work this way?

`convert` to `C` at which point `complement` will be applied. Like `complement`
the alpha channel is not modified.

The main interpretation of this type is to invert the *interpretation* of the
Copy link
Member

Choose a reason for hiding this comment

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

Two "interpretation"s on one line. Do you mean "main application"?

Comment on lines 254 to 255
Mathematical operations on `Complement` will affect the underlying
`Colorant` rather than changing the value of the complement. That is
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 worried about this behavior---generally, we value the interpretation above the representation. What should Complement(0.1) + 0.1 return? I'd say 1.0 and not 0.2. Complement(0.0) is white, but with this logic, Complement(0.0) == 1.0 should return false.

Copy link
Author

Choose a reason for hiding this comment

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

Perhaps Complement(0.1) + 0.1 should be Complement(0.0) then. That is Complement(x) + y is Complement(x-y).

@mkitti
Copy link
Author

mkitti commented Dec 20, 2023

I'm tempted to drop the math part from line 315 down

@tlnagy
Copy link
Member

tlnagy commented Jan 9, 2024

Bump, this would be nice to have merged for tlnagy/TiffImages.jl#134

src/ColorVectorSpace.jl Outdated Show resolved Hide resolved
@mkitti mkitti marked this pull request as ready for review January 9, 2024 19:30
@mkitti
Copy link
Author

mkitti commented Jan 9, 2024

@tlnagy what are the minimum features that you need?

@tlnagy
Copy link
Member

tlnagy commented Jan 9, 2024

I think it would be nice for Complemented values to be realized anytime you look at them. So it would be lazy until you accessed the values (like for display)

julia> res = Gray{N0f8}.(Matrix(I, 5, 5));

julia> Complement.(res) # doesn't work

TypeError: in typeassert, expected Complement{N0f8, 1, Gray{N0f8}}, got a value of type Gray{N0f8}

Stacktrace:
 [1] setindex!
   @ ./array.jl:971 [inlined]
 [2] setindex!
   @ ./multidimensional.jl:670 [inlined]
 [3] macro expansion
   @ ./broadcast.jl:974 [inlined]
 [4] macro expansion
   @ ./simdloop.jl:77 [inlined]
 [5] copyto!
   @ ./broadcast.jl:973 [inlined]
 [6] copyto!
   @ ./broadcast.jl:926 [inlined]
 [7] copy
   @ ./broadcast.jl:898 [inlined]
 [8] materialize(bc::Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{2}, Nothing, Type{Complement}, Tuple{Matrix{Gray{N0f8}}}})
   @ Base.Broadcast ./broadcast.jl:873
 [9] top-level scope
   @ In[12]:1

julia> convert.(Gray{N0f8}, Complement.(res)) # does work, but clunky

For TiffImages the idea would be to return an <: AbstractArray{Complement{Gray{N0f8}}, 3} and the users wouldn't even be able to tell that the complementing is happening when they access the data. It should just happen behind the scenes and behave identically to the standard <: AbstractArray{Gray{N0f8}, 3} containers.

@mkitti
Copy link
Author

mkitti commented Jan 9, 2024

Wait... why doesn't Complement.(res) work?

julia> using ColorVectorSpace, Colors, FixedPointNumbers, LinearAlgebra

julia> res = Gray{N0f8}.(Matrix(I, 5, 5));

julia> Complement.(res)
5×5 Array{Complement{N0f8},2} with eltype Complement{N0f8, 1, Gray{N0f8}}:
 Complement(Gray{N0f8}(1.0))  Complement(Gray{N0f8}(0.0))    Complement(Gray{N0f8}(0.0))  Complement(Gray{N0f8}(0.0))
 Complement(Gray{N0f8}(0.0))  Complement(Gray{N0f8}(1.0))     Complement(Gray{N0f8}(0.0))  Complement(Gray{N0f8}(0.0))
 Complement(Gray{N0f8}(0.0))  Complement(Gray{N0f8}(0.0))     Complement(Gray{N0f8}(0.0))  Complement(Gray{N0f8}(0.0))
 Complement(Gray{N0f8}(0.0))  Complement(Gray{N0f8}(0.0))     Complement(Gray{N0f8}(1.0))  Complement(Gray{N0f8}(0.0))
 Complement(Gray{N0f8}(0.0))  Complement(Gray{N0f8}(0.0))     Complement(Gray{N0f8}(0.0))  Complement(Gray{N0f8}(1.0))

I see that it should display, but it should "work".

@tlnagy
Copy link
Member

tlnagy commented Jan 9, 2024

I had ImageShow loaded in a Jupyter notebook, maybe that's why?

IMO the lazy complement should be realized on display.

@mkitti
Copy link
Author

mkitti commented Jan 9, 2024

I think the problem is that Complement is a Colorant rather than a Color. Perhaps it should be a Color.

@mkitti
Copy link
Author

mkitti commented Jan 10, 2024

I made Complement <: Color, so now it renders in VS Code. I'm not sure if that is the best design at the moment. Maybe I should write particular display for Complement?

@tlnagy
Copy link
Member

tlnagy commented Jan 10, 2024

Would it be possible to write a display for Complement that just punts to the correct display based on the wrapped type? i.e. Complement{T, N, C <: Colorant{T,N}} realizes the transform and then punts to the display for Colorant{T, N} etc

@mkitti
Copy link
Author

mkitti commented Jan 10, 2024

I believe so.

My understanding is that this is implemented in Colors.jl:

https://github.com/JuliaGraphics/Colors.jl/blob/master/src%2Fdisplay.jl

It looks like we just need to overload show for Complement. I think the implementation could be simple in that we would do the conversion to C, the underlying Colorant, and then invoke its show method.

@mkitti
Copy link
Author

mkitti commented Jan 10, 2024

I have an implementation of display now.

julia> using ColorVectorSpace, Colors, FixedPointNumbers, LinearAlgebra

julia> res = rand(RGB{Float64}, 10, 10)

julia> c_res = reinterpret(Complement, res) # less allocations

juila> using TestImages

julia> cameraman = testimage("cameraman.tif")

juila> c_cameraman = reinterpret(Complement, cameraman) # no allocation complement

julia> ca_cameraman = ComplementArray(c_cameraman) # no allocation, now of element type Gray{N0f8}

julia> [cameraman ca_cameraman]

@mkitti
Copy link
Author

mkitti commented Jan 10, 2024

I'm getting tempted to implement this in its own package. It seems slightly out of place here and we might be able to iterate on it faster if it were independent.

@tlnagy
Copy link
Member

tlnagy commented Jan 10, 2024

Looks great! There's a slight issue though with display for larger images:

using ColorVectorSpace
using Colors
using FixedPointNumbers
using ImageShow
using LinearAlgebra

res = Gray{N0f8}.(Matrix(I, 500, 500)) # no warning
#snip

@time reinterpret(Complement, res) # works but with warning
#snip
┌ Warning: Output swatches are reduced due to the large size (500×500).
│ Load the ImageShow package for large images.
└ @ Colors ~/.julia/packages/Colors/mIuXl/src/display.jl:15

This happens despite ImageShow being loaded and this warning doesn't happen for non-Complemented images.

EDIT: wrapping with ComplementArray fixes this:

ComplementArray(reinterpret(Complement, res)) # no warning

@mkitti
Copy link
Author

mkitti commented Jan 24, 2024

Should we push to get this merged now or should I break out into a package to iterate on this independently?

@tlnagy
Copy link
Member

tlnagy commented Jan 24, 2024

Given that complement lives here, I think it makes sense to have the lazy version here as well. But I'm willing to defer to other folks who know the Colors ecosystem better. Would be nice to have merged either way.

@timholy
Copy link
Member

timholy commented Jan 24, 2024

Sorry, I'll try to get this a review by next Monday. I got started and remembered getting worried about something fundamental, but that was long enough ago that I've forgotten.

@mkitti
Copy link
Author

mkitti commented Jan 25, 2024

There's been considerable changes since you last took a look, Tim.

  1. Complement subtypes Color instead of Colorant
  2. I changed the order of parameters from {T,N,C} to {C,T,N}
  3. I dropped arithmetic on Complement
  4. Accessing the components of Complement returns the complement of the parent Colorant.

@mkitti
Copy link
Author

mkitti commented Jan 25, 2024

Implementation of Base.reinterpret(::Type{Complement}, array::AbstractArray{C}) where {T, N, C <: Colorant{T,N}} may be an abuse because Complement by itself is not fully specified with parameters. However, it is inferrable that we mean Complement{C,T,N}

Copy link
Member

@timholy timholy left a comment

Choose a reason for hiding this comment

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

I'm on board with the general idea. Needs tests.

src/ColorVectorSpace.jl Outdated Show resolved Hide resolved
src/ColorVectorSpace.jl Outdated Show resolved Hide resolved
Base.parent(a::ComplementArray) = a.parent
Base.size(a::ComplementArray) = size(a.parent)
function Base.getindex(a::ComplementArray{T}, I::Vararg{Int,N})::T where {T,N}
getindex(a.parent, I...)
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 confused, shouldn't this be complement(getindex(a.parent, I...))? Basically this works like mappedarray(complement, A), right?

Which begs the question, do we actually need ComplementArray? But perhaps I misunderstand the intention.

Copy link
Member

Choose a reason for hiding this comment

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

Is this still a TODO? Given that I don't know what this is about, I'm unsure.

getindex(a.parent, I...)
end
function Base.setindex!(a::ComplementArray{T}, v, I::Vararg{Int, N}) where {T,N}
setindex!(a.parent, convert(eltype(a.parent), v))
Copy link
Member

Choose a reason for hiding this comment

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

The convert will happen automatically, no need to do it manually.

function Base.setindex!(a::ComplementArray{T}, v, I::Vararg{Int, N}) where {T,N}
setindex!(a.parent, convert(eltype(a.parent), v))
end
Base.IndexStyle(a::ComplementArray) = IndexStyle(a.parent)
Copy link
Member

Choose a reason for hiding this comment

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

If you do this you should also implement getindex for a single Int.

Copy link
Member

Choose a reason for hiding this comment

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

Given your Vararg{Int, N} for any N I doubt that's really needed.

Base.IndexStyle(a::ComplementArray) = IndexStyle(a.parent)

function Base.show(io::IO, mime::MIME"image/svg+xml", c::Complement{C}) where C
show(io, mime, C(c))
Copy link
Member

Choose a reason for hiding this comment

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

What happens without this method? I would think that if you have all the abstractions correct, this Just Works without needing a new method. Similar thoughts about the other methods below.

@tlnagy
Copy link
Member

tlnagy commented Feb 27, 2024

Bump @mkitti, this would be great to have!

@mkitti
Copy link
Author

mkitti commented Feb 27, 2024

I agree. I just have to find some time to get back around to it. Feel free to beat me to it.

Copy link
Author

@mkitti mkitti left a comment

Choose a reason for hiding this comment

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

Commit suggestions from Tim Holy

@mkitti mkitti requested a review from timholy May 6, 2024 07:39
@mkitti
Copy link
Author

mkitti commented May 6, 2024

@timholy and @tlnagy I have written tests.

Copy link
Member

@timholy timholy left a comment

Choose a reason for hiding this comment

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

Thanks for adding the tests and other fixes! It's looking good, but I've left a few other notes about things that seem likely to need fixing.

In cases where you're unsure how something should behave, I tend to expect that mappedarray(complement, a) should be a reasonable guide for how ComplementArray should behave. IF, that is, I understand why it's here at all.

Base.parent(a::ComplementArray) = a.parent
Base.size(a::ComplementArray) = size(a.parent)
function Base.getindex(a::ComplementArray{T}, I::Vararg{Int,N})::T where {T,N}
getindex(a.parent, I...)
Copy link
Member

Choose a reason for hiding this comment

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

Is this still a TODO? Given that I don't know what this is about, I'm unsure.

@test red(comp) ≈ red(comp2)
@test green(comp) ≈ green(comp2)
@test blue(comp) ≈ blue(comp2)
@test alpha(comp) == alpha(comp2)
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
@test alpha(comp) == alpha(comp2)
@test alpha(comp) == alpha(comp2) == alpha(_color)

@test red(comp) ≈ red(comp2)
@test green(comp) ≈ green(comp2)
@test blue(comp) ≈ blue(comp2)
@test alpha(comp) == alpha(comp2)
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
@test alpha(comp) == alpha(comp2)
@test alpha(comp) == alpha(comp2) == 1

@test color_type(comp) == Complement{RGB{Float64}, Float64, 4}
@test base_color_type(comp) == Complement{RGB, <:Any, 4}
@test base_colorant_type(comp) == Complement{RGBA, <:Any, 4}
@test red(comp) ≈ red(comp2)
Copy link
Member

Choose a reason for hiding this comment

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

Any reason these shouldn't be == instead of ?

@test base_color_type(comp) == Complement{Gray, <:Any, 1}
@test base_colorant_type(comp) == Complement{Gray, <:Any, 1}
@test gray(comp) ≈ gray(comp2)
@test alpha(comp) == alpha(comp2)
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
@test alpha(comp) == alpha(comp2)
@test alpha(comp) == alpha(comp2) == 1

struct ComplementArray{T,N,A<:AbstractArray{<:Complement{T},N}} <: AbstractArray{T,N}
parent::A
ComplementArray(parent::A) where {T,N,A <: AbstractArray{<:Complement{T},N}} = new{T,N,A}(parent)
end
Copy link
Member

Choose a reason for hiding this comment

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

Fundamentally I think I don't understand what this is for. I imagine this is the same as ca = mappedarray(complement, a) but without relying on MappedArrays. Is that true, or is it something else?

ColorTypes.color(c::Complement) = Complement(color(parent(c)))
ColorTypes.color_type(::Type{Complement{C, T, N}}) where {C,T,N} = Complement{color_type(C), T, N}
ColorTypes.base_color_type(::Type{<: Complement{C, <: Any, N}}) where {N,C} = Complement{base_color_type(C), <: Any, N}
ColorTypes.base_colorant_type(::Type{<: Complement{C, <: Any, N}}) where {N,C} = Complement{base_colorant_type(C), <: Any, N}
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 not sure we always want the <:Any on the RHS. Can we also specialize ColorTypes.base_color_type(::Type{Complement{C, T, N}}) where {C,T,N}?

Moreover, since base_color_type(complement(c)) === base_color_type(c), I suspect you want the same property for Complement. I think of Complement as something that represents a lazy computation but it doesn't fundamentally change the underlying colorspace.

@test reinterpret(typeof(comp), _color) == comp
@test reinterpret(Complement, _color) == comp
end
@test all(reinterpret(typeof(comp), [_color]) .≈ [comp])
Copy link
Member

Choose a reason for hiding this comment

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

Missing:

julia> complement(complement(c)) == c
true

julia> Complement(Complement(c)) == c
false

The latter should fail for === but pass for ==.

function Base.setindex!(a::ComplementArray{T}, v, I::Vararg{Int, N}) where {T,N}
setindex!(a.parent, convert(eltype(a.parent), v))
end
Base.IndexStyle(a::ComplementArray) = IndexStyle(a.parent)
Copy link
Member

Choose a reason for hiding this comment

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

Given your Vararg{Int, N} for any N I doubt that's really needed.

comp2 = complement(_color)
@test comp isa Complement{RGBA{Float64}, Float64, 4}
@test color_type(comp) == Complement{RGB{Float64}, Float64, 4}
@test base_color_type(comp) == Complement{RGB, <:Any, 4}
Copy link
Member

Choose a reason for hiding this comment

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

When you're testing types I recommend === (it's nicer on the compiler, for one, and should let the tests run more quickly). Not sure I agree with the choice for the RHS though.

@mkitti
Copy link
Author

mkitti commented May 6, 2024

Thanks for adding the tests and other fixes! It's looking good, but I've left a few other notes about things that seem likely to need fixing.

In cases where you're unsure how something should behave, I tend to expect that mappedarray(complement, a) should be a reasonable guide for how ComplementArray should behave. IF, that is, I understand why it's here at all.

  • mappedarray(complement, a) is a good choice if you have an array of C <: Colorant and want an AbstractArray{C} that has an element type of C.
  • ComplementArray is for when you have a AbstractArray{Complement} and want a AbstractArray{C} where C <: Colorant. Now that you mention mappedarray, it could probably just be a mappedarray(parent, a).

@kimikage
Copy link
Collaborator

kimikage commented May 7, 2024

it could probably just be a mappedarray(parent, a).

or more symmetrically, mappedarray(complement, a)?
(In short, we don't care if it supports Base.parent() or not.)

@kimikage
Copy link
Collaborator

kimikage commented May 9, 2024

My main concern is operation and support in the downstream packages.

julia> Complement(RGB(1,1,0)) isa AbstractRGB
false

It is not very pleasant to have to wrap AbstractRGB (or AbstactGray) with Union, in order to support Complement.
One ad hoc solution is to define ComplementRGB{T} <: AbstractRGB{T}, etc., respectively.😑

One idea is to push such an ugly hack into a separate package.
cf. https://github.com/kimikage/StorageColorTypes.jl

Edit:
In other words, my real concern is whether to implement InvertedGray/ComplementGray in ColorTypes.jl at the start point of the discussion.
xref: JuliaGraphics/ColorTypes.jl#295 (comment)

@kimikage
Copy link
Collaborator

kimikage commented May 9, 2024

For reference, AbstractGray will be changed from an alias to an abstract type (sometime in the future).
cf. JuliaGraphics/ColorTypes.jl#252
Ironically, the change is to address this issue properly. (JuliaGraphics/ColorTypes.jl#230)

@timholy
Copy link
Member

timholy commented May 10, 2024

The other option is to convert AbstractRGB into a trait. That's feasible but a little harder to work with (given that traits do not have a standard convenient interface) than leaving it as an abstract type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants