Skip to content

Commit d40f48c

Browse files
Merge pull request #130 from StruffelProductions/add-hue-adjustment
Add function "AdjustHue"
2 parents 9821411 + a6f4d64 commit d40f48c

File tree

9 files changed

+325
-0
lines changed

9 files changed

+325
-0
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ Original image | Saturation = 30
129129
-----------------------------------|----------------------------------------------|---------------------------------------------
130130
![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_saturation_p30.png) | ![dstImage](testdata/out_saturation_m30.png)
131131

132+
### Hue adjustment
133+
134+
```go
135+
dstImage := imaging.AdjustHue(srcImage, 20)
136+
```
137+
138+
Original image | Hue = 60 | Hue = -60
139+
-----------------------------------|----------------------------------------------|---------------------------------------------
140+
![srcImage](testdata/flowers_small.png) | ![dstImage](testdata/out_hue_p60.png) | ![dstImage](testdata/out_hue_m60.png)
141+
132142
## FAQ
133143

134144
### Incorrect image orientation after processing (e.g. an image appears rotated after resizing)

adjust.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,34 @@ func AdjustSaturation(img image.Image, percentage float64) *image.NRGBA {
8080
})
8181
}
8282

83+
// AdjustHue changes the hue of the image using the shift parameter (measured in degrees) and returns the adjusted image.
84+
// The shift = 0 (or 360 / -360 / etc.) gives the original image.
85+
// 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.
86+
//
87+
// Examples:
88+
// dstImage = imaging.AdjustHue(srcImage, 90) // Shift Hue by 90°.
89+
// dstImage = imaging.AdjustHue(srcImage, -30) // Shift Hue by -30°.
90+
//
91+
func AdjustHue(img image.Image, shift float64) *image.NRGBA {
92+
if math.Mod(shift, 360) == 0 {
93+
return Clone(img)
94+
}
95+
96+
summand := shift / 360
97+
98+
return AdjustFunc(img, func(c color.NRGBA) color.NRGBA {
99+
h, s, l := rgbToHSL(c.R, c.G, c.B)
100+
h += summand
101+
h = math.Mod(h, 1)
102+
//Adding 1 because Golang's Modulo function behaves differently to similar operators in most other languages.
103+
if h < 0 {
104+
h++
105+
}
106+
r, g, b := hslToRGB(h, s, l)
107+
return color.NRGBA{r, g, b, c.A}
108+
})
109+
}
110+
83111
// AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
84112
// The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
85113
// The percentage = -100 gives solid gray image.

adjust_test.go

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,293 @@ func BenchmarkAdjustSaturation(b *testing.B) {
247247
}
248248
}
249249

