diff --git a/.github/README.md b/.github/README.md index 450bcc5..2aa8fba 100644 --- a/.github/README.md +++ b/.github/README.md @@ -245,6 +245,8 @@ Full Fiber example is available at https://github.com/peterdee/filtering-backend - `BRILLE_JPEG_QUALITY` (`int`) - controls output quality for JPEG images, should be a number from 0 (low quality) to 100 (highest quality). Highest quality is used by default. +- `BRILLE_THREADS` (`int`) - controls the number of threads used when performing image processing. By default `runtime.NumCPU()` value is used, and provided number should be less or equal to that value. + ### Contributing Please check [contributing rules](CONTRIBUTING.md). diff --git a/filters/binary.go b/filters/binary.go index 982538d..81917ba 100644 --- a/filters/binary.go +++ b/filters/binary.go @@ -2,6 +2,7 @@ package filters import ( "io" + "sync" "github.com/julyskies/brille/v2/utilities" ) @@ -11,13 +12,27 @@ func Binary(file io.Reader, threshold uint8) (io.Reader, string, error) { if convertationError != nil { return nil, "", convertationError } - for i := 0; i < len(img.Pix); i += 4 { - average := uint8((int(img.Pix[i]) + int(img.Pix[i+1]) + int(img.Pix[i+2])) / 3) - channel := uint8(255) - if average < threshold { - channel = 0 + pixLen := len(img.Pix) + threads := utilities.GetThreads() + pixPerThread := utilities.GetPixPerThread(pixLen, threads) + var wg sync.WaitGroup + processing := func(thread int) { + defer wg.Done() + startIndex := pixPerThread * thread + endIndex := utilities.ClampMax(startIndex+pixPerThread, pixLen) + for i := startIndex; i < endIndex; i += 4 { + average := uint8((int(img.Pix[i]) + int(img.Pix[i+1]) + int(img.Pix[i+2])) / 3) + channel := uint8(255) + if average < threshold { + channel = 0 + } + img.Pix[i], img.Pix[i+1], img.Pix[i+2] = channel, channel, channel } - img.Pix[i], img.Pix[i+1], img.Pix[i+2] = channel, channel, channel } + for t := 0; t < threads; t += 1 { + wg.Add(1) + go processing(t) + } + wg.Wait() return utilities.EncodeResult(img, format) } diff --git a/filters/box-blur.go b/filters/box-blur.go index 1a16f23..8d4cc77 100644 --- a/filters/box-blur.go +++ b/filters/box-blur.go @@ -2,6 +2,7 @@ package filters import ( "io" + "sync" "github.com/julyskies/brille/v2/utilities" ) @@ -13,23 +14,41 @@ func BoxBlur(file io.Reader, radius uint) (io.Reader, string, error) { } radiusInt := int(radius) width, height := img.Rect.Max.X, img.Rect.Max.Y - for i := 0; i < len(img.Pix); i += 4 { - x, y := utilities.GetCoordinates(i/4, width) - sumR, sumG, sumB, pixelCount := 0, 0, 0, 0 - x2s, x2e := utilities.GetAperture(x, width, -radiusInt, radiusInt) - y2s, y2e := utilities.GetAperture(y, height, -radiusInt, radiusInt) - for x2 := x2s; x2 < x2e; x2 += 1 { - for y2 := y2s; y2 < y2e; y2 += 1 { - px := utilities.GetPixel(x2, y2, width) - sumR += int(img.Pix[px]) - sumG += int(img.Pix[px+1]) - sumB += int(img.Pix[px+2]) - pixelCount += 1 + pixLen := len(img.Pix) + threads := utilities.GetThreads() + pixPerThread := utilities.GetPixPerThread(pixLen, threads) + result := make([]uint8, pixLen) + var wg sync.WaitGroup + processing := func(thread int) { + defer wg.Done() + startIndex := pixPerThread * thread + endIndex := utilities.ClampMax(startIndex+pixPerThread, pixLen) + for i := startIndex; i < endIndex; i += 4 { + x, y := utilities.GetCoordinates(i/4, width) + dR, dG, dB := 0, 0, 0 + pixelCount := 0 + x2s, x2e := utilities.GetAperture(x, width, -radiusInt, radiusInt) + y2s, y2e := utilities.GetAperture(y, height, -radiusInt, radiusInt) + for x2 := x2s; x2 < x2e; x2 += 1 { + for y2 := y2s; y2 < y2e; y2 += 1 { + px := utilities.GetPixel(x2, y2, width) + dR += int(img.Pix[px]) + dG += int(img.Pix[px+1]) + dB += int(img.Pix[px+2]) + pixelCount += 1 + } } + result[i] = uint8(dR / pixelCount) + result[i+1] = uint8(dG / pixelCount) + result[i+2] = uint8(dB / pixelCount) + result[i+3] = img.Pix[i+3] } - img.Pix[i] = uint8(sumR / pixelCount) - img.Pix[i+1] = uint8(sumG / pixelCount) - img.Pix[i+2] = uint8(sumB / pixelCount) } + for t := 0; t < threads; t += 1 { + wg.Add(1) + go processing(t) + } + wg.Wait() + img.Pix = result return utilities.EncodeResult(img, format) } diff --git a/filters/brightness.go b/filters/brightness.go index 38f8e22..ed51d93 100644 --- a/filters/brightness.go +++ b/filters/brightness.go @@ -2,6 +2,7 @@ package filters import ( "io" + "sync" "github.com/julyskies/brille/v2/utilities" ) @@ -11,11 +12,25 @@ func Brightness(file io.Reader, amount int) (io.Reader, string, error) { if convertationError != nil { return nil, "", convertationError } - amount = utilities.MaxMin(amount, 255, -255) - for i := 0; i < len(img.Pix); i += 4 { - img.Pix[i] = uint8(utilities.MaxMin(int(img.Pix[i])+amount, 255, 0)) - img.Pix[i+1] = uint8(utilities.MaxMin(int(img.Pix[i+1])+amount, 255, 0)) - img.Pix[i+2] = uint8(utilities.MaxMin(int(img.Pix[i+2])+amount, 255, 0)) + amount = utilities.Clamp(amount, 255, -255) + pixLen := len(img.Pix) + threads := utilities.GetThreads() + pixPerThread := utilities.GetPixPerThread(pixLen, threads) + var wg sync.WaitGroup + processing := func(thread int) { + defer wg.Done() + startIndex := pixPerThread * thread + endIndex := utilities.ClampMax(startIndex+pixPerThread, pixLen) + for i := startIndex; i < endIndex; i += 4 { + img.Pix[i] = uint8(utilities.Clamp(int(img.Pix[i])+amount, 255, 0)) + img.Pix[i+1] = uint8(utilities.Clamp(int(img.Pix[i+1])+amount, 255, 0)) + img.Pix[i+2] = uint8(utilities.Clamp(int(img.Pix[i+2])+amount, 255, 0)) + } } + for t := 0; t < threads; t += 1 { + wg.Add(1) + go processing(t) + } + wg.Wait() return utilities.EncodeResult(img, format) } diff --git a/filters/contrast.go b/filters/contrast.go index f12dc92..674af50 100644 --- a/filters/contrast.go +++ b/filters/contrast.go @@ -2,6 +2,7 @@ package filters import ( "io" + "sync" "github.com/julyskies/brille/v2/utilities" ) @@ -11,12 +12,26 @@ func Contrast(file io.Reader, amount int) (io.Reader, string, error) { if convertationError != nil { return nil, "", convertationError } - amount = utilities.MaxMin(amount, 255, -255) + amount = utilities.Clamp(amount, 255, -255) factor := float64(259*(amount+255)) / float64(255*(259-amount)) - for i := 0; i < len(img.Pix); i += 4 { - img.Pix[i] = uint8(utilities.MaxMin(factor*(float64(img.Pix[i])-128)+128, 255, 0)) - img.Pix[i+1] = uint8(utilities.MaxMin(factor*(float64(img.Pix[i+1])-128)+128, 255, 0)) - img.Pix[i+2] = uint8(utilities.MaxMin(factor*(float64(img.Pix[i+2])-128)+128, 255, 0)) + pixLen := len(img.Pix) + threads := utilities.GetThreads() + pixPerThread := utilities.GetPixPerThread(pixLen, threads) + var wg sync.WaitGroup + processing := func(thread int) { + defer wg.Done() + startIndex := pixPerThread * thread + endIndex := utilities.ClampMax(startIndex+pixPerThread, pixLen) + for i := startIndex; i < endIndex; i += 4 { + img.Pix[i] = uint8(utilities.Clamp(factor*(float64(img.Pix[i])-128)+128, 255, 0)) + img.Pix[i+1] = uint8(utilities.Clamp(factor*(float64(img.Pix[i+1])-128)+128, 255, 0)) + img.Pix[i+2] = uint8(utilities.Clamp(factor*(float64(img.Pix[i+2])-128)+128, 255, 0)) + } } + for t := 0; t < threads; t += 1 { + wg.Add(1) + go processing(t) + } + wg.Wait() return utilities.EncodeResult(img, format) } diff --git a/filters/eight-colors.go b/filters/eight-colors.go index fd7137d..cc1e692 100644 --- a/filters/eight-colors.go +++ b/filters/eight-colors.go @@ -2,6 +2,7 @@ package filters import ( "io" + "sync" "github.com/julyskies/brille/v2/utilities" ) @@ -26,23 +27,37 @@ func EightColors(file io.Reader) (io.Reader, string, error) { if convertationError != nil { return nil, "", convertationError } - for i := 0; i < len(img.Pix); i += 4 { - minDelta := 195076 - var selectedColor Color - for j := range COLORS { - indexColor := COLORS[j] - rDifference := int(img.Pix[i]) - indexColor.R - gDifference := int(img.Pix[i+1]) - indexColor.G - bDifference := int(img.Pix[i+2]) - indexColor.B - delta := rDifference*rDifference + gDifference*gDifference + bDifference*bDifference - if delta < minDelta { - minDelta = delta - selectedColor = indexColor + pixLen := len(img.Pix) + threads := utilities.GetThreads() + pixPerThread := utilities.GetPixPerThread(pixLen, threads) + var wg sync.WaitGroup + processing := func(thread int) { + defer wg.Done() + startIndex := pixPerThread * thread + endIndex := utilities.ClampMax(startIndex+pixPerThread, pixLen) + for i := startIndex; i < endIndex; i += 4 { + minDelta := 195076 + var selectedColor Color + for j := range COLORS { + indexColor := COLORS[j] + rDifference := int(img.Pix[i]) - indexColor.R + gDifference := int(img.Pix[i+1]) - indexColor.G + bDifference := int(img.Pix[i+2]) - indexColor.B + delta := rDifference*rDifference + gDifference*gDifference + bDifference*bDifference + if delta < minDelta { + minDelta = delta + selectedColor = indexColor + } } + img.Pix[i] = uint8(selectedColor.R) + img.Pix[i+1] = uint8(selectedColor.G) + img.Pix[i+2] = uint8(selectedColor.B) } - img.Pix[i] = uint8(selectedColor.R) - img.Pix[i+1] = uint8(selectedColor.G) - img.Pix[i+2] = uint8(selectedColor.B) } + for t := 0; t < threads; t += 1 { + wg.Add(1) + go processing(t) + } + wg.Wait() return utilities.EncodeResult(img, format) } diff --git a/filters/emboss.go b/filters/emboss.go index 69555b0..29ae9d6 100644 --- a/filters/emboss.go +++ b/filters/emboss.go @@ -3,6 +3,7 @@ package filters import ( "io" "math" + "sync" "github.com/julyskies/brille/v2/utilities" ) @@ -25,27 +26,43 @@ func Emboss(file io.Reader) (io.Reader, string, error) { return nil, "", convertationError } width, height := img.Rect.Max.X, img.Rect.Max.Y - for i := 0; i < len(img.Pix); i += 4 { - x, y := utilities.GetCoordinates(i/4, width) - gradientX, gradientY := 0, 0 - for m := 0; m < 3; m += 1 { - for n := 0; n < 3; n += 1 { - k := utilities.GradientPoint(x, m, width) - l := utilities.GradientPoint(y, n, height) - px := utilities.GetPixel(x+k, y+l, width) - average := (int(img.Pix[px]) + int(img.Pix[px+1]) + int(img.Pix[px+2])) / 3 - gradientX += average * embossHorizontal[m][n] - gradientY += average * embossVertical[m][n] + pixLen := len(img.Pix) + result := make([]uint8, pixLen) + threads := utilities.GetThreads() + pixPerThread := utilities.GetPixPerThread(pixLen, threads) + var wg sync.WaitGroup + processing := func(thread int) { + defer wg.Done() + startIndex := pixPerThread * thread + endIndex := utilities.ClampMax(startIndex+pixPerThread, pixLen) + for i := startIndex; i < endIndex; i += 4 { + x, y := utilities.GetCoordinates(i/4, width) + gradientX := 0 + gradientY := 0 + for m := 0; m < 3; m += 1 { + for n := 0; n < 3; n += 1 { + k := utilities.GradientPoint(x, m, width) + l := utilities.GradientPoint(y, n, height) + px := utilities.GetPixel(x+k, y+l, width) + average := (int(img.Pix[px]) + int(img.Pix[px+1]) + int(img.Pix[px+2])) / 3 + gradientX += average * embossHorizontal[m][n] + gradientY += average * embossVertical[m][n] + } } + channel := uint8( + 255 - utilities.ClampMax( + math.Sqrt(float64(gradientX*gradientX+gradientY*gradientY)), + 255, + ), + ) + result[i], result[i+1], result[i+2], result[i+3] = channel, channel, channel, img.Pix[i+3] } - colorCode := uint8( - 255 - utilities.MaxMin( - math.Sqrt(float64(gradientX*gradientX+gradientY*gradientY)), - 255, - 0, - ), - ) - img.Pix[i], img.Pix[i+1], img.Pix[i+2] = colorCode, colorCode, colorCode } + for t := 0; t < threads; t += 1 { + wg.Add(1) + go processing(t) + } + wg.Wait() + img.Pix = result return utilities.EncodeResult(img, format) } diff --git a/filters/flip.go b/filters/flip.go index 94aae63..5dc18fd 100644 --- a/filters/flip.go +++ b/filters/flip.go @@ -2,6 +2,7 @@ package filters import ( "io" + "sync" "github.com/julyskies/brille/v2/constants" "github.com/julyskies/brille/v2/utilities" @@ -24,23 +25,37 @@ func Flip(file io.Reader, direction string) (io.Reader, string, error) { if height%2 != 0 { heightCorrection = 1 } - for i := 0; i < len(img.Pix); i += 4 { - x, y := utilities.GetCoordinates(i/4, width) - var j int - skip := true - if direction == constants.FLIP_DIRECTION_HORIZONTAL && x < width/2+widthCorrection { - j = utilities.GetPixel(width-x-1, y, width) - skip = false - } - if direction == constants.FLIP_DIRECTION_VERTICAL && y < height/2+heightCorrection { - j = utilities.GetPixel(x, height-y-1, width) - skip = false - } - if !skip { - r, g, b := img.Pix[i], img.Pix[i+1], img.Pix[i+2] - img.Pix[i], img.Pix[i+1], img.Pix[i+2] = img.Pix[j], img.Pix[j+1], img.Pix[j+2] - img.Pix[j], img.Pix[j+1], img.Pix[j+2] = r, g, b + pixLen := len(img.Pix) + threads := utilities.GetThreads() + pixPerThread := utilities.GetPixPerThread(pixLen, threads) + var wg sync.WaitGroup + processing := func(thread int) { + defer wg.Done() + startIndex := pixPerThread * thread + endIndex := utilities.ClampMax(startIndex+pixPerThread, pixLen) + for i := startIndex; i < endIndex; i += 4 { + x, y := utilities.GetCoordinates(i/4, width) + var j int + skip := true + if direction == constants.FLIP_DIRECTION_HORIZONTAL && x < width/2+widthCorrection { + j = utilities.GetPixel(width-x-1, y, width) + skip = false + } + if direction == constants.FLIP_DIRECTION_VERTICAL && y < height/2+heightCorrection { + j = utilities.GetPixel(x, height-y-1, width) + skip = false + } + if !skip { + r, g, b := img.Pix[i], img.Pix[i+1], img.Pix[i+2] + img.Pix[i], img.Pix[i+1], img.Pix[i+2] = img.Pix[j], img.Pix[j+1], img.Pix[j+2] + img.Pix[j], img.Pix[j+1], img.Pix[j+2] = r, g, b + } } } + for t := 0; t < threads; t += 1 { + wg.Add(1) + go processing(t) + } + wg.Wait() return utilities.EncodeResult(img, format) } diff --git a/filters/gamma-correction.go b/filters/gamma-correction.go index 11a161f..9a51299 100644 --- a/filters/gamma-correction.go +++ b/filters/gamma-correction.go @@ -3,6 +3,7 @@ package filters import ( "io" "math" + "sync" "github.com/julyskies/brille/v2/utilities" ) @@ -12,12 +13,26 @@ func GammaCorrection(file io.Reader, amount float64) (io.Reader, string, error) if convertationError != nil { return nil, "", convertationError } - amount = utilities.MaxMin(amount, 3.99, 0) + amount = utilities.Clamp(amount, 3.99, 0) power := 1 / amount - for i := 0; i < len(img.Pix); i += 4 { - img.Pix[i] = uint8(255 * math.Pow(float64(img.Pix[i])/255, power)) - img.Pix[i+1] = uint8(255 * math.Pow(float64(img.Pix[i+1])/255, power)) - img.Pix[i+2] = uint8(255 * math.Pow(float64(img.Pix[i+2])/255, power)) + pixLen := len(img.Pix) + threads := utilities.GetThreads() + pixPerThread := utilities.GetPixPerThread(pixLen, threads) + var wg sync.WaitGroup + processing := func(thread int) { + defer wg.Done() + startIndex := pixPerThread * thread + endIndex := utilities.ClampMax(startIndex+pixPerThread, pixLen) + for i := startIndex; i < endIndex; i += 4 { + img.Pix[i] = uint8(255 * math.Pow(float64(img.Pix[i])/255, power)) + img.Pix[i+1] = uint8(255 * math.Pow(float64(img.Pix[i+1])/255, power)) + img.Pix[i+2] = uint8(255 * math.Pow(float64(img.Pix[i+2])/255, power)) + } } + for t := 0; t < threads; t += 1 { + wg.Add(1) + go processing(t) + } + wg.Wait() return utilities.EncodeResult(img, format) } diff --git a/index.go b/index.go index 75ec660..7e380ec 100644 --- a/index.go +++ b/index.go @@ -121,7 +121,7 @@ func Kuwahara(file io.Reader, radius uint) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - radius = utilities.MaxMin(radius, 40, 0) + radius = utilities.Clamp(radius, 40, 0) return filters.Kuwahara(file, radius) } diff --git a/utilities/encode-result.go b/utilities/encode-result.go index dbaa976..ec3d462 100644 --- a/utilities/encode-result.go +++ b/utilities/encode-result.go @@ -18,7 +18,7 @@ func getJPEGQuality() int { if jpegQualityENV != "" { parsed, parsingError := strconv.Atoi(jpegQualityENV) if parsingError == nil { - jpegQuality = MaxMin(parsed, 100, 0) + jpegQuality = Clamp(parsed, 100, 0) } } return jpegQuality diff --git a/utilities/get-threads.go b/utilities/get-threads.go index 03444b4..a946bcd 100644 --- a/utilities/get-threads.go +++ b/utilities/get-threads.go @@ -14,7 +14,7 @@ func GetThreads() int { if threadsNumberENV != "" { parsed, parsingError := strconv.Atoi(threadsNumberENV) if parsingError == nil { - threadsNumber = MaxMin(parsed, threadsNumber, 1) + threadsNumber = Clamp(parsed, threadsNumber, 1) } } return threadsNumber diff --git a/utilities/max-min.go b/utilities/max-min.go deleted file mode 100644 index 690ec51..0000000 --- a/utilities/max-min.go +++ /dev/null @@ -1,11 +0,0 @@ -package utilities - -func MaxMin[T float64 | int | uint](value, max, min T) T { - if value < min { - return min - } - if value > max { - return max - } - return value -}