ColorVectorSpace.jl icon indicating copy to clipboard operation
ColorVectorSpace.jl copied to clipboard

Return type of `*` for `x::Gray{Bool}` and real number

Open kimikage opened this issue 4 years ago • 7 comments

I haven't made up my mind about what the return type should be if the definition of one for Colorant types is the real number 1. (cf. https://github.com/JuliaGraphics/ColorTypes.jl/issues/235) I found an odd type promotion in the case study.

julia> Gray{N0f8}(1) * 1
Gray{Float32}(1.0f0)

julia> Gray{N0f8}(1) * true
Gray{N0f8}(1.0)

julia> Gray{N0f8}(1) * 1N0f8
Gray{N0f8}(1.0)
julia> Gray{Bool}(1) * 1 # !?
Gray{BigFloat}(1.0)

julia> Gray{Bool}(1) * true # !?
Gray{Float32}(1.0f0)

julia> Gray{Bool}(1) * 1N0f8
Gray{N0f8}(1.0)

kimikage avatar Apr 03 '21 08:04 kimikage

As for multiplication (multype), it is easy to fix, but for other operations, it may be a Pandora's box.:sweat_smile:

kimikage avatar Apr 08 '21 17:04 kimikage

The root of the problem is that Bool has two aspects: the 1-bit "integer" representation and the quantized representation of the "real" number in [0, 1]. As for Gray{Bool}, the latter property is strong. However, the same is true for N0f8, but those types easily overflow, and Inf/NaN cannot be represented. In such a context, the following is reasonable:

julia> 0x1 + Gray{N0f8}(0)
Gray{Float32}(0.0f0)

And the following is completely irrational:

julia> true + Gray{Bool}(0)
Gray{N0f8}(1.0)

It is obvious that the result of addition and subtraction between integers is an integer, and there is no advantage to N0f8 (other than that it is often used in image processing). Of course,

julia> true + Gray{Bool}(1)
ERROR: ArgumentError: 2 is an integer in the range 0-255, but integer inputs are encoded with the N0f8
  type, an 8-bit type representing 256 discrete values between 0 and 1.
  Consider dividing your input values by 255, for example: Gray{N0f8}(2/255)
  Or use `reinterpret(N0f8, x)` if `x` is a `UInt8`.
  See the READMEs for FixedPointNumbers and ColorTypes for more information.

Personally, I think the following seems relatively reasonable. However, it goes against the idea of treating Gray and Real the same way (which, as I've said many times, I don't like :sweat_smile:):

julia> true + Gray{Bool}(1) # the `true` is a `Integer`
Gray{Float32}(2.0)

julia> Gray{Bool}(1) + Gray{Bool}(1) # similar to `Gray{N0f8}(1) + Gray{N0f8}(1)`
Gray{Bool}(false)

At least, there is no reason to maintain the backward compatibility with respect to the Gray{Bool} operations. This is because the current situation is as follows :joy: :

julia> Gray{Bool}(1) + Gray{Bool}(1)
Gray{BigFloat}(2.0)

kimikage avatar Apr 09 '21 02:04 kimikage

Of course, it is obvious that the Gray{Bool} arithmetic operations have never been used in practice, and I have no practical interest in enhancing support for Gray{Bool}. However, the current implementation is a collection of ad-hoc rules, and there are some inconsistencies. For example:

julia> Gray{N0f8}(1) + 0
Gray{Float32}(1.0f0)

julia> Gray24(1) + 0
Gray24(1.0N0f8)
julia> Gray(1.0) / Gray{N0f8}(0.6)
Gray{Float64}(1.6666666666666667)

julia> Gray(1.0) / 0.6N0f8
ERROR: ArgumentError: N0f8 is an 8-bit type representing 256 values from 0.0 to 1.0; cannot represent 1.6666666

kimikage avatar Apr 09 '21 04:04 kimikage

Proposal

cf: https://github.com/JuliaGraphics/ColorVectorSpace.jl/pull/153#issuecomment-837687064

