提交 6e4f39b1 编写于 作者: A Anthony N. Simon 提交者: GitHub

Add hue and saturation (#16)

* Add convert RGB to and from HSV with tests

* Add saturation func

* Add hue function

* Rename Saturation param

* Fix Hue and Saturation function mappings

* Add lightness func

* Add RGBToHSL func

* Add HSL to RGB

* Refactor variable names

* Fix saturation return value

* Add doc

* Add HSL tests

* Add doc

* Add saturation and hue tests

* Add hue and saturation to README
上级 e54a4d4c
......@@ -77,6 +77,17 @@ func main() {
![example](https://anthonynsimon.github.io/projects/bild/gamma.jpg)
### Hue
result := adjust.Hue(img, -42)
![example](https://anthonynsimon.github.io/projects/bild/hue.jpg)
### Saturation
result := adjust.Saturation(img, 0.5)
![example](https://anthonynsimon.github.io/projects/bild/saturation.jpg)
## Blend modes
import "github.com/anthonynsimon/bild/blend"
......
......@@ -7,6 +7,7 @@ import (
"math"
"github.com/anthonynsimon/bild/math/f64"
"github.com/anthonynsimon/bild/util"
)
// Brightness returns a copy of the image with the adjusted brightness.
......@@ -64,3 +65,35 @@ func Contrast(src image.Image, change float64) *image.RGBA {
return img
}
// Hue adjusts the overall hue of the provided image and returns the result.
// Parameter change is the amount of change to be applied and is of the range
// -360 to 360. It corresponds to the hue angle in the HSL color model.
func Hue(img image.Image, change int) *image.RGBA {
fn := func(c color.RGBA) color.RGBA {
h, s, l := util.RGBToHSL(c)
h = float64((int(h) + change) % 360)
outColor := util.HSLToRGB(h, s, l)
outColor.A = c.A
return outColor
}
return Apply(img, fn)
}
// Saturation adjusts the saturation of the image and returns the result.
// Parameter change is the amount of change to be applied and is of the range
// -1.0 to 1.0. It's applied as relative change. For example if the current color
// saturation is 1.0 and the saturation change is set to -0.5, a change of -50%
// will be applied so that the resulting saturation is 0.5 in the HSL color model.
func Saturation(img image.Image, change float64) *image.RGBA {
fn := func(c color.RGBA) color.RGBA {
h, s, l := util.RGBToHSL(c)
s = f64.Clamp(s*(1+change), 0.0, 1.0)
outColor := util.HSLToRGB(h, s, l)
outColor.A = c.A
return outColor
}
return Apply(img, fn)
}
......@@ -317,3 +317,225 @@ func TestContrast(t *testing.T) {
}
}
}
func TestSaturation(t *testing.T) {
cases := []struct {
change float64
value image.Image
expected *image.RGBA
}{
{
change: 0.0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
},
{
change: 1.0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
},
{
change: -1.0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
},
{
change: 0.0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x00, 0x00, 0xFF, 0x00, 0x80, 0x00, 0xFF,
0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x00, 0x00, 0xFF, 0x00, 0x80, 0x00, 0xFF,
0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xFF,
},
},
},
{
change: 1.0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x7A, 0x44, 0x44, 0xFF, 0x44, 0x7A, 0x44, 0xFF,
0x44, 0x44, 0x7A, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x95, 0x29, 0x29, 0xFF, 0x29, 0x95, 0x29, 0xFF,
0x29, 0x29, 0x95, 0xFF, 0x00, 0x00, 0x00, 0xFF,
},
},
},
{
change: -1.0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x00, 0x00, 0xFF, 0x00, 0x80, 0x00, 0xFF,
0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0x00, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x40, 0x40, 0x40, 0xFF, 0x40, 0x40, 0x40, 0xFF,
0x40, 0x40, 0x40, 0xFF, 0x00, 0x00, 0x00, 0xFF,
},
},
},
}
for _, c := range cases {
actual := Saturation(c.value, c.change)
if !util.RGBAImageEqual(actual, c.expected) {
t.Errorf("%s:\nexpected: %v\nactual: %v", "Saturation", util.RGBAToString(c.expected), util.RGBAToString(actual))
}
}
}
func TestHue(t *testing.T) {
cases := []struct {
change int
value image.Image
expected *image.RGBA
}{
{
change: 0,
value: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
},
{
change: 360,
value: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
},
{
change: 40,
value: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x55, 0x0, 0x80, 0xFF, 0xAA, 0x0, 0xFF,
0x0, 0x55, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
},
{
change: -67,
value: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x80, 0x00, 0x00, 0x80, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0xFF, 0xFF, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
expected: &image.RGBA{
Rect: image.Rect(0, 0, 2, 2),
Stride: 8,
Pix: []uint8{
0x71, 0x0, 0x80, 0x80, 0xE1, 0x0, 0xFF, 0xFF,
0x1E, 0xFF, 0x0, 0xFF, 0x0, 0x0, 0x0, 0xFF,
},
},
},
}
for _, c := range cases {
actual := Hue(c.value, c.change)
if !util.RGBAImageEqual(actual, c.expected) {
t.Errorf("%s:\nexpected: %v\nactual: %v", "Hue", util.RGBAToString(c.expected), util.RGBAToString(actual))
}
}
}
package util
import (
"image/color"
"math"
"github.com/anthonynsimon/bild/math/f64"
)
// RGBToHSL converts from RGB to HSL color model.
// Parameter c is the RGBA color and must implement the color.RGBA interface.
// Returned values h, s and l correspond to the hue, saturation and lightness.
// The hue is of range 0 to 360 and the saturation and lightness are of range 0.0 to 1.0.
func RGBToHSL(c color.RGBA) (float64, float64, float64) {
r, g, b := float64(c.R)/255, float64(c.G)/255, float64(c.B)/255
max := math.Max(r, math.Max(g, b))
min := math.Min(r, math.Min(g, b))
delta := max - min
var h, s, l float64
l = (max + min) / 2
// Achromatic
if delta <= 0 {
return h, s, l
}
// Should it be smaller than or equals instead?
if l < 0.5 {
s = delta / (max + min)
} else {
s = delta / (2 - max - min)
}
if r >= max {
h = (g - b) / delta
} else if g >= max {
h = (b-r)/delta + 2
} else {
h = (r-g)/delta + 4
}
h *= 60
if h < 0 {
h += 360
}
return h, s, l
}
// HSLToRGB converts from HSL to RGB color model.
// Parameter h is the hue and its range is from 0 to 360 degrees.
// Parameter s is the saturation and its range is from 0.0 to 1.0.
// Parameter l is the lightness and its range is from 0.0 to 1.0.
func HSLToRGB(h, s, l float64) color.RGBA {
var r, g, b float64
if s == 0 {
r = l
g = l
b = l
} else {
var temp0, temp1 float64
if l < 0.5 {
temp0 = l * (1 + s)
} else {
temp0 = (l + s) - (s * l)
}
temp1 = 2*l - temp0
h /= 360
hueFn := func(v float64) float64 {
if v < 0 {
v++
} else if v > 1 {
v--
}
if v < 1.0/6.0 {
return temp1 + (temp0-temp1)*6*v
}
if v < 1.0/2.0 {
return temp0
}
if v < 2.0/3.0 {
return temp1 + (temp0-temp1)*(2.0/3.0-v)*6
}
return temp1
}
r = hueFn(h + 1.0/3.0)
g = hueFn(h)
b = hueFn(h - 1.0/3.0)
}
outR := uint8(f64.Clamp(r*255+0.5, 0, 255))
outG := uint8(f64.Clamp(g*255+0.5, 0, 255))
outB := uint8(f64.Clamp(b*255+0.5, 0, 255))
return color.RGBA{outR, outG, outB, 0xFF}
}
// RGBToHSV converts from RGB to HSV color model.
// Parameter c is the RGBA color and must implement the color.RGBA interface.
// Returned values h, s and v correspond to the hue, saturation and value.
// The hue is of range 0 to 360 and the saturation and value are of range 0.0 to 1.0.
func RGBToHSV(c color.RGBA) (h, s, v float64) {
r, g, b := float64(c.R)/255, float64(c.G)/255, float64(c.B)/255
max := math.Max(r, math.Max(g, b))
min := math.Min(r, math.Min(g, b))
v = max
delta := max - min
// Avoid division by zero
if max > 0 {
s = delta / max
} else {
h = 0
s = 0
return
}
// Achromatic
if max == min {
h = 0
return
}
if r >= max {
h = (g - b) / delta
} else if g >= max {
h = (b-r)/delta + 2
} else {
h = (r-g)/delta + 4
}
h *= 60
if h < 0 {
h += 360
}
return
}
// HSVToRGB converts from HSV to RGB color model.
// Parameter h is the hue and its range is from 0 to 360 degrees.
// Parameter s is the saturation and its range is from 0.0 to 1.0.
// Parameter v is the value and its range is from 0.0 to 1.0.
func HSVToRGB(h, s, v float64) color.RGBA {
var i, f, p, q, t float64
// Achromatic
if s == 0 {
outV := uint8(f64.Clamp(v*255+0.5, 0, 255))
return color.RGBA{outV, outV, outV, 0xFF}
}
h /= 60
i = math.Floor(h)
f = h - i
p = v * (1 - s)
q = v * (1 - s*f)
t = v * (1 - s*(1-f))
var r, g, b float64
switch i {
case 0:
r = v
g = t
b = p
case 1:
r = q
g = v
b = p
case 2:
r = p
g = v
b = t
case 3:
r = p
g = q
b = v
case 4:
r = t
g = p
b = v
default:
r = v
g = p
b = q
}
outR := uint8(f64.Clamp(r*255+0.5, 0, 255))
outG := uint8(f64.Clamp(g*255+0.5, 0, 255))
outB := uint8(f64.Clamp(b*255+0.5, 0, 255))
return color.RGBA{outR, outG, outB, 0xFF}
}
package util
import (
"image/color"
"math"
"testing"
)
func TestRGBToHSV(t *testing.T) {
cases := []struct {
input color.RGBA
expected [3]float64
}{
{
input: color.RGBA{45, 166, 115, 255},
expected: [3]float64{155, 0.73, 0.65},
},
{
input: color.RGBA{0, 255, 0, 255},
expected: [3]float64{120, 1, 1},
},
{
input: color.RGBA{242, 220, 97, 255},
expected: [3]float64{51, 0.6, 0.95},
},
{
input: color.RGBA{10, 10, 10, 255},
expected: [3]float64{0, 0.0, 0.04},
},
{
input: color.RGBA{255, 255, 255, 255},
expected: [3]float64{0, 0.0, 1.0},
},
{
input: color.RGBA{0, 0, 0, 255},
expected: [3]float64{0, 0.0, 0.0},
},
{
input: color.RGBA{255, 0, 0, 255},
expected: [3]float64{0, 1.0, 1.0},
},
{
input: color.RGBA{255, 0, 255, 255},
expected: [3]float64{300, 1.0, 1.0},
},
}
for _, c := range cases {
h, s, v := RGBToHSV(c.input)
h = math.Floor(h + 0.5)
s = math.Floor((s*100)+0.5) / 100
v = math.Floor((v*100)+0.5) / 100
if h != c.expected[0] || s != c.expected[1] || v != c.expected[2] {
t.Errorf("RGBToHSV failed: expected: %#v, actual: %#v, %#v, %#v", c.expected, h, s, v)
}
}
}
func TestHSVToRGB(t *testing.T) {
cases := []struct {
input [3]float64
expected color.RGBA
}{
{
input: [3]float64{155, 0.73, 0.65},
expected: color.RGBA{45, 166, 115, 255},
},
{
input: [3]float64{120, 1, 1},
expected: color.RGBA{0, 255, 0, 255},
},
{
input: [3]float64{51, 0.6, 0.95},
expected: color.RGBA{242, 220, 97, 255},
},
{
input: [3]float64{0, 0.0, 0.04},
expected: color.RGBA{10, 10, 10, 255},
},
{
input: [3]float64{0, 0.0, 1.0},
expected: color.RGBA{255, 255, 255, 255},
},
{
input: [3]float64{0, 0.0, 0.0},
expected: color.RGBA{0, 0, 0, 255},
},
{
input: [3]float64{0, 1.0, 1.0},
expected: color.RGBA{255, 0, 0, 255},
},
{
input: [3]float64{300, 1.0, 1.0},
expected: color.RGBA{255, 0, 255, 255},
},
}
for _, c := range cases {
actual := HSVToRGB(c.input[0], c.input[1], c.input[2])
if actual != c.expected {
t.Errorf("HSVToRGB failed: expected: %#v, actual: %#v", c.expected, actual)
}
}
}
func TestRGBToHSL(t *testing.T) {
cases := []struct {
input color.RGBA
expected [3]float64
}{
{
input: color.RGBA{45, 166, 115, 255},
expected: [3]float64{155, 0.57, 0.41},
},
{
input: color.RGBA{0, 255, 0, 255},
expected: [3]float64{120, 1, 0.5},
},
{
input: color.RGBA{242, 220, 97, 255},
expected: [3]float64{51, 0.85, 0.66},
},
{
input: color.RGBA{10, 10, 10, 255},
expected: [3]float64{0, 0.0, 0.04},
},
{
input: color.RGBA{255, 255, 255, 255},
expected: [3]float64{0, 0.0, 1.0},
},
{
input: color.RGBA{0, 0, 0, 255},
expected: [3]float64{0, 0.0, 0.0},
},
{
input: color.RGBA{255, 0, 0, 255},
expected: [3]float64{0, 1.0, 0.5},
},
{
input: color.RGBA{0, 0, 255, 255},
expected: [3]float64{240, 1.0, 0.5},
},
{
input: color.RGBA{255, 0, 255, 255},
expected: [3]float64{300, 1.0, 0.5},
},
}
for _, c := range cases {
h, s, l := RGBToHSL(c.input)
h = math.Floor(h + 0.5)
s = math.Floor((s*100)+0.5) / 100
l = math.Floor((l*100)+0.5) / 100
if h != c.expected[0] || s != c.expected[1] || l != c.expected[2] {
t.Errorf("RGBToHSL failed: expected: %#v, actual: %#v, %#v, %#v", c.expected, h, s, l)
}
}
}
func TestHSLToRGB(t *testing.T) {
cases := []struct {
input [3]float64
expected color.RGBA
}{
{
input: [3]float64{155, 0.57, 0.41},
expected: color.RGBA{0x2d, 0xa4, 0x72, 0xff},
},
{
input: [3]float64{120, 1, 0.5},
expected: color.RGBA{0, 255, 0, 255},
},
{
input: [3]float64{51, 0.85, 0.66},
expected: color.RGBA{0xf2, 0xdc, 0x5f, 0xff},
},
{
input: [3]float64{0, 0.0, 0.04},
expected: color.RGBA{10, 10, 10, 255},
},
{
input: [3]float64{0, 0.0, 1.0},
expected: color.RGBA{255, 255, 255, 255},
},
{
input: [3]float64{0, 0.0, 0.0},
expected: color.RGBA{0, 0, 0, 255},
},
{
input: [3]float64{0, 1.0, 0.5},
expected: color.RGBA{255, 0, 0, 255},
},
{
input: [3]float64{240, 1.0, 0.5},
expected: color.RGBA{0, 0, 255, 255},
},
{
input: [3]float64{300, 1.0, 0.5},
expected: color.RGBA{255, 0, 255, 255},
},
}
for _, c := range cases {
actual := HSLToRGB(c.input[0], c.input[1], c.input[2])
if actual != c.expected {
t.Errorf("HSLToRGB failed: expected: %#v, actual: %#v", c.expected, actual)
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册