Przeglądaj źródła

AdjustSaturation

J Delaney 7 lat temu
rodzic
commit
eca413ec14
4 zmienionych plików z 405 dodań i 0 usunięć
  1. 28 0
      adjust.go
  2. 120 0
      adjust_test.go
  3. 119 0
      utils.go
  4. 138 0
      utils_test.go

+ 28 - 0
adjust.go

@@ -49,6 +49,34 @@ func Invert(img image.Image) *image.NRGBA {
 	return dst
 }
 
+// AdjustSaturation changes the saturation of the image using the percentage parameter and returns the adjusted image.
+// The percentage must be in the range (-100, 100).
+// The percentage = 0 gives the original image.
+// The percentage = 100 gives the image with the saturation maxed for each pixel.
+// The percentage = -100 gives the image with the saturation value zeroed for each pixel (grayscale).
+//
+// Examples:
+//  dstImage = imaging.AdjustSaturation(srcImage, 25) // increase image saturation by 25%
+//  dstImage = imaging.AdjustSaturation(srcImage, -10) // decrease image saturation by 10%
+//
+func AdjustSaturation(img image.Image, percentage float64) *image.NRGBA {
+	percentage = math.Min(math.Max(percentage, -100), 100)
+	multiplier := percentage / 100
+
+	return AdjustFunc(img, func(c color.NRGBA) color.NRGBA {
+		h, s, l := nrgbaToHSL(c)
+		if multiplier > 0 {
+			s += (1 - s) * multiplier
+		} else {
+			s += s * multiplier
+		}
+
+		newColor := hslToNRGBA(h, s, l)
+		newColor.A = c.A
+		return newColor
+	})
+}
+
 // 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.

+ 120 - 0
adjust_test.go

@@ -669,3 +669,123 @@ func TestAdjustFunc(t *testing.T) {
 		})
 	}
 }
+
+func TestAdjustSaturation(t *testing.T) {
+	src := &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,
+		},
+	}
+
+	testCases := []struct {
+		name string
+		src  image.Image
+		p    float64
+		want *image.NRGBA
+	}{
+		{
+			"AdjustSaturation 3x3 10",
+			src,
+			10,
+			&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,
+					0x0f, 0x21, 0x34, 0xff, 0x34, 0x21, 0x0f, 0xff, 0xad, 0x2d, 0xc0, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x38, 0x2d, 0x2d, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustSaturation 3x3 50",
+			src,
+			50,
+			&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,
+					0x08, 0x21, 0x3b, 0xff, 0x3b, 0x21, 0x08, 0xff, 0xbd, 0x19, 0xd4, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x4c, 0x19, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustSaturation 3x3 100",
+			src,
+			100,
+			&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,
+					0x00, 0x21, 0x44, 0xff, 0x44, 0x21, 0x00, 0xff, 0xd0, 0x00, 0xee, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x66, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustSaturation 3x3 -10",
+			src,
+			-10,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0xc1, 0x0a, 0x0a, 0x01, 0x0a, 0xc1, 0x0a, 0x02, 0x0a, 0x0a, 0xc1, 0x03,
+					0x12, 0x21, 0x31, 0xff, 0x31, 0x21, 0x12, 0xff, 0xa4, 0x39, 0xb4, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustSaturation 3x3 -50",
+			src,
+			-50,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x99, 0x32, 0x32, 0x01, 0x32, 0x99, 0x32, 0x02, 0x32, 0x32, 0x99, 0x03,
+					0x19, 0x21, 0x2a, 0xff, 0x2a, 0x22, 0x19, 0xff, 0x90, 0x55, 0x99, 0xff,
+					0x00, 0x00, 0x00, 0xff, 0x33, 0x33, 0x33, 0xff, 0xff, 0xff, 0xff, 0xff,
+				},
+			},
+		},
+		{
+			"AdjustSaturation 3x3 -100",
+			src,
+			-100,
+			&image.NRGBA{
+				Rect:   image.Rect(0, 0, 3, 3),
+				Stride: 3 * 4,
+				Pix: []uint8{
+					0x66, 0x66, 0x66, 0x01, 0x66, 0x66, 0x66, 0x02, 0x66, 0x66, 0x66, 0x03,
+					0x22, 0x22, 0x22, 0xff, 0x22, 0x22, 0x22, 0xff, 0x77, 0x77, 0x77, 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 := AdjustSaturation(tc.src, tc.p)
+			if !compareNRGBA(got, tc.want, 0) {
+				t.Fatalf("got result %#v want %#v", got, tc.want)
+			}
+		})
+	}
+}
+
+func BenchmarkAdjustSaturation(b *testing.B) {
+	b.ReportAllocs()
+	for i := 0; i < b.N; i++ {
+		AdjustSaturation(testdataBranchesJPG, 50)
+	}
+}