+ Bool N0f8 Int64 Float32 Float64
Gray{Bool} Gray{Bool}:up: Gray{N0f8} Gray{Float32}:up: Gray{Float32} Gray{Float64}
Gray{N0f8} Gray{N0f8} Gray{N0f8} Gray{Float32} Gray{Float32} Gray{Float64}
Gray24 Gray24 Gray24 Gray{Float32}:up: Gray{Float32}:up: Gray{Float64}:up:
Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float64}
+ Gray{Bool} Gray{N0f8} Gray24 Gray{Float32} Gray{Float64}
Gray{Bool} Gray{Bool} Gray{N0f8} Gray{N0f8} Gray{Float32} Gray{Float64}
Gray{N0f8} Gray{N0f8} Gray{N0f8} Gray{N0f8} Gray{Float32} Gray{Float64}
Gray24 Gray{N0f8} Gray{N0f8} Gray24 Gray{Float32} Gray{Float64}
Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float64}
* Bool N0f8 Int64 Float32 Float64
Gray{Bool} Gray{Bool} Gray{N0f8} Gray{Float32} Gray{Float32} Gray{Float64}
Gray{N0f8} Gray{N0f8} Gray{N0f8} Gray{Float32} Gray{Float32} Gray{Float64}
Gray24 Gray24:up: Gray24:up: Gray{Float32} Gray{Float32} Gray{Float64}
Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float64}
* Gray{Bool} Gray{N0f8} Gray24 Gray{Float32} Gray{Float64}
Gray{Bool} Gray{Bool} Gray{N0f8} Gray{N0f8} Gray{Float32} Gray{Float64}
Gray{N0f8} Gray{N0f8} Gray{N0f8} Gray{N0f8} Gray{Float32} Gray{Float64}
Gray24 Gray{N0f8} Gray{N0f8} Gray24 Gray{Float32} Gray{Float64}
Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float64}
/ Bool N0f8 Int64 Float32 Float64
Gray{Bool} Gray{Float32} Gray{Float32}:up: Gray{Float32} Gray{Float32} Gray{Float64}
Gray{N0f8} Gray{Float32}:up: Gray{N0f8} Gray{Float32} Gray{Float32} Gray{Float64}
Gray24 Gray{Float32}:up: Gray24:up: Gray{Float32} Gray{Float32} Gray{Float64}
Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float64}
/ Gray{Bool} Gray{N0f8} Gray24 Gray{Float32} Gray{Float64}
Gray{Bool} Gray{Float32} Gray{Float32}:up: Gray{Float32}:up: Gray{Float32} Gray{Float64}
Gray{N0f8} Gray{Float32}:up: Gray{N0f8} Gray{N0f8} Gray{Float32} Gray{Float64}
Gray24 Gray{Float32}:up: Gray{N0f8} Gray24 Gray{Float32} Gray{Float64}
Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float32} Gray{Float64}

:up: : proposal for v0.10.0

kimikage avatar May 11 '21 03:05 kimikage

LGTM.

Are there any plans to support the other side of /? For example, I see Gray{Bool}/Bool = Gray{Float32} 🆙 (If I understand the order correctly), but don't see Bool/Gray{Bool} in the table.

Current Gray{Bool}/Bool = Gray{Float64} is consistent with Bool/Bool = Float64. I guess people would be surprised when trying to dig into the details. Maybe we can complicate the logic by checking if typeof(true/true) === Float64?

johnnychen94 avatar May 11 '21 07:05 johnnychen94

but don't see Bool/Gray{Bool} in the table.

I added the ::Number/::Gray cases, and updated (fixed) the PR.

cf. https://github.com/JuliaGraphics/ColorVectorSpace.jl/pull/153#issuecomment-835802085, https://github.com/JuliaGraphics/ColorVectorSpace.jl/pull/153#issuecomment-837687064

/ Gray{Bool} Gray{N0f8} Gray24 Gray{Float32}
Bool Gray{Float32} Gray{Float32}:up: Gray{Float32}:up: Gray{Float32}
N0f8 Gray{Float32}:up: Gray{N0f8} Gray24 Gray{Float32}
Int64 Gray{Float32} Gray{Float32} Gray{Float32}:up: Gray{Float32}
Float32 Gray{Float32} Gray{Float32} Gray{Float32}:up: Gray{Float32}
Float64 Gray{Float64} Gray{Float64} Gray{Float64}:up: Gray{Float64}

There are several other cases, but the current tests for ColorVectorSpace are not so systematic to begin with.

Current Gray{Bool}/Bool = Gray{Float64} is consistent with Bool/Bool = Float64

You're right, but I think it's also non-intuitive to suddenly promote to Gray{Float64} when manipulating binary grayscale images. I think it is natural for Bool to behave in the same way as UInt8 in the most cases.

kimikage avatar May 11 '21 10:05 kimikage

Incidentally, my motivation for considering the return types and intermediate representations is to bring consistency to the new definition of one.

However, the more essential issue is related to the relaxation from RGB{T<:Fractional} to RGB{T<:Real}. When not only RGB{Bool} but also RGB{Int} and others become "valid" types, it is important to manage the rules so that unwanted promotions do not occur.

It is debatable what kind of rules are most preferable, but it is definitely beneficial to generalize the codes as much as possible and consolidate the rules in one place.

kimikage avatar May 11 '21 14:05 kimikage