Merge pull request #130 from StruffelProductions/add-hue-adjustment
Add function "AdjustHue"
diff --git a/README.md b/README.md
index eecdb3d..4963add 100644
--- a/README.md
+++ b/README.md
@@ -129,6 +129,16 @@
-----------------------------------|----------------------------------------------|---------------------------------------------
![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_saturation_p30.png) | ![dstImage](testdata/out_saturation_m30.png)
+### Hue adjustment
+
+```go
+dstImage := imaging.AdjustHue(srcImage, 20)
+```
+
+Original image | Hue = 60 | Hue = -60
+-----------------------------------|----------------------------------------------|---------------------------------------------
+![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_hue_p60.png) | ![dstImage](testdata/out_hue_m60.png)
+
## FAQ
### Incorrect image orientation after processing (e.g. an image appears rotated after resizing)
diff --git a/adjust.go b/adjust.go
index 03ec97f..971aebc 100644
--- a/adjust.go
+++ b/adjust.go
@@ -80,6 +80,34 @@
})
}
+// AdjustHue changes the hue of the image using the shift parameter (measured in degrees) and returns the adjusted image.
+// The shift = 0 (or 360 / -360 / etc.) gives the original image.
+// The shift = 180 (or -180) corresponds to a 180° degree rotation of the color wheel and thus gives the image with its hue inverted for each pixel.
+//
+// Examples:
+// dstImage = imaging.AdjustHue(srcImage, 90) // Shift Hue by 90°.
+// dstImage = imaging.AdjustHue(srcImage, -30) // Shift Hue by -30°.
+//
+func AdjustHue(img image.Image, shift float64) *image.NRGBA {
+ if math.Mod(shift, 360) == 0 {
+ return Clone(img)
+ }
+
+ summand := shift / 360
+
+ return AdjustFunc(img, func(c color.NRGBA) color.NRGBA {
+ h, s, l := rgbToHSL(c.R, c.G, c.B)
+ h += summand
+ h = math.Mod(h, 1)
+ //Adding 1 because Golang's Modulo function behaves differently to similar operators in most other languages.
+ if h < 0 {
+ h++
+ }
+ r, g, b := hslToRGB(h, s, l)
+ return color.NRGBA{r, g, b, c.A}
+ })
+}
+
// AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
// The percentage = -100 gives solid gray image.
diff --git a/adjust_test.go b/adjust_test.go
index 49a7cc6..c928676 100644
--- a/adjust_test.go
+++ b/adjust_test.go
@@ -247,6 +247,293 @@
}
}
+func TestAdjustHue(t *testing.T) {
+ testCases := []struct {
+ name string
+ src image.Image
+ p float64
+ want *image.NRGBA
+ }{
+ {
+ "AdjustHue 3x3 -540",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ -540,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
+ 0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ {
+ "AdjustHue 3x3 -360",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ -360,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ {
+ "AdjustHue 3x3 -350",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ -350,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x22, 0x00, 0x01, 0x00, 0xcc, 0x22, 0x02, 0x22, 0x00, 0xcc, 0x03,
+ 0x11, 0x1c, 0x33, 0xff, 0x33, 0x28, 0x11, 0xff, 0xbb, 0x33, 0xb5, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ {
+ "AdjustHue 3x3 -180",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ -180,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
+ 0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ {
+ "AdjustHue 3x3 -10",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ -10,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x22, 0x01, 0x22, 0xcc, 0x00, 0x02, 0x00, 0x22, 0xcc, 0x03,
+ 0x11, 0x28, 0x33, 0xff, 0x33, 0x1c, 0x11, 0xff, 0x93, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ {
+ "AdjustHue 3x3 0",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ 0,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ {
+ "AdjustHue 3x3 10",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ 10,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x22, 0x00, 0x01, 0x00, 0xcc, 0x22, 0x02, 0x22, 0x00, 0xcc, 0x03,
+ 0x11, 0x1c, 0x33, 0xff, 0x33, 0x28, 0x11, 0xff, 0xbb, 0x33, 0xb5, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ {
+ "AdjustHue 3x3 180",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ 180,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
+ 0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ {
+ "AdjustHue 3x3 350",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ 350,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x22, 0x01, 0x22, 0xcc, 0x00, 0x02, 0x00, 0x22, 0xcc, 0x03,
+ 0x11, 0x28, 0x33, 0xff, 0x33, 0x1c, 0x11, 0xff, 0x93, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ {
+ "AdjustHue 3x3 360",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ 360,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ {
+ "AdjustHue 3x3 540",
+ &image.NRGBA{
+ Rect: image.Rect(-1, -1, 2, 2),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
+ 0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ 540,
+ &image.NRGBA{
+ Rect: image.Rect(0, 0, 3, 3),
+ Stride: 3 * 4,
+ Pix: []uint8{
+ 0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
+ 0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
+ 0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+ },
+ },
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ got := AdjustHue(tc.src, tc.p)
+ if !compareNRGBA(got, tc.want, 0) {
+ t.Fatalf("got result %#v want %#v", got, tc.want)
+ }
+ })
+ }
+}
+
+func TestAdjustHueGolden(t *testing.T) {
+ for name, p := range map[string]float64{
+ "out_hue_m480.png": -480,
+ "out_hue_m120.png": -120,
+ "out_hue_m60.png": -60,
+ "out_hue_p60.png": 60,
+ "out_hue_p120.png": 120,
+ "out_hue_p480.png": 480,
+ } {
+ got := AdjustHue(testdataFlowersSmallPNG, p)
+ want, err := Open("testdata/" + name)
+ if err != nil {
+ t.Fatalf("failed to open image: %v", err)
+ }
+ if !compareNRGBAGolden(got, toNRGBA(want)) {
+ t.Errorf("resulting image differs from golden: %s", name)
+ }
+ }
+}
+
+func BenchmarkAdjustHue(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ AdjustHue(testdataBranchesJPG, 10)
+ }
+}
+
func TestAdjustContrast(t *testing.T) {
testCases := []struct {
name string
diff --git a/testdata/out_hue_m120.png b/testdata/out_hue_m120.png
new file mode 100644
index 0000000..32b2b37
--- /dev/null
+++ b/testdata/out_hue_m120.png
Binary files differ
diff --git a/testdata/out_hue_m480.png b/testdata/out_hue_m480.png
new file mode 100644
index 0000000..32b2b37
--- /dev/null
+++ b/testdata/out_hue_m480.png
Binary files differ
diff --git a/testdata/out_hue_m60.png b/testdata/out_hue_m60.png
new file mode 100644
index 0000000..da28824
--- /dev/null
+++ b/testdata/out_hue_m60.png
Binary files differ
diff --git a/testdata/out_hue_p120.png b/testdata/out_hue_p120.png
new file mode 100644
index 0000000..28d4c55
--- /dev/null
+++ b/testdata/out_hue_p120.png
Binary files differ
diff --git a/testdata/out_hue_p480.png b/testdata/out_hue_p480.png
new file mode 100644
index 0000000..28d4c55
--- /dev/null
+++ b/testdata/out_hue_p480.png
Binary files differ
diff --git a/testdata/out_hue_p60.png b/testdata/out_hue_p60.png
new file mode 100644
index 0000000..44c62e8
--- /dev/null
+++ b/testdata/out_hue_p60.png
Binary files differ