+ 119 - 0
utils.go

@@ -4,6 +4,8 @@ import (
 	"image"
 	"runtime"
 	"sync"
+	"math"
+	"image/color"
 )
 
 // parallel processes the data in separate goroutines.
@@ -81,3 +83,120 @@ func toNRGBA(img image.Image) *image.NRGBA {
 	}
 	return Clone(img)
 }
+
+// nrgbaToHSL converts NRGBA to HSL.
+func nrgbaToHSL(c color.NRGBA) (float64, float64, float64) {
+	var h, s, l float64
+
+	r := float64(c.R) / float64(255)
+	g := float64(c.G) / float64(255)
+	b := float64(c.B) / float64(255)
+
+	min := math.Min(math.Min(r, g), b)
+	max := math.Max(math.Max(r, g), b)
+
+	l = (max + min) / 2
+
+	if min == max {
+		s = 0
+		h = 0
+	} else {
+		if l < 0.5 {
+			s = (max - min) / (max + min)
+		} else {
+			s = (max - min) / (2.0 - max - min)
+		}
+
+		if max == r {
+			h = (g - b) / (max - min)
+		} else if max == g {
+			h = 2.0 + (b-r)/(max-min)
+		} else {
+			h = 4.0 + (r-g)/(max-min)
+		}
+
+		h *= 60
+
+		if h < 0 {
+			h += 360
+		}
+	}
+
+	return h, s, l
+}
+
+// hslToNRGBA converts HSL to NRGBA with A=1.
+func hslToNRGBA(h, s, l float64) color.NRGBA {
+	if s == 0 {
+		c := uint8(l * 255)
+		return color.NRGBA{R: c, G: c, B: c, A: 1}
+	}
+
+	var r, g, b float64
+	var t1, t2, tr, tg, tb float64
+
+	if l < 0.5 {
+		t1 = l * (1.0 + s)
+	} else {
+		t1 = l + s - l*s
+	}
+
+	t2 = 2*l - t1
+	h = h / 360
+	tr = h + 1.0/3.0
+	tg = h
+	tb = h - 1.0/3.0
+
+	if tr < 0 {
+		tr++
+	} else if tr > 1 {
+		tr--
+	}
+
+	if tg < 0 {
+		tg++
+	} else if tg > 1 {
+		tg--
+	}
+
+	if tb < 0 {
+		tb++
+	} else if tb > 1 {
+		tb--
+	}
+
+	// Red
+	if 6*tr < 1 {
+		r = t2 + (t1-t2)*6*tr
+	} else if 2*tr < 1 {
+		r = t1
+	} else if 3*tr < 2 {
+		r = t2 + (t1-t2)*(2.0/3.0-tr)*6
+	} else {
+		r = t2
+	}
+
+	// Green
+	if 6*tg < 1 {
+		g = t2 + (t1-t2)*6*tg
+	} else if 2*tg < 1 {
+		g = t1
+	} else if 3*tg < 2 {
+		g = t2 + (t1-t2)*(2.0/3.0-tg)*6
+	} else {
+		g = t2
+	}
+
+	// Blue
+	if 6*tb < 1 {
+		b = t2 + (t1-t2)*6*tb
+	} else if 2*tb < 1 {
+		b = t1
+	} else if 3*tb < 2 {
+		b = t2 + (t1-t2)*(2.0/3.0-tb)*6
+	} else {
+		b = t2
+	}
+
+	return color.NRGBA{R: uint8(r * 255), G: uint8(g * 255), B: uint8(b * 255)}
+}

+ 138 - 0
utils_test.go

@@ -2,6 +2,8 @@ package imaging
 
 import (
 	"image"
+	"image/color"
+	"math"
 	"runtime"
 	"testing"
 )
@@ -124,3 +126,139 @@ func compareBytes(a, b []uint8, delta int) bool {
 	}
 	return true
 }
