Skip to content

Commit

Permalink
When averaging, ensure achromatic hues are undefined (#356)
Browse files Browse the repository at this point in the history
* When averaging, ensure achromatic hues are undefined

* Fix hue clipping and add test

* Updates to test and documentation
  • Loading branch information
facelessuser authored Aug 23, 2023
1 parent 508f1ee commit 2410d77
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 7 deletions.
6 changes: 5 additions & 1 deletion coloraide/average.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ def average(create: type[Color], colors: Iterable[ColorInput], space: str, premu
# Sum channel values
i = -1
for c in colors:
coords = obj.update(c)[:]
obj.update(c)
# If cylindrical color is achromatic, ensure hue is undefined
if hue_index >= 0 and not math.isnan(obj[hue_index]) and obj.is_achromatic():
obj[hue_index] = math.nan
coords = obj[:]
alpha = coords[-1]
if math.isnan(alpha):
alpha = 1.0
Expand Down
1 change: 1 addition & 0 deletions coloraide/gamut/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def clip_channels(color: Color, nans: bool = True) -> None:
# Wrap the angle. Not technically out of gamut, but we will clean it up.
if chan.flags & FLG_ANGLE:
color[i] = util.constrain_hue(value)
continue

# Ignore undefined or unbounded channels
if not chan.bound or math.isnan(value):
Expand Down
2 changes: 2 additions & 0 deletions docs/src/markdown/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

- **NEW**: Add `HWBish` mixin class.
- **NEW**: Deprecate `algebra.no_nan()`, `algebra.no_nans()`, and `algebra.is_nan()`.
- **NEW**: When averaging in a cylindrical space, always treat achromatic hues as powerless for better results.
- **FIX**: Fix RLAB conversion.
- **FIX**: Fix clipping of hues.
- **ENHANCE**: Tweaks to some matrix calculations.
- **ENHANCE**: Various performance related tweaks.

Expand Down
37 changes: 31 additions & 6 deletions docs/src/markdown/average.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,24 @@ Color.average(['red', 'yellow', 'orange', 'green'])
ColorAide can average colors in rectangular spaces and cylindrical spaces. When applying averaging in a cylindrical
space, hues will be averaged taking the circular mean.

Cylindrical averaging may not provide as good of results as using rectangular spaces, but is provided to provide a sane
approach if a cylindrical space is used.

/// note | Achromatic Colors
When a color is considered achromatic, the hue will always be considered powerless, regardless of whether the color has
an explicit hue or not.
///

```py play
Color.average(['orange', 'yellow', 'red'], space='srgb')
Color.average(['orange', 'yellow', 'red'])
Color.average(['orange', 'yellow', 'red'], space='hsl')
```

Because calculations are done in a cylindrical space, the averaged colors can be different than what is acquired with
rectangular space averaging.

```py play
Color.average(['purple', 'green', 'blue'], space='srgb')
Color.average(['purple', 'green', 'blue'])
Color.average(['purple', 'green', 'blue'], space='hsl')
```

Expand All @@ -50,7 +58,7 @@ less of an impact on the average. This is done by premultiplying the colors befo
```py play
Steps([Color('darkgreen'), Color('color(srgb 0 0.50196 0 / 1)'), Color('color(srgb 0 0 1)')])
for i in range(12):
Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / {i / 11})', 'color(srgb 0 0 1)'], space='srgb')
Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / {i / 11})', 'color(srgb 0 0 1)'])
```

If you'd like to average the channels without taking transparency into consideration, simply set `premultiplied` to
Expand All @@ -59,15 +67,32 @@ If you'd like to average the channels without taking transparency into considera
```py play
Steps([Color('darkgreen'), Color('color(srgb 0 0.50196 0 / 1)'), Color('color(srgb 0 0 1)')])
for i in range(12):
Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / {i / 11})', 'color(srgb 0 0 1)'], space='srgb', premultiplied=False)
Color.average(['darkgreen', f'color(srgb 0 0.50196 0 / {i / 11})', 'color(srgb 0 0 1)'], premultiplied=False)
```

## Averaging with Undefined Values

When averaging with undefined values, ColorAide will not consider the undefined values in the average.
When averaging with undefined values, ColorAide will not consider the undefined values in the average. This is mainly
provided for averaging cylindrical colors, particularly achromatic colors.

```py play
Color.average(['white', 'color(srgb 0 0 1)'], space='hsl')
```

Implied achromatic hues are also considered undefined.


```py play
Color.average(['hsl(0 0 100)', 'hsl(240 100 50 / 1)'], space='hsl')
```

While undefined logic is intended to handle achromatic hues, this logic will be applied to any channel. It should be
noted that no attempt to carry forward the undefined values through conversion is made. Conversions will remove any
undefined status unless the channel is an achromatic hues.

```py play
for i in range(12):
Color.average(['darkgreen', f'color(srgb 0 none 0 / {i / 11})', 'color(srgb 0 0 1)'], space='srgb')
Color.average(['darkgreen', f'color(srgb 0 none 0 / {i / 11})', 'color(srgb 0 0 1)'])

```

Expand Down
8 changes: 8 additions & 0 deletions tests/test_average.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
class TestAverage(util.ColorAsserts, unittest.TestCase):
"""Test averaging."""

def test_average_achromatic(self):
"""Test that we force achromatic hues to undefined."""

self.assertEqual(
Color.average(['hsl(0 0 100)', 'color(srgb 0 0 1)'], space='hsl').to_string(),
'hsl(240 50% 75%)'
)

def test_no_colors(self):
"""Test averaging no colors."""

Expand Down
5 changes: 5 additions & 0 deletions tests/test_gamut.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
class TestGamut(util.ColorAsserts, unittest.TestCase):
"""Test gamut mapping/fitting."""

def test_hue_clipping(self):
"""Test hue clipping."""

self.assertEqual(Color('hsl(-120 50% 75% / 1)').clip().to_string(), 'hsl(240 50% 75%)')

def test_in_gamut(self):
"""Test in gamut check."""

Expand Down

0 comments on commit 2410d77

Please sign in to comment.