250+
func TestAdjustHue(t *testing.T) {
251+
testCases := []struct {
252+
name string
253+
src image.Image
254+
p float64
255+
want *image.NRGBA
256+
}{
257+
{
258+
"AdjustHue 3x3 -540",
259+
&image.NRGBA{
260+
Rect: image.Rect(-1, -1, 2, 2),
261+
Stride: 3 * 4,
262+
Pix: []uint8{
263+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
264+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
265+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
266+
},
267+
},
268+
-540,
269+
&image.NRGBA{
270+
Rect: image.Rect(0, 0, 3, 3),
271+
Stride: 3 * 4,
272+
Pix: []uint8{
273+
0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
274+
0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
275+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
276+
},
277+
},
278+
},
279+
{
280+
"AdjustHue 3x3 -360",
281+
&image.NRGBA{
282+
Rect: image.Rect(-1, -1, 2, 2),
283+
Stride: 3 * 4,
284+
Pix: []uint8{
285+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
286+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
287+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
288+
},
289+
},
290+
-360,
291+
&image.NRGBA{
292+
Rect: image.Rect(0, 0, 3, 3),
293+
Stride: 3 * 4,
294+
Pix: []uint8{
295+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
296+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
297+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
298+
},
299+
},
300+
},
301+
{
302+
"AdjustHue 3x3 -350",
303+
&image.NRGBA{
304+
Rect: image.Rect(-1, -1, 2, 2),
305+
Stride: 3 * 4,
306+
Pix: []uint8{
307+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
308+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
309+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
310+
},
311+
},
312+
-350,
313+
&image.NRGBA{
314+
Rect: image.Rect(0, 0, 3, 3),
315+
Stride: 3 * 4,
316+
Pix: []uint8{
317+
0xcc, 0x22, 0x00, 0x01, 0x00, 0xcc, 0x22, 0x02, 0x22, 0x00, 0xcc, 0x03,
318+
0x11, 0x1c, 0x33, 0xff, 0x33, 0x28, 0x11, 0xff, 0xbb, 0x33, 0xb5, 0xff,
319+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
320+
},
321+
},
322+
},
323+
{
324+
"AdjustHue 3x3 -180",
325+
&image.NRGBA{
326+
Rect: image.Rect(-1, -1, 2, 2),
327+
Stride: 3 * 4,
328+
Pix: []uint8{
329+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
330+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
331+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
332+
},
333+
},
334+
-180,
335+
&image.NRGBA{
336+
Rect: image.Rect(0, 0, 3, 3),
337+
Stride: 3 * 4,
338+
Pix: []uint8{
339+
0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
340+
0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
341+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
342+
},
343+
},
344+
},
345+
{
346+
"AdjustHue 3x3 -10",
347+
&image.NRGBA{
348+
Rect: image.Rect(-1, -1, 2, 2),
349+
Stride: 3 * 4,
350+
Pix: []uint8{
351+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
352+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
353+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
354+
},
355+
},
356+
-10,
357+
&image.NRGBA{
358+
Rect: image.Rect(0, 0, 3, 3),
359+
Stride: 3 * 4,
360+
Pix: []uint8{
361+
0xcc, 0x00, 0x22, 0x01, 0x22, 0xcc, 0x00, 0x02, 0x00, 0x22, 0xcc, 0x03,
362+
0x11, 0x28, 0x33, 0xff, 0x33, 0x1c, 0x11, 0xff, 0x93, 0x33, 0xbb, 0xff,
363+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
364+
},
365+
},
366+
},
367+
{
368+
"AdjustHue 3x3 0",
369+
&image.NRGBA{
370+
Rect: image.Rect(-1, -1, 2, 2),
371+
Stride: 3 * 4,
372+
Pix: []uint8{
373+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
374+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
375+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
376+
},
377+
},
378+
0,
379+
&image.NRGBA{
380+
Rect: image.Rect(0, 0, 3, 3),
381+
Stride: 3 * 4,
382+
Pix: []uint8{
383+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
384+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
385+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
386+
},
387+
},
388+
},
389+
{
390+
"AdjustHue 3x3 10",
391+
&image.NRGBA{
392+
Rect: image.Rect(-1, -1, 2, 2),
393+
Stride: 3 * 4,
394+
Pix: []uint8{
395+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
396+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
397+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
398+
},
399+
},
400+
10,
401+
&image.NRGBA{
402+
Rect: image.Rect(0, 0, 3, 3),
403+
Stride: 3 * 4,
404+
Pix: []uint8{
405+
0xcc, 0x22, 0x00, 0x01, 0x00, 0xcc, 0x22, 0x02, 0x22, 0x00, 0xcc, 0x03,
406+
0x11, 0x1c, 0x33, 0xff, 0x33, 0x28, 0x11, 0xff, 0xbb, 0x33, 0xb5, 0xff,
407+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
408+
},
409+
},
410+
},
411+
{
412+
"AdjustHue 3x3 180",
413+
&image.NRGBA{
414+
Rect: image.Rect(-1, -1, 2, 2),
415+
Stride: 3 * 4,
416+
Pix: []uint8{
417+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
418+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
419+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
420+
},
421+
},
422+
180,
423+
&image.NRGBA{
424+
Rect: image.Rect(0, 0, 3, 3),
425+
Stride: 3 * 4,
426+
Pix: []uint8{
427+
0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
428+
0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
429+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
430+
},
431+
},
432+
},
433+
{
434+
"AdjustHue 3x3 350",
435+
&image.NRGBA{
436+
Rect: image.Rect(-1, -1, 2, 2),
437+
Stride: 3 * 4,
438+
Pix: []uint8{
439+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
440+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
441+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
442+
},
443+
},
444+
350,
445+
&image.NRGBA{
446+
Rect: image.Rect(0, 0, 3, 3),
447+
Stride: 3 * 4,
448+
Pix: []uint8{
449+
0xcc, 0x00, 0x22, 0x01, 0x22, 0xcc, 0x00, 0x02, 0x00, 0x22, 0xcc, 0x03,
450+
0x11, 0x28, 0x33, 0xff, 0x33, 0x1c, 0x11, 0xff, 0x93, 0x33, 0xbb, 0xff,
451+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
452+
},
453+
},
454+
},
455+
{
456+
"AdjustHue 3x3 360",
457+
&image.NRGBA{
458+
Rect: image.Rect(-1, -1, 2, 2),
459+
Stride: 3 * 4,
460+
Pix: []uint8{
461+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
462+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
463+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
464+
},
465+
},
466+
360,
467+
&image.NRGBA{
468+
Rect: image.Rect(0, 0, 3, 3),
469+
Stride: 3 * 4,
470+
Pix: []uint8{
471+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
472+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
473+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
474+
},
475+
},
476+
},
477+
{
478+
"AdjustHue 3x3 540",
479+
&image.NRGBA{
480+
Rect: image.Rect(-1, -1, 2, 2),
481+
Stride: 3 * 4,
482+
Pix: []uint8{
483+
0xcc, 0x00, 0x00, 0x01, 0x00, 0xcc, 0x00, 0x02, 0x00, 0x00, 0xcc, 0x03,
484+
0x11, 0x22, 0x33, 0xff, 0x33, 0x22, 0x11, 0xff, 0xaa, 0x33, 0xbb, 0xff,
485+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
486+
},
487+
},
488+
540,
489+
&image.NRGBA{
490+
Rect: image.Rect(0, 0, 3, 3),
491+
Stride: 3 * 4,
492+
Pix: []uint8{
493+
0x00, 0xcc, 0xcc, 0x01, 0xcc, 0x00, 0xcc, 0x02, 0xcc, 0xcc, 0x00, 0x03,
494+
0x33, 0x22, 0x11, 0xff, 0x11, 0x22, 0x33, 0xff, 0x44, 0xbb, 0x33, 0xff,
495+
0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
496+
},
497+
},
498+
},
499+
}
500+
for _, tc := range testCases {
501+
t.Run(tc.name, func(t *testing.T) {
502+
got := AdjustHue(tc.src, tc.p)
503+
if !compareNRGBA(got, tc.want, 0) {
504+
t.Fatalf("got result %#v want %#v", got, tc.want)
505+
}
506+
})
507+
}
508+
}
509+
510+
func TestAdjustHueGolden(t *testing.T) {
511+
for name, p := range map[string]float64{
512+
"out_hue_m480.png": -480,
513+
"out_hue_m120.png": -120,
514+
"out_hue_m60.png": -60,
515+
"out_hue_p60.png": 60,
516+
"out_hue_p120.png": 120,
517+
"out_hue_p480.png": 480,
518+
} {
519+
got := AdjustHue(testdataFlowersSmallPNG, p)
520+
want, err := Open("testdata/" + name)
521+
if err != nil {
522+
t.Fatalf("failed to open image: %v", err)
523+
}
524+
if !compareNRGBAGolden(got, toNRGBA(want)) {
525+
t.Errorf("resulting image differs from golden: %s", name)
526+
}
527+
}
528+
}
529+
530+
func BenchmarkAdjustHue(b *testing.B) {
531+
b.ReportAllocs()
532+
for i := 0; i < b.N; i++ {
533+
AdjustHue(testdataBranchesJPG, 10)
534+
}
535+
}
536+
250537
func TestAdjustContrast(t *testing.T) {
251538
testCases := []struct {
252539
name string

testdata/out_hue_m120.png

55.8 KB
Loading

testdata/out_hue_m480.png

55.8 KB
Loading

testdata/out_hue_m60.png

56.7 KB
Loading

testdata/out_hue_p120.png

55.8 KB
Loading

testdata/out_hue_p480.png

55.8 KB
Loading

testdata/out_hue_p60.png

56.5 KB
Loading

0 commit comments

Comments
 (0)