+
+func compareFloat64(a, b, delta float64) bool {
+	return math.Abs(a-b) <= delta
+}
+
+func compareUint8(a, b, delta uint8) bool {
+	if a > b {
+		return a - b <= delta
+	} else {
+		return b - a <= delta
+	}
+}
+
+var nrgbaHslTestCases = []struct {
+	rgb     color.NRGBA
+	h, s, l float64
+}{
+	{
+		rgb: color.NRGBA{R: 255, G: 0, B: 0},
+		h:   0,
+		s:   1,
+		l:   0.5,
+	},
+	{
+		rgb: color.NRGBA{R: 191, G: 191, B: 0},
+		h:   60,
+		s:   1,
+		l:   0.375,
+	},
+	{
+		rgb: color.NRGBA{R: 0, G: 128, B: 0},
+		h:   120,
+		s:   1,
+		l:   0.25,
+	},
+	{
+		rgb: color.NRGBA{R: 128, G: 255, B: 255},
+		h:   180,
+		s:   1,
+		l:   0.75,
+	},
+	{
+		rgb: color.NRGBA{R: 128, G: 128, B: 255},
+		h:   240,
+		s:   1,
+		l:   0.75,
+	},
+	{
+		rgb: color.NRGBA{R: 191, G: 64, B: 191},
+		h:   300,
+		s:   0.5,
+		l:   0.5,
+	},
+	{
+		rgb: color.NRGBA{R: 160, G: 164, B: 36},
+		h:   61.8,
+		s:   0.638,
+		l:   0.393,
+	},
+	{
+		rgb: color.NRGBA{R: 65, G: 27, B: 234},
+		h:   251.1,
+		s:   0.832,
+		l:   0.511,
+	},
+	{
+		rgb: color.NRGBA{R: 30, G: 172, B: 65},
+		h:   134.9,
+		s:   0.707,
+		l:   0.396,
+	},
+	{
+		rgb: color.NRGBA{R: 240, G: 200, B: 14},
+		h:   49.5,
+		s:   0.893,
+		l:   0.497,
+	},
+	{
+		rgb: color.NRGBA{R: 180, G: 48, B: 229},
+		h:   283.7,
+		s:   0.775,
+		l:   0.542,
+	},
+	{
+		rgb: color.NRGBA{R: 237, G: 118, B: 81},
+		h:   14.3,
+		s:   0.817,
+		l:   0.624,
+	},
+	{
+		rgb: color.NRGBA{R: 254, G: 248, B: 136},
+		h:   56.9,
+		s:   0.991,
+		l:   0.765,
+	},
+	{
+		rgb: color.NRGBA{R: 25, G: 203, B: 151},
+		h:   162.4,
+		s:   0.779,
+		l:   0.447,
+	},
+	{
+		rgb: color.NRGBA{R: 54, G: 38, B: 152},
+		h:   248.3,
+		s:   0.601,
+		l:   0.373,
+	},
+	{
+		rgb: color.NRGBA{R: 126, G: 126, B: 184},
+		h:   240.5,
+		s:   0.29,
+		l:   0.607,
+	},
+}
+
+func TestNrgbaToHSL(t *testing.T) {
+	for _, tc := range nrgbaHslTestCases {
+		t.Run("", func(t *testing.T) {
+			h, s, l := nrgbaToHSL(tc.rgb)
+			if !compareFloat64(h, tc.h, 1) || !compareFloat64(s, tc.s, 1) || !compareFloat64(l, tc.l, 1) {
+				t.Fatalf("with %v, expected (%.2f, %.2f, %.2f) but got (%.2f, %.2f, %.2f)", tc.rgb, h, s, l, tc.h, tc.s, tc.l)
+			}
+		})
+	}
+}
+
+func TestHslToNRGBA(t *testing.T) {
+	for _, tc := range nrgbaHslTestCases {
+		t.Run("", func(t *testing.T) {
+			rgb := hslToNRGBA(tc.h, tc.s, tc.l)
+			if !compareUint8(rgb.R, tc.rgb.R, 1) || !compareUint8(rgb.G, tc.rgb.G, 1) || !compareUint8(rgb.B, tc.rgb.B, 1) {
+				t.Fatalf("expected %+v but got %+v", tc.rgb, rgb)
+			}
+		})
+	}
+}