git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/imaging/adjust.go (about)

     1  package imaging
     2  
     3  import (
     4  	"image"
     5  	"image/color"
     6  	"math"
     7  )
     8  
     9  // Grayscale produces a grayscale version of the image.
    10  func Grayscale(img image.Image) *image.NRGBA {
    11  	src := newScanner(img)
    12  	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
    13  	parallel(0, src.h, func(ys <-chan int) {
    14  		for y := range ys {
    15  			i := y * dst.Stride
    16  			src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
    17  			for x := 0; x < src.w; x++ {
    18  				d := dst.Pix[i : i+3 : i+3]
    19  				r := d[0]
    20  				g := d[1]
    21  				b := d[2]
    22  				f := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
    23  				y := uint8(f + 0.5)
    24  				d[0] = y
    25  				d[1] = y
    26  				d[2] = y
    27  				i += 4
    28  			}
    29  		}
    30  	})
    31  	return dst
    32  }
    33  
    34  // Invert produces an inverted (negated) version of the image.
    35  func Invert(img image.Image) *image.NRGBA {
    36  	src := newScanner(img)
    37  	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
    38  	parallel(0, src.h, func(ys <-chan int) {
    39  		for y := range ys {
    40  			i := y * dst.Stride
    41  			src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
    42  			for x := 0; x < src.w; x++ {
    43  				d := dst.Pix[i : i+3 : i+3]
    44  				d[0] = 255 - d[0]
    45  				d[1] = 255 - d[1]
    46  				d[2] = 255 - d[2]
    47  				i += 4
    48  			}
    49  		}
    50  	})
    51  	return dst
    52  }
    53  
    54  // AdjustSaturation changes the saturation of the image using the percentage parameter and returns the adjusted image.
    55  // The percentage must be in the range (-100, 100).
    56  // The percentage = 0 gives the original image.
    57  // The percentage = 100 gives the image with the saturation value doubled for each pixel.
    58  // The percentage = -100 gives the image with the saturation value zeroed for each pixel (grayscale).
    59  //
    60  // Examples:
    61  //
    62  //	dstImage = imaging.AdjustSaturation(srcImage, 25) // Increase image saturation by 25%.
    63  //	dstImage = imaging.AdjustSaturation(srcImage, -10) // Decrease image saturation by 10%.
    64  func AdjustSaturation(img image.Image, percentage float64) *image.NRGBA {
    65  	if percentage == 0 {
    66  		return Clone(img)
    67  	}
    68  
    69  	percentage = math.Min(math.Max(percentage, -100), 100)
    70  	multiplier := 1 + percentage/100
    71  
    72  	return AdjustFunc(img, func(c color.NRGBA) color.NRGBA {
    73  		h, s, l := rgbToHSL(c.R, c.G, c.B)
    74  		s *= multiplier
    75  		if s > 1 {
    76  			s = 1
    77  		}
    78  		r, g, b := hslToRGB(h, s, l)
    79  		return color.NRGBA{r, g, b, c.A}
    80  	})
    81  }
    82  
    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  //
    89  //	dstImage = imaging.AdjustHue(srcImage, 90) // Shift Hue by 90°.
    90  //	dstImage = imaging.AdjustHue(srcImage, -30) // Shift Hue by -30°.
    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  
   111  // AdjustContrast changes the contrast of the image using the percentage parameter and returns the adjusted image.
   112  // The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
   113  // The percentage = -100 gives solid gray image.
   114  //
   115  // Examples:
   116  //
   117  //	dstImage = imaging.AdjustContrast(srcImage, -10) // Decrease image contrast by 10%.
   118  //	dstImage = imaging.AdjustContrast(srcImage, 20) // Increase image contrast by 20%.
   119  func AdjustContrast(img image.Image, percentage float64) *image.NRGBA {
   120  	if percentage == 0 {
   121  		return Clone(img)
   122  	}
   123  
   124  	percentage = math.Min(math.Max(percentage, -100.0), 100.0)
   125  	lut := make([]uint8, 256)
   126  
   127  	v := (100.0 + percentage) / 100.0
   128  	for i := 0; i < 256; i++ {
   129  		switch {
   130  		case 0 <= v && v <= 1:
   131  			lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*v) * 255.0)
   132  		case 1 < v && v < 2:
   133  			lut[i] = clamp((0.5 + (float64(i)/255.0-0.5)*(1/(2.0-v))) * 255.0)
   134  		default:
   135  			lut[i] = uint8(float64(i)/255.0+0.5) * 255
   136  		}
   137  	}
   138  
   139  	return adjustLUT(img, lut)
   140  }
   141  
   142  // AdjustBrightness changes the brightness of the image using the percentage parameter and returns the adjusted image.
   143  // The percentage must be in range (-100, 100). The percentage = 0 gives the original image.
   144  // The percentage = -100 gives solid black image. The percentage = 100 gives solid white image.
   145  //
   146  // Examples:
   147  //
   148  //	dstImage = imaging.AdjustBrightness(srcImage, -15) // Decrease image brightness by 15%.
   149  //	dstImage = imaging.AdjustBrightness(srcImage, 10) // Increase image brightness by 10%.
   150  func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA {
   151  	if percentage == 0 {
   152  		return Clone(img)
   153  	}
   154  
   155  	percentage = math.Min(math.Max(percentage, -100.0), 100.0)
   156  	lut := make([]uint8, 256)
   157  
   158  	shift := 255.0 * percentage / 100.0
   159  	for i := 0; i < 256; i++ {
   160  		lut[i] = clamp(float64(i) + shift)
   161  	}
   162  
   163  	return adjustLUT(img, lut)
   164  }
   165  
   166  // AdjustGamma performs a gamma correction on the image and returns the adjusted image.
   167  // Gamma parameter must be positive. Gamma = 1.0 gives the original image.
   168  // Gamma less than 1.0 darkens the image and gamma greater than 1.0 lightens it.
   169  //
   170  // Example:
   171  //
   172  //	dstImage = imaging.AdjustGamma(srcImage, 0.7)
   173  func AdjustGamma(img image.Image, gamma float64) *image.NRGBA {
   174  	if gamma == 1 {
   175  		return Clone(img)
   176  	}
   177  
   178  	e := 1.0 / math.Max(gamma, 0.0001)
   179  	lut := make([]uint8, 256)
   180  
   181  	for i := 0; i < 256; i++ {
   182  		lut[i] = clamp(math.Pow(float64(i)/255.0, e) * 255.0)
   183  	}
   184  
   185  	return adjustLUT(img, lut)
   186  }
   187  
   188  // AdjustSigmoid changes the contrast of the image using a sigmoidal function and returns the adjusted image.
   189  // It's a non-linear contrast change useful for photo adjustments as it preserves highlight and shadow detail.
   190  // The midpoint parameter is the midpoint of contrast that must be between 0 and 1, typically 0.5.
   191  // The factor parameter indicates how much to increase or decrease the contrast, typically in range (-10, 10).
   192  // If the factor parameter is positive the image contrast is increased otherwise the contrast is decreased.
   193  //
   194  // Examples:
   195  //
   196  //	dstImage = imaging.AdjustSigmoid(srcImage, 0.5, 3.0) // Increase the contrast.
   197  //	dstImage = imaging.AdjustSigmoid(srcImage, 0.5, -3.0) // Decrease the contrast.
   198  func AdjustSigmoid(img image.Image, midpoint, factor float64) *image.NRGBA {
   199  	if factor == 0 {
   200  		return Clone(img)
   201  	}
   202  
   203  	lut := make([]uint8, 256)
   204  	a := math.Min(math.Max(midpoint, 0.0), 1.0)
   205  	b := math.Abs(factor)
   206  	sig0 := sigmoid(a, b, 0)
   207  	sig1 := sigmoid(a, b, 1)
   208  	e := 1.0e-6
   209  
   210  	if factor > 0 {
   211  		for i := 0; i < 256; i++ {
   212  			x := float64(i) / 255.0
   213  			sigX := sigmoid(a, b, x)
   214  			f := (sigX - sig0) / (sig1 - sig0)
   215  			lut[i] = clamp(f * 255.0)
   216  		}
   217  	} else {
   218  		for i := 0; i < 256; i++ {
   219  			x := float64(i) / 255.0
   220  			arg := math.Min(math.Max((sig1-sig0)*x+sig0, e), 1.0-e)
   221  			f := a - math.Log(1.0/arg-1.0)/b
   222  			lut[i] = clamp(f * 255.0)
   223  		}
   224  	}
   225  
   226  	return adjustLUT(img, lut)
   227  }
   228  
   229  func sigmoid(a, b, x float64) float64 {
   230  	return 1 / (1 + math.Exp(b*(a-x)))
   231  }
   232  
   233  // adjustLUT applies the given lookup table to the colors of the image.
   234  func adjustLUT(img image.Image, lut []uint8) *image.NRGBA {
   235  	src := newScanner(img)
   236  	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
   237  	lut = lut[0:256]
   238  	parallel(0, src.h, func(ys <-chan int) {
   239  		for y := range ys {
   240  			i := y * dst.Stride
   241  			src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
   242  			for x := 0; x < src.w; x++ {
   243  				d := dst.Pix[i : i+3 : i+3]
   244  				d[0] = lut[d[0]]
   245  				d[1] = lut[d[1]]
   246  				d[2] = lut[d[2]]
   247  				i += 4
   248  			}
   249  		}
   250  	})
   251  	return dst
   252  }
   253  
   254  // AdjustFunc applies the fn function to each pixel of the img image and returns the adjusted image.
   255  //
   256  // Example:
   257  //
   258  //	dstImage = imaging.AdjustFunc(
   259  //		srcImage,
   260  //		func(c color.NRGBA) color.NRGBA {
   261  //			// Shift the red channel by 16.
   262  //			r := int(c.R) + 16
   263  //			if r > 255 {
   264  //				r = 255
   265  //			}
   266  //			return color.NRGBA{uint8(r), c.G, c.B, c.A}
   267  //		}
   268  //	)
   269  func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGBA {
   270  	src := newScanner(img)
   271  	dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h))
   272  	parallel(0, src.h, func(ys <-chan int) {
   273  		for y := range ys {
   274  			i := y * dst.Stride
   275  			src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4])
   276  			for x := 0; x < src.w; x++ {
   277  				d := dst.Pix[i : i+4 : i+4]
   278  				r := d[0]
   279  				g := d[1]
   280  				b := d[2]
   281  				a := d[3]
   282  				c := fn(color.NRGBA{r, g, b, a})
   283  				d[0] = c.R
   284  				d[1] = c.G
   285  				d[2] = c.B
   286  				d[3] = c.A
   287  				i += 4
   288  			}
   289  		}
   290  	})
   291  	return dst
   292  }