From c1c3679daab4e06490b582cf6dbceaa2b6eb3316 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 6 Feb 2023 20:16:40 +0300 Subject: [PATCH 1/5] Start working on filter optimization --- filters/binary.go | 23 +++++++++++++++++++++ index.go | 15 +++----------- processing/binary.go | 24 ---------------------- utilities/decode-source.go | 18 ++++++++++++++++ utilities/encode-result.go | 42 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 filters/binary.go delete mode 100644 processing/binary.go create mode 100644 utilities/decode-source.go create mode 100644 utilities/encode-result.go diff --git a/filters/binary.go b/filters/binary.go new file mode 100644 index 0000000..ea4123a --- /dev/null +++ b/filters/binary.go @@ -0,0 +1,23 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +func Binary(file io.Reader, threshold uint8) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + 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 + } + img.Pix[i], img.Pix[i+1], img.Pix[i+2] = channel, channel, channel + } + return utilities.EncodeResult(img, format) +} diff --git a/index.go b/index.go index ed921b6..604d6ed 100644 --- a/index.go +++ b/index.go @@ -5,6 +5,7 @@ import ( "io" "github.com/julyskies/brille/constants" + "github.com/julyskies/brille/filters" "github.com/julyskies/brille/processing" "github.com/julyskies/brille/utilities" ) @@ -14,21 +15,11 @@ const GRAYSCALE_AVERAGE string = constants.GRAYSCALE_AVERAGE const GRAYSCALE_LUMINOCITY string = constants.GRAYSCALE_LUMINOCITY // threshold: 0 to 255 -func Binary(file io.Reader, threshold uint) (io.Reader, string, error) { +func Binary(file io.Reader, threshold uint8) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - threshold = utilities.MaxMin(threshold, 255, 0) - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - binary := processing.Binary(source, threshold) - encoded, encodingError := utilities.PrepareResult(binary, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Binary(file, threshold) } // max amount: (min(width, height) / 2) diff --git a/processing/binary.go b/processing/binary.go deleted file mode 100644 index b6dec2d..0000000 --- a/processing/binary.go +++ /dev/null @@ -1,24 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/constants" - "github.com/julyskies/brille/utilities" -) - -func Binary(source [][]color.Color, threshold uint) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - grayColor, alpha := utilities.Gray(source[x][y], constants.GRAYSCALE_AVERAGE) - value := uint8(255) - if uint(grayColor) < threshold { - value = 0 - } - destination[x][y] = color.RGBA{value, value, value, alpha} - } - } - return destination -} diff --git a/utilities/decode-source.go b/utilities/decode-source.go new file mode 100644 index 0000000..b85f530 --- /dev/null +++ b/utilities/decode-source.go @@ -0,0 +1,18 @@ +package utilities + +import ( + "image" + "image/draw" + "io" +) + +func DecodeSource(file io.Reader) (*image.RGBA, string, error) { + content, format, decodingError := image.Decode(file) + if decodingError != nil { + return nil, "", decodingError + } + rect := content.Bounds() + img := image.NewRGBA(rect) + draw.Draw(img, img.Bounds(), content, rect.Min, draw.Src) + return img, format, nil +} diff --git a/utilities/encode-result.go b/utilities/encode-result.go new file mode 100644 index 0000000..8b64578 --- /dev/null +++ b/utilities/encode-result.go @@ -0,0 +1,42 @@ +package utilities + +import ( + "bytes" + "image" + "image/jpeg" + "image/png" + "io" + "os" + "strconv" +) + +func EncodeResult(img *image.RGBA, format string) (io.Reader, string, error) { + jpegQualityENV := os.Getenv("BRILLE_JPEG_QUALITY") + jpegQuality := 100 + if jpegQualityENV != "" { + parsed, parsingError := strconv.Atoi(jpegQualityENV) + if parsingError == nil { + jpegQuality = MaxMin(parsed, 100, 0) + } + } + var buffer bytes.Buffer + writer := io.Writer(&buffer) + if format == "png" { + encodingError := png.Encode(writer, img.SubImage(img.Rect)) + if encodingError != nil { + return nil, "", encodingError + } + } else { + encodingError := jpeg.Encode( + writer, + img.SubImage(img.Rect), + &jpeg.Options{ + Quality: jpegQuality, + }, + ) + if encodingError != nil { + return nil, "", encodingError + } + } + return bytes.NewReader(buffer.Bytes()), format, nil +} From d9a5bce326b811fb8685805aa5d2d9ee14a7ebd4 Mon Sep 17 00:00:00 2001 From: Peter Date: Tue, 7 Feb 2023 16:33:50 +0300 Subject: [PATCH 2/5] Work on optimization --- constants/index.go | 4 + filters/box-blur.go | 35 +++++++++ filters/brightness.go | 21 ++++++ filters/color-inversion.go | 20 +++++ filters/contrast.go | 22 ++++++ filters/eight-colors.go | 48 ++++++++++++ filters/emboss.go | 51 +++++++++++++ filters/flip.go | 41 +++++++++++ filters/gamma-correction.go | 23 ++++++ index.go | 130 ++++++--------------------------- processing/box-blur.go | 53 -------------- processing/brightness.go | 28 ------- processing/color-inversion.go | 24 ------ processing/contrast.go | 29 -------- processing/eight-colors.go | 52 ------------- processing/emboss-filter.go | 49 ------------- processing/flip-horizontal.go | 23 ------ processing/flip-vertical.go | 23 ------ processing/gamma-correction.go | 24 ------ utilities/get-aperture.go | 12 +++ utilities/get-coordinates.go | 7 ++ utilities/get-pixel.go | 5 ++ 22 files changed, 313 insertions(+), 411 deletions(-) create mode 100644 filters/box-blur.go create mode 100644 filters/brightness.go create mode 100644 filters/color-inversion.go create mode 100644 filters/contrast.go create mode 100644 filters/eight-colors.go create mode 100644 filters/emboss.go create mode 100644 filters/flip.go create mode 100644 filters/gamma-correction.go delete mode 100644 processing/box-blur.go delete mode 100644 processing/brightness.go delete mode 100644 processing/color-inversion.go delete mode 100644 processing/contrast.go delete mode 100644 processing/eight-colors.go delete mode 100644 processing/emboss-filter.go delete mode 100644 processing/flip-horizontal.go delete mode 100644 processing/flip-vertical.go delete mode 100644 processing/gamma-correction.go create mode 100644 utilities/get-aperture.go create mode 100644 utilities/get-coordinates.go create mode 100644 utilities/get-pixel.go diff --git a/constants/index.go b/constants/index.go index ccbaebb..727cd86 100644 --- a/constants/index.go +++ b/constants/index.go @@ -4,6 +4,10 @@ const ERROR_INVALID_GRAYSCALE_TYPE string = "invalid grayscale type" const ERROR_NO_FILE_PROVIDED string = "no file provided" +const FLIP_DIRECTION_HORIZONTAL string = "horizontal" + +const FLIP_DIRECTION_VERTICAL string = "vertical" + const GRAYSCALE_AVERAGE string = "average" const GRAYSCALE_LUMINOCITY string = "luminocity" diff --git a/filters/box-blur.go b/filters/box-blur.go new file mode 100644 index 0000000..186f835 --- /dev/null +++ b/filters/box-blur.go @@ -0,0 +1,35 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +func BoxBlur(file io.Reader, radius uint) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + 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 + } + } + img.Pix[i] = uint8(sumR / pixelCount) + img.Pix[i+1] = uint8(sumG / pixelCount) + img.Pix[i+2] = uint8(sumB / pixelCount) + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/brightness.go b/filters/brightness.go new file mode 100644 index 0000000..4bd5272 --- /dev/null +++ b/filters/brightness.go @@ -0,0 +1,21 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +func Brightness(file io.Reader, amount int) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + 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)) + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/color-inversion.go b/filters/color-inversion.go new file mode 100644 index 0000000..95a3698 --- /dev/null +++ b/filters/color-inversion.go @@ -0,0 +1,20 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +func ColorInversion(file io.Reader) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + for i := 0; i < len(img.Pix); i += 4 { + img.Pix[i] = 255 - img.Pix[i] + img.Pix[i+1] = 255 - img.Pix[i+1] + img.Pix[i+2] = 255 - img.Pix[i+2] + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/contrast.go b/filters/contrast.go new file mode 100644 index 0000000..e2c9282 --- /dev/null +++ b/filters/contrast.go @@ -0,0 +1,22 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +func Contrast(file io.Reader, amount int) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + amount = utilities.MaxMin(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)) + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/eight-colors.go b/filters/eight-colors.go new file mode 100644 index 0000000..3a15af1 --- /dev/null +++ b/filters/eight-colors.go @@ -0,0 +1,48 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +type Color struct { + R, G, B int +} + +var COLORS = [8]Color{ + {255, 0, 0}, + {0, 255, 0}, + {0, 0, 255}, + {255, 255, 0}, + {255, 0, 255}, + {0, 255, 255}, + {255, 255, 255}, + {0, 0, 0}, +} + +func EightColors(file io.Reader) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + 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 + } + } + img.Pix[i] = uint8(selectedColor.R) + img.Pix[i+1] = uint8(selectedColor.G) + img.Pix[i+2] = uint8(selectedColor.B) + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/emboss.go b/filters/emboss.go new file mode 100644 index 0000000..8988fce --- /dev/null +++ b/filters/emboss.go @@ -0,0 +1,51 @@ +package filters + +import ( + "io" + "math" + + "github.com/julyskies/brille/utilities" +) + +var embossHorizontal = [3][3]int{ + {0, 0, 0}, + {1, 0, -1}, + {0, 0, 0}, +} + +var embossVertical = [3][3]int{ + {0, 1, 0}, + {0, 0, 0}, + {0, -1, 0}, +} + +func Emboss(file io.Reader) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + 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] + } + } + 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 + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/flip.go b/filters/flip.go new file mode 100644 index 0000000..6143721 --- /dev/null +++ b/filters/flip.go @@ -0,0 +1,41 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/constants" + "github.com/julyskies/brille/utilities" +) + +func Flip(file io.Reader, direction string) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + if direction != constants.FLIP_DIRECTION_HORIZONTAL && + direction != constants.FLIP_DIRECTION_VERTICAL { + direction = constants.FLIP_DIRECTION_HORIZONTAL + } + width, height := img.Rect.Max.X, img.Rect.Max.Y + widthCorrection, heightCorrection := 0, 0 + if width%2 != 0 { + widthCorrection = 1 + } + 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 + if direction == constants.FLIP_DIRECTION_HORIZONTAL && x < width/2+widthCorrection { + j = utilities.GetPixel(width-x-1, y, width) + } + if direction == constants.FLIP_DIRECTION_VERTICAL && y < height/2+heightCorrection { + j = utilities.GetPixel(x, height-y-1, width) + } + 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 + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/gamma-correction.go b/filters/gamma-correction.go new file mode 100644 index 0000000..94c6339 --- /dev/null +++ b/filters/gamma-correction.go @@ -0,0 +1,23 @@ +package filters + +import ( + "io" + "math" + + "github.com/julyskies/brille/utilities" +) + +func GammaCorrection(file io.Reader, amount float64) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + amount = utilities.MaxMin(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)) + } + return utilities.EncodeResult(img, format) +} diff --git a/index.go b/index.go index 604d6ed..6c53302 100644 --- a/index.go +++ b/index.go @@ -10,10 +10,16 @@ import ( "github.com/julyskies/brille/utilities" ) +const FLIP_DIRECTION_HORIZONTAL string = constants.FLIP_DIRECTION_HORIZONTAL + +const FLIP_DIRECTION_VERTICAL string = constants.FLIP_DIRECTION_VERTICAL + const GRAYSCALE_AVERAGE string = constants.GRAYSCALE_AVERAGE const GRAYSCALE_LUMINOCITY string = constants.GRAYSCALE_LUMINOCITY +/* Optimized filters */ + // threshold: 0 to 255 func Binary(file io.Reader, threshold uint8) (io.Reader, string, error) { if file == nil { @@ -22,157 +28,69 @@ func Binary(file io.Reader, threshold uint8) (io.Reader, string, error) { return filters.Binary(file, threshold) } -// max amount: (min(width, height) / 2) -func BoxBlur(file io.Reader, amount uint) (io.Reader, string, error) { +// radius: any uint +func BoxBlur(file io.Reader, radius uint) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - blurred := processing.BoxBlur(source, amount) - encoded, encodingError := utilities.PrepareResult(blurred, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.BoxBlur(file, radius) } -// amount: from -255 to 255 +// amount: -255 to 255 func Brightness(file io.Reader, amount int) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - amount = utilities.MaxMin(amount, 255, -255) - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - brightness := processing.Brightness(source, amount) - encoded, encodingError := utilities.PrepareResult(brightness, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Brightness(file, amount) } func ColorInversion(file io.Reader) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - binary := processing.ColorInversion(source) - encoded, encodingError := utilities.PrepareResult(binary, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.ColorInversion(file) } -// amount: from -255 to 255 +// amount: -255 to 255 func Contrast(file io.Reader, amount int) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - amount = utilities.MaxMin(amount, 255, -255) - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - contrast := processing.Contrast(source, amount) - encoded, encodingError := utilities.PrepareResult(contrast, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Contrast(file, amount) } func EightColors(file io.Reader) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - eightColors := processing.EightColors(source) - encoded, encodingError := utilities.PrepareResult(eightColors, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil -} - -func EmbossFilter(file io.Reader) (io.Reader, string, error) { - if file == nil { - return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) - } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - emboss := processing.EmbossFilter(source) - encoded, encodingError := utilities.PrepareResult(emboss, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.EightColors(file) } -func FlipHorizontal(file io.Reader) (io.Reader, string, error) { +func Emboss(file io.Reader) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - flipped := processing.FlipHorizontal(source) - encoded, encodingError := utilities.PrepareResult(flipped, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Emboss(file) } -func FlipVertical(file io.Reader) (io.Reader, string, error) { +// direction: horizontal or vertical +func Flip(file io.Reader, direction string) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - flipped := processing.FlipVertical(source) - encoded, encodingError := utilities.PrepareResult(flipped, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Flip(file, direction) } -// amount: from 0 to 3.99 +// amount: 0 to 3.99 func GammaCorrection(file io.Reader, amount float64) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - amount = utilities.MaxMin(amount, 3.99, 0) - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - gamma := processing.GammaCorrection(source, amount) - encoded, encodingError := utilities.PrepareResult(gamma, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.GammaCorrection(file, amount) } +/* Non-optimized filters */ + // type: average or luminocity func Grayscale(file io.Reader, grayscaleType string) (io.Reader, string, error) { if file == nil { diff --git a/processing/box-blur.go b/processing/box-blur.go deleted file mode 100644 index 8ff4cf6..0000000 --- a/processing/box-blur.go +++ /dev/null @@ -1,53 +0,0 @@ -package processing - -import ( - "image/color" - "math" - - "github.com/julyskies/brille/utilities" -) - -func getPoints(current, amount, total int) (int, int) { - start, end := 0, total - if current >= amount { - start = current - amount - } - if current < total-amount { - end = current + amount - } - return start, end -} - -func BoxBlur(source [][]color.Color, amount uint) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - min := math.Min(float64(width), float64(height)) - if amount > (uint(min) / 2) { - amount = uint(min / 2) - } - amountInt := int(amount) - var denominator uint - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - var tR, tG, tB uint - _, _, _, A := source[x][y].RGBA() - denominator = 0 - iStart, iEnd := getPoints(x, amountInt, width) - jStart, jEnd := getPoints(y, amountInt, height) - for i := iStart; i < iEnd; i += 1 { - for j := jStart; j < jEnd; j += 1 { - denominator += 1 - R, G, B, _ := source[i][j].RGBA() - tR += uint(uint8(R)) - tG += uint(uint8(G)) - tB += uint(uint8(B)) - } - } - bR := tR / denominator - bG := tG / denominator - bB := tB / denominator - destination[x][y] = color.RGBA{uint8(bR), uint8(bG), uint8(bB), uint8(A)} - } - } - return destination -} diff --git a/processing/brightness.go b/processing/brightness.go deleted file mode 100644 index ff13110..0000000 --- a/processing/brightness.go +++ /dev/null @@ -1,28 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -func calculateNewBrightness(partial uint8, amount int) uint8 { - newPartial := int(partial) + amount - newPartial = utilities.MaxMin(newPartial, 255, 0) - return uint8(newPartial) -} - -func Brightness(source [][]color.Color, amount int) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - r, g, b, alpha := utilities.RGBA(source[x][y]) - cR := calculateNewBrightness(r, amount) - cG := calculateNewBrightness(g, amount) - cB := calculateNewBrightness(b, amount) - destination[x][y] = color.RGBA{cR, cG, cB, alpha} - } - } - return destination -} diff --git a/processing/color-inversion.go b/processing/color-inversion.go deleted file mode 100644 index 32d45d5..0000000 --- a/processing/color-inversion.go +++ /dev/null @@ -1,24 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -func ColorInversion(source [][]color.Color) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - R, G, B, A := source[x][y].RGBA() - destination[x][y] = color.RGBA{ - 255 - uint8(R), - 255 - uint8(G), - 255 - uint8(B), - uint8(A), - } - } - } - return destination -} diff --git a/processing/contrast.go b/processing/contrast.go deleted file mode 100644 index 56171bb..0000000 --- a/processing/contrast.go +++ /dev/null @@ -1,29 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -func calculateNewContrast(partial uint8, factor float64) uint8 { - newPartial := factor*(float64(partial)-128) + 128 - newPartial = utilities.MaxMin(newPartial, 255, 0) - return uint8(newPartial) -} - -func Contrast(source [][]color.Color, amount int) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - factor := float64(259*(amount+255)) / float64(255*(259-amount)) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - r, g, b, alpha := utilities.RGBA(source[x][y]) - cR := calculateNewContrast(r, factor) - cG := calculateNewContrast(g, factor) - cB := calculateNewContrast(b, factor) - destination[x][y] = color.RGBA{cR, cG, cB, alpha} - } - } - return destination -} diff --git a/processing/eight-colors.go b/processing/eight-colors.go deleted file mode 100644 index a2008d4..0000000 --- a/processing/eight-colors.go +++ /dev/null @@ -1,52 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -type Color struct { - R, G, B int -} - -var COLORS = [8]Color{ - {255, 0, 0}, - {0, 255, 0}, - {0, 0, 255}, - {255, 255, 0}, - {255, 0, 255}, - {0, 255, 255}, - {255, 255, 255}, - {0, 0, 0}, -} - -func EightColors(source [][]color.Color) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - r, g, b, alpha := utilities.RGBA(source[x][y]) - minDelta := 195076 - var selectedColor Color - for i := range COLORS { - indexColor := COLORS[i] - rDifference := int(r) - indexColor.R - gDifference := int(g) - indexColor.G - bDifference := int(b) - indexColor.B - delta := rDifference*rDifference + gDifference*gDifference + bDifference*bDifference - if delta < minDelta { - minDelta = delta - selectedColor = indexColor - } - } - destination[x][y] = color.RGBA{ - uint8(selectedColor.R), - uint8(selectedColor.G), - uint8(selectedColor.B), - alpha, - } - } - } - return destination -} diff --git a/processing/emboss-filter.go b/processing/emboss-filter.go deleted file mode 100644 index 37bbdb5..0000000 --- a/processing/emboss-filter.go +++ /dev/null @@ -1,49 +0,0 @@ -package processing - -import ( - "image/color" - "math" - - "github.com/julyskies/brille/constants" - "github.com/julyskies/brille/utilities" -) - -var embossHorizontal = [3][3]int{ - {0, 0, 0}, - {1, 0, -1}, - {0, 0, 0}, -} - -var embossVertical = [3][3]int{ - {0, 1, 0}, - {0, 0, 0}, - {0, -1, 0}, -} - -func EmbossFilter(source [][]color.Color) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - gradientX := 0 - gradientY := 0 - for i := 0; i < 3; i += 1 { - for j := 0; j < 3; j += 1 { - k := utilities.GradientPoint(x, i, width) - l := utilities.GradientPoint(y, j, height) - grayColor, _ := utilities.Gray( - source[x+k][y+l], - constants.GRAYSCALE_AVERAGE, - ) - gradientX += int(grayColor) * embossHorizontal[i][j] - gradientY += int(grayColor) * embossVertical[i][j] - } - } - colorCode := 255 - uint8(int(math.Sqrt( - float64((gradientX*gradientX)+(gradientY*gradientY)), - ))) - destination[x][y] = color.RGBA{colorCode, colorCode, colorCode, 255} - } - } - return destination -} diff --git a/processing/flip-horizontal.go b/processing/flip-horizontal.go deleted file mode 100644 index 7222897..0000000 --- a/processing/flip-horizontal.go +++ /dev/null @@ -1,23 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -func FlipHorizontal(source [][]color.Color) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - correction := 0 - if width%2 != 0 { - correction = 1 - } - for x := 0; x < width/2+correction; x += 1 { - for y := 0; y < height; y += 1 { - z := width - x - 1 - destination[x][y], destination[z][y] = source[z][y], source[x][y] - } - } - return destination -} diff --git a/processing/flip-vertical.go b/processing/flip-vertical.go deleted file mode 100644 index 932ef91..0000000 --- a/processing/flip-vertical.go +++ /dev/null @@ -1,23 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -func FlipVertical(source [][]color.Color) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - correction := 0 - if height%2 != 0 { - correction = 1 - } - for x := 0; x < width; x += 1 { - for y := 0; y < height/2+correction; y += 1 { - z := height - y - 1 - destination[x][y], destination[x][z] = source[x][z], source[x][y] - } - } - return destination -} diff --git a/processing/gamma-correction.go b/processing/gamma-correction.go deleted file mode 100644 index 190bb17..0000000 --- a/processing/gamma-correction.go +++ /dev/null @@ -1,24 +0,0 @@ -package processing - -import ( - "image/color" - "math" - - "github.com/julyskies/brille/utilities" -) - -func GammaCorrection(source [][]color.Color, amount float64) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - power := 1 / amount - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - r, g, b, alpha := utilities.RGBA(source[x][y]) - cR := uint8(255 * math.Pow(float64(r)/255, power)) - cG := uint8(255 * math.Pow(float64(g)/255, power)) - cB := uint8(255 * math.Pow(float64(b)/255, power)) - destination[x][y] = color.RGBA{cR, cG, cB, alpha} - } - } - return destination -} diff --git a/utilities/get-aperture.go b/utilities/get-aperture.go new file mode 100644 index 0000000..c5c65b2 --- /dev/null +++ b/utilities/get-aperture.go @@ -0,0 +1,12 @@ +package utilities + +func GetAperture(axisValue, axisMax, apertureMin, apertureMax int) (int, int) { + start, end := 0, axisMax + if axisValue+apertureMin > 0 { + start = axisValue + apertureMin + } + if axisValue+apertureMax < axisMax { + end = axisValue + apertureMax + } + return start, end +} diff --git a/utilities/get-coordinates.go b/utilities/get-coordinates.go new file mode 100644 index 0000000..aeea2d5 --- /dev/null +++ b/utilities/get-coordinates.go @@ -0,0 +1,7 @@ +package utilities + +import "math" + +func GetCoordinates(pixel, width int) (int, int) { + return pixel % width, int(math.Floor(float64(pixel) / float64(width))) +} diff --git a/utilities/get-pixel.go b/utilities/get-pixel.go new file mode 100644 index 0000000..8f78976 --- /dev/null +++ b/utilities/get-pixel.go @@ -0,0 +1,5 @@ +package utilities + +func GetPixel(x, y, width int) int { + return ((y * width) + x) * 4 +} From 600bd5e341353221a8531bf52bc6bc3fc59ae7bf Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 8 Feb 2023 14:17:16 +0300 Subject: [PATCH 3/5] Optimize the rest of the filters --- constants/index.go | 12 +- filters/grayscale.go | 31 ++++ filters/hue-rotate.go | 32 ++++ filters/kuwahara.go | 80 ++++++++ filters/laplacian.go | 37 ++++ filters/rotate-fixed.go | 63 +++++++ filters/sepia.go | 22 +++ filters/sharpen.go | 46 +++++ filters/sobel.go | 51 ++++++ filters/solarize.go | 27 +++ index.go | 173 +++--------------- processing/grayscale.go | 25 --- processing/hue-rotate.go | 49 ----- processing/image-rotation.go | 31 ---- processing/kuwahara-filter.go | 87 --------- processing/laplasian-filter.go | 38 ---- processing/sepia.go | 31 ---- processing/sharpen.go | 41 ----- processing/sobel-filter.go | 49 ----- processing/solarize.go | 29 --- utilities/create-grid.go | 11 -- ...repare-result.go => encode-grid-result.go} | 21 +-- utilities/encode-result.go | 7 +- utilities/gray.go | 27 --- utilities/prepare-source.go | 33 ---- utilities/rgba.go | 12 -- 26 files changed, 436 insertions(+), 629 deletions(-) create mode 100644 filters/grayscale.go create mode 100644 filters/hue-rotate.go create mode 100644 filters/kuwahara.go create mode 100644 filters/laplacian.go create mode 100644 filters/rotate-fixed.go create mode 100644 filters/sepia.go create mode 100644 filters/sharpen.go create mode 100644 filters/sobel.go create mode 100644 filters/solarize.go delete mode 100644 processing/grayscale.go delete mode 100644 processing/hue-rotate.go delete mode 100644 processing/image-rotation.go delete mode 100644 processing/kuwahara-filter.go delete mode 100644 processing/laplasian-filter.go delete mode 100644 processing/sepia.go delete mode 100644 processing/sharpen.go delete mode 100644 processing/sobel-filter.go delete mode 100644 processing/solarize.go delete mode 100644 utilities/create-grid.go rename utilities/{prepare-result.go => encode-grid-result.go} (60%) delete mode 100644 utilities/gray.go delete mode 100644 utilities/prepare-source.go delete mode 100644 utilities/rgba.go diff --git a/constants/index.go b/constants/index.go index 727cd86..61ef269 100644 --- a/constants/index.go +++ b/constants/index.go @@ -1,13 +1,17 @@ package constants -const ERROR_INVALID_GRAYSCALE_TYPE string = "invalid grayscale type" - const ERROR_NO_FILE_PROVIDED string = "no file provided" const FLIP_DIRECTION_HORIZONTAL string = "horizontal" const FLIP_DIRECTION_VERTICAL string = "vertical" -const GRAYSCALE_AVERAGE string = "average" +const GRAYSCALE_TYPE_AVERAGE string = "average" + +const GRAYSCALE_TYPE_LUMINANCE string = "luminance" + +const ROTATE_FIXED_90 uint = 90 + +const ROTATE_FIXED_180 uint = 180 -const GRAYSCALE_LUMINOCITY string = "luminocity" +const ROTATE_FIXED_270 uint = 270 diff --git a/filters/grayscale.go b/filters/grayscale.go new file mode 100644 index 0000000..e93bd8e --- /dev/null +++ b/filters/grayscale.go @@ -0,0 +1,31 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/constants" + "github.com/julyskies/brille/utilities" +) + +func Grayscale(file io.Reader, grayscaleType string) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + if grayscaleType != constants.GRAYSCALE_TYPE_AVERAGE && + grayscaleType != constants.GRAYSCALE_TYPE_LUMINANCE { + grayscaleType = constants.GRAYSCALE_TYPE_AVERAGE + } + for i := 0; i < len(img.Pix); i += 4 { + var channel uint8 + if grayscaleType == constants.GRAYSCALE_TYPE_AVERAGE { + channel = uint8((int(img.Pix[i]) + int(img.Pix[i+1]) + int(img.Pix[i+2])) / 3) + } else { + channel = uint8( + float64(img.Pix[i])*0.21 + float64(img.Pix[i+1])*0.72 + float64(img.Pix[i+2])*0.07, + ) + } + img.Pix[i], img.Pix[i+1], img.Pix[i+2] = channel, channel, channel + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/hue-rotate.go b/filters/hue-rotate.go new file mode 100644 index 0000000..47e7ff5 --- /dev/null +++ b/filters/hue-rotate.go @@ -0,0 +1,32 @@ +package filters + +import ( + "io" + "math" + + "github.com/julyskies/brille/utilities" +) + +const DEG float64 = math.Pi / 180 + +func HueRotate(file io.Reader, angle int) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + cos := math.Cos(float64(angle) * DEG) + sin := math.Sin(float64(angle) * DEG) + matrix := [3]float64{ + cos + (1-cos)/3, + (1-cos)/3 - math.Sqrt(float64(1)/3)*sin, + (1-cos)/3 + math.Sqrt(float64(1)/3)*sin, + } + for i := 0; i < len(img.Pix); i += 4 { + r, g, b := img.Pix[i], img.Pix[i+1], img.Pix[i+2] + rr := utilities.MaxMin(float64(r)*matrix[0]+float64(g)*matrix[1]+float64(b)*matrix[2], 255, 0) + rg := utilities.MaxMin(float64(r)*matrix[2]+float64(g)*matrix[0]+float64(b)*matrix[1], 255, 0) + rb := utilities.MaxMin(float64(r)*matrix[1]+float64(g)*matrix[2]+float64(b)*matrix[0], 255, 0) + img.Pix[i], img.Pix[i+1], img.Pix[i+2] = uint8(rr), uint8(rg), uint8(rb) + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/kuwahara.go b/filters/kuwahara.go new file mode 100644 index 0000000..8d1e475 --- /dev/null +++ b/filters/kuwahara.go @@ -0,0 +1,80 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +func Kuwahara(file io.Reader, radius uint) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + width, height := img.Rect.Max.X, img.Rect.Max.Y + radiusInt := int(radius) + destination := make([]uint8, len(img.Pix)) + apertureMinX := [4]int{-radiusInt, 0, -radiusInt, 0} + apertureMaxX := [4]int{0, radiusInt, 0, radiusInt} + apertureMinY := [4]int{-radiusInt, -radiusInt, 0, 0} + apertureMaxY := [4]int{0, 0, radiusInt, radiusInt} + for i := 0; i < len(img.Pix); i += 4 { + x, y := utilities.GetCoordinates(i/4, width) + rValues := [4]int{0, 0, 0, 0} + gValues := [4]int{0, 0, 0, 0} + bValues := [4]int{0, 0, 0, 0} + maxRValue := [4]int{0, 0, 0, 0} + maxGValue := [4]int{0, 0, 0, 0} + maxBValue := [4]int{0, 0, 0, 0} + minRValue := [4]int{255, 255, 255, 255} + minGValue := [4]int{255, 255, 255, 255} + minBValue := [4]int{255, 255, 255, 255} + pixelsCount := [4]int{0, 0, 0, 0} + for i := 0; i < 4; i += 1 { + x2s, x2e := utilities.GetAperture(x, width, apertureMinX[i], apertureMaxX[i]) + y2s, y2e := utilities.GetAperture(y, height, apertureMinY[i], apertureMaxY[i]) + for x2 := x2s; x2 < x2e; x2 += 1 { + for y2 := y2s; y2 < y2e; y2 += 1 { + px := utilities.GetPixel(x2, y2, width) + r, g, b := img.Pix[px], img.Pix[px+1], img.Pix[px+2] + rValues[i] += int(r) + gValues[i] += int(g) + bValues[i] += int(b) + if int(r) > maxRValue[i] { + maxRValue[i] = int(r) + } else if int(r) < minRValue[i] { + minRValue[i] = int(r) + } + if int(g) > maxGValue[i] { + maxGValue[i] = int(g) + } else if int(g) < minGValue[i] { + minGValue[i] = int(g) + } + if int(b) > maxBValue[i] { + maxBValue[i] = int(b) + } else if int(b) < minBValue[i] { + minBValue[i] = int(b) + } + pixelsCount[i] += 1 + } + } + } + j := 0 + minDifference := 10000 + for i := 0; i < 4; i += 1 { + cdR := maxRValue[i] - minRValue[i] + cdG := maxGValue[i] - minGValue[i] + cdB := maxBValue[i] - minBValue[i] + CurrentDifference := cdR + cdG + cdB + if CurrentDifference < minDifference && pixelsCount[i] > 0 { + j = i + minDifference = CurrentDifference + } + } + destination[i] = uint8(rValues[j] / pixelsCount[j]) + destination[i+1] = uint8(gValues[j] / pixelsCount[j]) + destination[i+2] = uint8(bValues[j] / pixelsCount[j]) + } + img.Pix = destination + return utilities.EncodeResult(img, format) +} diff --git a/filters/laplacian.go b/filters/laplacian.go new file mode 100644 index 0000000..87605e6 --- /dev/null +++ b/filters/laplacian.go @@ -0,0 +1,37 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +var laplacianKernel = [3][3]int{ + {-1, -1, -1}, + {-1, 8, -1}, + {-1, -1, -1}, +} + +func Laplacian(file io.Reader) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + width, height := img.Rect.Max.X, img.Rect.Max.Y + for i := 0; i < len(img.Pix); i += 4 { + averageSum := 0 + x, y := utilities.GetCoordinates(i/4, width) + 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 + averageSum += average * laplacianKernel[m][n] + } + } + channel := 255 - uint8(utilities.MaxMin(averageSum, 255, 0)) + img.Pix[i], img.Pix[i+1], img.Pix[i+2] = channel, channel, channel + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/rotate-fixed.go b/filters/rotate-fixed.go new file mode 100644 index 0000000..d918c5c --- /dev/null +++ b/filters/rotate-fixed.go @@ -0,0 +1,63 @@ +package filters + +import ( + "image/color" + "io" + + "github.com/julyskies/brille/constants" + "github.com/julyskies/brille/utilities" +) + +func RotateFixed(file io.Reader, angle uint) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + if angle != constants.ROTATE_FIXED_90 && + angle != constants.ROTATE_FIXED_180 && + angle != constants.ROTATE_FIXED_270 { + angle = constants.ROTATE_FIXED_90 + } + width, height := img.Rect.Max.X, img.Rect.Max.Y + heightCorrection := 0 + if height%2 != 0 { + heightCorrection = 1 + } + if angle == constants.ROTATE_FIXED_180 { + for i := 0; i < len(img.Pix); i += 4 { + x, y := utilities.GetCoordinates(i/4, width) + r, g, b := img.Pix[i], img.Pix[i+1], img.Pix[i+2] + var j int + if y < height/2+heightCorrection { + j = utilities.GetPixel(width-x-1, height-y-1, width) + } + 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 + } + return utilities.EncodeResult(img, format) + } + destination := make([][]color.Color, width) + for i := range destination { + destination[i] = make([]color.Color, height) + } + for i := 0; i < len(img.Pix); i += 4 { + x, y := utilities.GetCoordinates(i/4, width) + if angle == constants.ROTATE_FIXED_90 { + destination[height-y-1][x] = color.RGBA{ + img.Pix[i], + img.Pix[i+1], + img.Pix[i+2], + img.Pix[i+3], + } + } + if angle == constants.ROTATE_FIXED_270 { + destination[y][width-x-1] = color.RGBA{ + img.Pix[i], + img.Pix[i+1], + img.Pix[i+2], + img.Pix[i+3], + } + } + } + return utilities.EncodeGridResult(destination, format) +} diff --git a/filters/sepia.go b/filters/sepia.go new file mode 100644 index 0000000..c05928e --- /dev/null +++ b/filters/sepia.go @@ -0,0 +1,22 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +func Sepia(file io.Reader) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + for i := 0; i < len(img.Pix); i += 4 { + r, g, b := img.Pix[i], img.Pix[i+1], img.Pix[i+2] + sr := utilities.MaxMin(0.393*float64(r)+0.769*float64(g)+0.189*float64(b), 255.0, 0.0) + sg := utilities.MaxMin(0.349*float64(r)+0.686*float64(g)+0.168*float64(b), 255.0, 0.0) + sb := utilities.MaxMin(0.272*float64(r)+0.534*float64(g)+0.131*float64(b), 255.0, 0.0) + img.Pix[i], img.Pix[i+1], img.Pix[i+2] = uint8(sr), uint8(sg), uint8(sb) + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/sharpen.go b/filters/sharpen.go new file mode 100644 index 0000000..0e9b6fa --- /dev/null +++ b/filters/sharpen.go @@ -0,0 +1,46 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +var sharpenKernel = [3][3]int{ + {-1, -1, -1}, + {-1, 9, -1}, + {-1, -1, -1}, +} + +func Sharpen(file io.Reader, amount uint) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + mix := float64(utilities.MaxMin(amount, 100, 0)) / 100 + width, height := img.Rect.Max.X, img.Rect.Max.Y + for i := 0; i < len(img.Pix); i += 4 { + dR, dG, dB := 0, 0, 0 + x, y := utilities.GetCoordinates(i/4, width) + 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) + dR += int(img.Pix[px]) * sharpenKernel[m][n] + dG += int(img.Pix[px+1]) * sharpenKernel[m][n] + dB += int(img.Pix[px+2]) * sharpenKernel[m][n] + } + } + img.Pix[i] = uint8( + utilities.MaxMin(float64(dR)*mix+float64(img.Pix[i])*(1-mix), 255, 0), + ) + img.Pix[i+1] = uint8( + utilities.MaxMin(float64(dG)*mix+float64(img.Pix[i+1])*(1-mix), 255, 0), + ) + img.Pix[i+2] = uint8( + utilities.MaxMin(float64(dB)*mix+float64(img.Pix[i+2])*(1-mix), 255, 0), + ) + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/sobel.go b/filters/sobel.go new file mode 100644 index 0000000..40ece95 --- /dev/null +++ b/filters/sobel.go @@ -0,0 +1,51 @@ +package filters + +import ( + "io" + "math" + + "github.com/julyskies/brille/utilities" +) + +var sobelHorizontal = [3][3]int{ + {-1, 0, 1}, + {-2, 0, 2}, + {-1, 0, 1}, +} + +var sobelVertical = [3][3]int{ + {1, 2, 1}, + {0, 0, 0}, + {-1, -2, -1}, +} + +func Sobel(file io.Reader) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + 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 * sobelHorizontal[m][n] + gradientY += average * sobelVertical[m][n] + } + } + channel := uint8( + 255 - utilities.MaxMin( + math.Sqrt(float64(gradientX*gradientX+gradientY*gradientY)), + 255, + 0, + ), + ) + img.Pix[i], img.Pix[i+1], img.Pix[i+2] = channel, channel, channel + } + return utilities.EncodeResult(img, format) +} diff --git a/filters/solarize.go b/filters/solarize.go new file mode 100644 index 0000000..54d5b6e --- /dev/null +++ b/filters/solarize.go @@ -0,0 +1,27 @@ +package filters + +import ( + "io" + + "github.com/julyskies/brille/utilities" +) + +func applySolarizeThreshold(subpixel, threshold uint8) uint8 { + if subpixel < threshold { + return 255 - subpixel + } + return subpixel +} + +func Solarize(file io.Reader, threshold uint8) (io.Reader, string, error) { + img, format, convertationError := utilities.DecodeSource(file) + if convertationError != nil { + return nil, "", convertationError + } + for i := 0; i < len(img.Pix); i += 4 { + img.Pix[i] = applySolarizeThreshold(img.Pix[i], threshold) + img.Pix[i+1] = applySolarizeThreshold(img.Pix[i+1], threshold) + img.Pix[i+2] = applySolarizeThreshold(img.Pix[i+2], threshold) + } + return utilities.EncodeResult(img, format) +} diff --git a/index.go b/index.go index 6c53302..a0b17d6 100644 --- a/index.go +++ b/index.go @@ -6,7 +6,6 @@ import ( "github.com/julyskies/brille/constants" "github.com/julyskies/brille/filters" - "github.com/julyskies/brille/processing" "github.com/julyskies/brille/utilities" ) @@ -14,11 +13,15 @@ const FLIP_DIRECTION_HORIZONTAL string = constants.FLIP_DIRECTION_HORIZONTAL const FLIP_DIRECTION_VERTICAL string = constants.FLIP_DIRECTION_VERTICAL -const GRAYSCALE_AVERAGE string = constants.GRAYSCALE_AVERAGE +const GRAYSCALE_TYPE_AVERAGE string = constants.GRAYSCALE_TYPE_AVERAGE -const GRAYSCALE_LUMINOCITY string = constants.GRAYSCALE_LUMINOCITY +const GRAYSCALE_TYPE_LUMINANCE string = constants.GRAYSCALE_TYPE_LUMINANCE -/* Optimized filters */ +const ROTATE_FIXED_90 uint = constants.ROTATE_FIXED_90 + +const ROTATE_FIXED_180 uint = constants.ROTATE_FIXED_180 + +const ROTATE_FIXED_270 uint = constants.ROTATE_FIXED_270 // threshold: 0 to 255 func Binary(file io.Reader, threshold uint8) (io.Reader, string, error) { @@ -89,192 +92,72 @@ func GammaCorrection(file io.Reader, amount float64) (io.Reader, string, error) return filters.GammaCorrection(file, amount) } -/* Non-optimized filters */ - -// type: average or luminocity +// grayscale type: average or luminance func Grayscale(file io.Reader, grayscaleType string) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - if grayscaleType != GRAYSCALE_AVERAGE && - grayscaleType != GRAYSCALE_LUMINOCITY { - return nil, "", errors.New(constants.ERROR_INVALID_GRAYSCALE_TYPE) - } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - grayscale := processing.Grayscale(source, grayscaleType) - encoded, encodingError := utilities.PrepareResult(grayscale, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Grayscale(file, grayscaleType) } -// angle: any int value +// angle: any int func HueRotate(file io.Reader, angle int) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - rotated := processing.HueRotate(source, angle) - encoded, encodingError := utilities.PrepareResult(rotated, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.HueRotate(file, angle) } -// aperture: 0 to 40 -func KuwaharaFilter(file io.Reader, aperture uint) (io.Reader, string, error) { +// radius: 0 to 40 +func Kuwahara(file io.Reader, radius uint) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - aperture = utilities.MaxMin(aperture, 40, 0) - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - kuwahara := processing.KuwaharaFilter(source, aperture) - encoded, encodingError := utilities.PrepareResult(kuwahara, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + radius = utilities.MaxMin(radius, 40, 0) + return filters.Kuwahara(file, radius) } -func LaplasianFilter(file io.Reader) (io.Reader, string, error) { +func Laplacian(file io.Reader) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - laplasian := processing.LaplasianFilter(source) - encoded, encodingError := utilities.PrepareResult(laplasian, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Laplacian(file) } -func Rotate90(file io.Reader) (io.Reader, string, error) { +// angle: 90, 180 or 270 (use provided constants) +func RotateFixed(file io.Reader, angle uint) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - rotated := processing.ImageRotation(source, 90) - encoded, encodingError := utilities.PrepareResult(rotated, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil -} - -func Rotate180(file io.Reader) (io.Reader, string, error) { - if file == nil { - return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) - } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - rotated := processing.ImageRotation(source, 180) - encoded, encodingError := utilities.PrepareResult(rotated, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil -} - -func Rotate270(file io.Reader) (io.Reader, string, error) { - if file == nil { - return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) - } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - rotated := processing.ImageRotation(source, 270) - encoded, encodingError := utilities.PrepareResult(rotated, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.RotateFixed(file, angle) } func Sepia(file io.Reader) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - sepia := processing.Sepia(source) - encoded, encodingError := utilities.PrepareResult(sepia, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Sepia(file) } // amount: 0 to 100 -func SharpenFilter(file io.Reader, amount uint) (io.Reader, string, error) { +func Sharpen(file io.Reader, amount uint) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - mix := float64(utilities.MaxMin(amount, 100, 0)) / 100 - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - sharpen := processing.Sharpen(source, mix) - encoded, encodingError := utilities.PrepareResult(sharpen, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Sharpen(file, amount) } -func SobelFilter(file io.Reader) (io.Reader, string, error) { +func Sobel(file io.Reader) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - sobel := processing.SobelFilter(source) - encoded, encodingError := utilities.PrepareResult(sobel, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Sobel(file) } -// threshold: 0 to 255 -func Solarize(file io.Reader, threshold uint) (io.Reader, string, error) { +// threshold: any uint8 +func Solarize(file io.Reader, threshold uint8) (io.Reader, string, error) { if file == nil { return nil, "", errors.New(constants.ERROR_NO_FILE_PROVIDED) } - threshold = utilities.MaxMin(threshold, 255, 0) - source, format, preparationError := utilities.PrepareSource(file) - if preparationError != nil { - return nil, "", preparationError - } - solarized := processing.Solarize(source, threshold) - encoded, encodingError := utilities.PrepareResult(solarized, format) - if encodingError != nil { - return nil, "", encodingError - } - return encoded, format, nil + return filters.Solarize(file, threshold) } diff --git a/processing/grayscale.go b/processing/grayscale.go deleted file mode 100644 index 5fb3131..0000000 --- a/processing/grayscale.go +++ /dev/null @@ -1,25 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/constants" - "github.com/julyskies/brille/utilities" -) - -func Grayscale(source [][]color.Color, grayscaleType string) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - var grayColor, alpha uint8 - if grayscaleType == constants.GRAYSCALE_LUMINOCITY { - grayColor, alpha = utilities.Gray(source[x][y], constants.GRAYSCALE_LUMINOCITY) - } else { - grayColor, alpha = utilities.Gray(source[x][y], constants.GRAYSCALE_AVERAGE) - } - destination[x][y] = color.RGBA{grayColor, grayColor, grayColor, alpha} - } - } - return destination -} diff --git a/processing/hue-rotate.go b/processing/hue-rotate.go deleted file mode 100644 index 490ae24..0000000 --- a/processing/hue-rotate.go +++ /dev/null @@ -1,49 +0,0 @@ -package processing - -import ( - "image/color" - "math" - - "github.com/julyskies/brille/utilities" -) - -const DEG float64 = math.Pi / 180 - -func HueRotate(source [][]color.Color, angle int) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - cos := math.Cos(float64(angle) * DEG) - sin := math.Sin(float64(angle) * DEG) - matrix := [3]float64{ - cos + (1-cos)/3, - (1-cos)/3 - math.Sqrt(float64(1)/3)*sin, - (1-cos)/3 + math.Sqrt(float64(1)/3)*sin, - } - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - r, g, b, alpha := utilities.RGBA(source[x][y]) - rr := utilities.MaxMin( - float64(r)*matrix[0]+float64(g)*matrix[1]+float64(b)*matrix[2], - 255, - 0, - ) - rg := utilities.MaxMin( - float64(r)*matrix[2]+float64(g)*matrix[0]+float64(b)*matrix[1], - 255, - 0, - ) - rb := utilities.MaxMin( - float64(r)*matrix[1]+float64(g)*matrix[2]+float64(b)*matrix[0], - 255, - 0, - ) - destination[x][y] = color.RGBA{ - uint8(rr), - uint8(rg), - uint8(rb), - alpha, - } - } - } - return destination -} diff --git a/processing/image-rotation.go b/processing/image-rotation.go deleted file mode 100644 index 0550ba5..0000000 --- a/processing/image-rotation.go +++ /dev/null @@ -1,31 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -func ImageRotation(source [][]color.Color, angle uint) [][]color.Color { - width, height := len(source), len(source[0]) - var destination [][]color.Color - if angle == 90 || angle == 270 { - destination = utilities.CreateGrid(height, width) - } else { - destination = utilities.CreateGrid(width, height) - } - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - if angle == 90 { - destination[height-y-1][x] = source[x][y] - } - if angle == 180 { - destination[width-x-1][height-y-1] = source[x][y] - } - if angle == 270 { - destination[y][width-x-1] = source[x][y] - } - } - } - return destination -} diff --git a/processing/kuwahara-filter.go b/processing/kuwahara-filter.go deleted file mode 100644 index 20b8ae4..0000000 --- a/processing/kuwahara-filter.go +++ /dev/null @@ -1,87 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -func getAperture(axisValue, axisMax, apertureMin, apertureMax int) (int, int) { - start, end := 0, axisMax - if axisValue+apertureMin > 0 { - start = axisValue + apertureMin - } - if axisValue+apertureMax < axisMax { - end = axisValue + apertureMax - } - return start, end -} - -func KuwaharaFilter(source [][]color.Color, aperture uint) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - apertureHalf := int(aperture / 2) - apertureMinX := [4]int{-apertureHalf, 0, -apertureHalf, 0} - apertureMaxX := [4]int{0, apertureHalf, 0, apertureHalf} - apertureMinY := [4]int{-apertureHalf, -apertureHalf, 0, 0} - apertureMaxY := [4]int{0, 0, apertureHalf, apertureHalf} - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - pixelCount := [4]int{0, 0, 0, 0} - rValues := [4]int{0, 0, 0, 0} - gValues := [4]int{0, 0, 0, 0} - bValues := [4]int{0, 0, 0, 0} - maxRValue := [4]int{0, 0, 0, 0} - maxGValue := [4]int{0, 0, 0, 0} - maxBValue := [4]int{0, 0, 0, 0} - minRValue := [4]int{255, 255, 255, 255} - minGValue := [4]int{255, 255, 255, 255} - minBValue := [4]int{255, 255, 255, 255} - for i := 0; i < 4; i += 1 { - x2start, x2end := getAperture(x, width, apertureMinX[i], apertureMaxX[i]) - y2start, y2end := getAperture(y, height, apertureMinY[i], apertureMaxY[i]) - for x2 := x2start; x2 < x2end; x2 += 1 { - for y2 := y2start; y2 < y2end; y2 += 1 { - r, g, b, _ := utilities.RGBA(source[x2][y2]) - rValues[i] += int(r) - gValues[i] += int(g) - bValues[i] += int(b) - if int(r) > maxRValue[i] { - maxRValue[i] = int(r) - } else if int(r) < minRValue[i] { - minRValue[i] = int(r) - } - if int(g) > maxGValue[i] { - maxGValue[i] = int(g) - } else if int(g) < minGValue[i] { - minGValue[i] = int(g) - } - if int(b) > maxBValue[i] { - maxBValue[i] = int(b) - } else if int(b) < minBValue[i] { - minBValue[i] = int(b) - } - pixelCount[i] += 1 - } - } - } - j := 0 - MinDifference := 10000 - for i := 0; i < 4; i += 1 { - cdR := maxRValue[i] - minRValue[i] - cdG := maxGValue[i] - minGValue[i] - cdB := maxBValue[i] - minBValue[i] - CurrentDifference := cdR + cdG + cdB - if CurrentDifference < MinDifference && pixelCount[i] > 0 { - j = i - MinDifference = CurrentDifference - } - } - cR := uint8(rValues[j] / pixelCount[j]) - cG := uint8(gValues[j] / pixelCount[j]) - cB := uint8(bValues[j] / pixelCount[j]) - destination[x][y] = color.RGBA{cR, cG, cB, 255} - } - } - return destination -} diff --git a/processing/laplasian-filter.go b/processing/laplasian-filter.go deleted file mode 100644 index 007ea15..0000000 --- a/processing/laplasian-filter.go +++ /dev/null @@ -1,38 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/constants" - "github.com/julyskies/brille/utilities" -) - -var laplacianKernel = [3][3]int{ - {-1, -1, -1}, - {-1, 8, -1}, - {-1, -1, -1}, -} - -func LaplasianFilter(source [][]color.Color) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - averageSum := 0 - for i := 0; i < 3; i += 1 { - for j := 0; j < 3; j += 1 { - k := utilities.GradientPoint(x, i, width) - l := utilities.GradientPoint(y, j, height) - grayColor, _ := utilities.Gray( - source[x+k][y+l], - constants.GRAYSCALE_AVERAGE, - ) - averageSum += int(grayColor) * laplacianKernel[i][j] - } - } - channel := 255 - uint8(utilities.MaxMin(averageSum, 255, 0)) - destination[x][y] = color.RGBA{channel, channel, channel, 255} - } - } - return destination -} diff --git a/processing/sepia.go b/processing/sepia.go deleted file mode 100644 index 4559f21..0000000 --- a/processing/sepia.go +++ /dev/null @@ -1,31 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -func Sepia(source [][]color.Color) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - r, g, b, alpha := utilities.RGBA(source[x][y]) - dR := 0.393*float64(r) + 0.769*float64(g) + 0.189*float64(b) - if dR > 255 { - dR = 255 - } - dG := 0.349*float64(r) + 0.686*float64(g) + 0.168*float64(b) - if dG > 255 { - dG = 255 - } - dB := 0.272*float64(r) + 0.534*float64(g) + 0.131*float64(b) - if dB > 255 { - dB = 255 - } - destination[x][y] = color.RGBA{uint8(dR), uint8(dG), uint8(dB), alpha} - } - } - return destination -} diff --git a/processing/sharpen.go b/processing/sharpen.go deleted file mode 100644 index 77393f7..0000000 --- a/processing/sharpen.go +++ /dev/null @@ -1,41 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -var sharpenKernel = [3][3]int{ - {-1, -1, -1}, - {-1, 9, -1}, - {-1, -1, -1}, -} - -func Sharpen(source [][]color.Color, mix float64) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - sumR := 0 - sumG := 0 - sumB := 0 - for i := 0; i < 3; i += 1 { - for j := 0; j < 3; j += 1 { - k := utilities.GradientPoint(x, i, width) - l := utilities.GradientPoint(y, j, height) - r, g, b, _ := utilities.RGBA(source[x+k][y+l]) - sumR += int(r) * sharpenKernel[i][j] - sumG += int(g) * sharpenKernel[i][j] - sumB += int(b) * sharpenKernel[i][j] - } - } - r, g, b, alpha := utilities.RGBA(source[x][y]) - R := utilities.MaxMin(float64(sumR)*mix+float64(r)*(1-mix), 255, 0) - G := utilities.MaxMin(float64(sumG)*mix+float64(g)*(1-mix), 255, 0) - B := utilities.MaxMin(float64(sumB)*mix+float64(b)*(1-mix), 255, 0) - destination[x][y] = color.RGBA{uint8(R), uint8(G), uint8(B), alpha} - } - } - return destination -} diff --git a/processing/sobel-filter.go b/processing/sobel-filter.go deleted file mode 100644 index 8811a25..0000000 --- a/processing/sobel-filter.go +++ /dev/null @@ -1,49 +0,0 @@ -package processing - -import ( - "image/color" - "math" - - "github.com/julyskies/brille/constants" - "github.com/julyskies/brille/utilities" -) - -var sobelHorizontal = [3][3]int{ - {-1, 0, 1}, - {-2, 0, 2}, - {-1, 0, 1}, -} - -var sobelVertical = [3][3]int{ - {1, 2, 1}, - {0, 0, 0}, - {-1, -2, -1}, -} - -func SobelFilter(source [][]color.Color) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - gradientX := 0 - gradientY := 0 - for i := 0; i < 3; i += 1 { - for j := 0; j < 3; j += 1 { - k := utilities.GradientPoint(x, i, width) - l := utilities.GradientPoint(y, j, height) - grayColor, _ := utilities.Gray( - source[x+k][y+l], - constants.GRAYSCALE_AVERAGE, - ) - gradientX += int(grayColor) * sobelHorizontal[i][j] - gradientY += int(grayColor) * sobelVertical[i][j] - } - } - colorCode := 255 - uint8(utilities.MaxMin(math.Sqrt( - float64(gradientX*gradientX+gradientY*gradientY), - ), 255, 0)) - destination[x][y] = color.RGBA{colorCode, colorCode, colorCode, 255} - } - } - return destination -} diff --git a/processing/solarize.go b/processing/solarize.go deleted file mode 100644 index 37ea931..0000000 --- a/processing/solarize.go +++ /dev/null @@ -1,29 +0,0 @@ -package processing - -import ( - "image/color" - - "github.com/julyskies/brille/utilities" -) - -func checkSolarizationThreshold(partial uint8, threshold uint) uint8 { - if partial <= uint8(threshold) { - return 255 - partial - } - return partial -} - -func Solarize(source [][]color.Color, threshold uint) [][]color.Color { - width, height := len(source), len(source[0]) - destination := utilities.CreateGrid(width, height) - for x := 0; x < width; x += 1 { - for y := 0; y < height; y += 1 { - r, g, b, alpha := utilities.RGBA(source[x][y]) - cR := checkSolarizationThreshold(r, threshold) - cG := checkSolarizationThreshold(g, threshold) - cB := checkSolarizationThreshold(b, threshold) - destination[x][y] = color.RGBA{cR, cG, cB, alpha} - } - } - return destination -} diff --git a/utilities/create-grid.go b/utilities/create-grid.go deleted file mode 100644 index 0e50336..0000000 --- a/utilities/create-grid.go +++ /dev/null @@ -1,11 +0,0 @@ -package utilities - -import "image/color" - -func CreateGrid(width, height int) [][]color.Color { - gridCopy := make([][]color.Color, width) - for i := range gridCopy { - gridCopy[i] = make([]color.Color, height) - } - return gridCopy -} diff --git a/utilities/prepare-result.go b/utilities/encode-grid-result.go similarity index 60% rename from utilities/prepare-result.go rename to utilities/encode-grid-result.go index ca8b731..0f1c0ae 100644 --- a/utilities/prepare-result.go +++ b/utilities/encode-grid-result.go @@ -7,11 +7,9 @@ import ( "image/jpeg" "image/png" "io" - "os" - "strconv" ) -func PrepareResult(result [][]color.Color, format string) (io.Reader, error) { +func EncodeGridResult(result [][]color.Color, format string) (io.Reader, string, error) { width, height := len(result), len(result[0]) nrgba := image.NewNRGBA(image.Rect(0, 0, width, height)) for x := 0; x < width; x += 1 { @@ -19,24 +17,15 @@ func PrepareResult(result [][]color.Color, format string) (io.Reader, error) { nrgba.Set(x, y, result[x][y]) } } - - jpegQualityENV := os.Getenv("BRILLE_JPEG_QUALITY") - jpegQuality := 100 - if jpegQualityENV != "" { - parsed, parsingError := strconv.Atoi(jpegQualityENV) - if parsingError == nil { - jpegQuality = MaxMin(parsed, 100, 0) - } - } - var buffer bytes.Buffer writer := io.Writer(&buffer) if format == "png" { encodingError := png.Encode(writer, nrgba.SubImage(nrgba.Rect)) if encodingError != nil { - return nil, encodingError + return nil, "", encodingError } } else { + jpegQuality := getJPEGQuality() encodingError := jpeg.Encode( writer, nrgba.SubImage(nrgba.Rect), @@ -45,8 +34,8 @@ func PrepareResult(result [][]color.Color, format string) (io.Reader, error) { }, ) if encodingError != nil { - return nil, encodingError + return nil, "", encodingError } } - return bytes.NewReader(buffer.Bytes()), nil + return bytes.NewReader(buffer.Bytes()), format, nil } diff --git a/utilities/encode-result.go b/utilities/encode-result.go index 8b64578..a1ce34e 100644 --- a/utilities/encode-result.go +++ b/utilities/encode-result.go @@ -10,7 +10,7 @@ import ( "strconv" ) -func EncodeResult(img *image.RGBA, format string) (io.Reader, string, error) { +func getJPEGQuality() int { jpegQualityENV := os.Getenv("BRILLE_JPEG_QUALITY") jpegQuality := 100 if jpegQualityENV != "" { @@ -19,6 +19,10 @@ func EncodeResult(img *image.RGBA, format string) (io.Reader, string, error) { jpegQuality = MaxMin(parsed, 100, 0) } } + return jpegQuality +} + +func EncodeResult(img *image.RGBA, format string) (io.Reader, string, error) { var buffer bytes.Buffer writer := io.Writer(&buffer) if format == "png" { @@ -27,6 +31,7 @@ func EncodeResult(img *image.RGBA, format string) (io.Reader, string, error) { return nil, "", encodingError } } else { + jpegQuality := getJPEGQuality() encodingError := jpeg.Encode( writer, img.SubImage(img.Rect), diff --git a/utilities/gray.go b/utilities/gray.go deleted file mode 100644 index ebc873e..0000000 --- a/utilities/gray.go +++ /dev/null @@ -1,27 +0,0 @@ -package utilities - -import ( - "image/color" - "math" - - "github.com/julyskies/brille/constants" -) - -func Gray(pixel color.Color, calculationType string) (gray uint8, alpha uint8) { - R, G, B, A := pixel.RGBA() - alpha = uint8(A) - if calculationType == constants.GRAYSCALE_LUMINOCITY { - gray = uint8( - math.Round( - (float64(uint8(R))*0.21 + float64(uint8(G))*0.72 + float64(uint8(B))*0.07), - ), - ) - return - } - gray = uint8( - math.Round( - (float64(uint8(R)) + float64(uint8(G)) + float64(uint8(B))) / 3.0, - ), - ) - return -} diff --git a/utilities/prepare-source.go b/utilities/prepare-source.go deleted file mode 100644 index ab556eb..0000000 --- a/utilities/prepare-source.go +++ /dev/null @@ -1,33 +0,0 @@ -package utilities - -import ( - "image" - "image/color" - "image/draw" - _ "image/jpeg" - _ "image/png" - "io" -) - -func PrepareSource(file io.Reader) ([][]color.Color, string, error) { - content, format, decodingError := image.Decode(file) - if decodingError != nil { - return nil, "", decodingError - } - - rect := content.Bounds() - height, width := rect.Dy(), rect.Dx() - rgba := image.NewRGBA(image.Rect(0, 0, width, height)) - draw.Draw(rgba, rgba.Bounds(), content, rect.Min, draw.Src) - - grid := make([][]color.Color, width) - for x := 0; x < width; x += 1 { - col := make([]color.Color, height) - for y := 0; y < height; y += 1 { - col[y] = rgba.At(x, y) - } - grid[x] = col - } - - return grid, format, nil -} diff --git a/utilities/rgba.go b/utilities/rgba.go deleted file mode 100644 index 40d5737..0000000 --- a/utilities/rgba.go +++ /dev/null @@ -1,12 +0,0 @@ -package utilities - -import "image/color" - -func RGBA(pixel color.Color) (r, g, b, alpha uint8) { - R, G, B, A := pixel.RGBA() - alpha = uint8(A) - b = uint8(B) - g = uint8(G) - r = uint8(R) - return -} From 1061787c77a34c9deba45a1611d0adeb39ea6acf Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 8 Feb 2023 17:33:22 +0300 Subject: [PATCH 4/5] README --- README.md | 87 +++++++++++++++++++++++++------------------------------ 1 file changed, 39 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index f4da523..6862ba4 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ func main() { } defer file.Close() - sobel, format, processingError := brille.SobelFilter(file) + sobel, format, processingError := brille.Sobel(file) if processingError != nil { log.Fatal(processingError) } @@ -94,7 +94,7 @@ func controller(context *fiber.Ctx) error { result, format, processingError := brille.Grayscale( fileHandle, - brille.GRAYSCALE_AVERAGE, + brille.GRAYSCALE_TYPE_LUMINANCE, ) if processingError != nil { return fiber.NewError(fiber.StatusInternalServerError) @@ -109,106 +109,97 @@ Full Fiber example is available at https://github.com/peterdee/filtering-backend ### Available filters -- **Binary**: convert an image to 1 bit black and white. Requires a threshold value (from 0 to 255): +- **Binary**: converts an image to 1 bit black and white. Requires a threshold value (`uint8`, 0 to 255): ```golang binary, format, processingError := brille.Binary(file, 155) ``` -- **Box blur**: blur the image. Requires blur amount to be provided. Blur amount is a `uint` value. Box blur function will automatically reduce the amount value if it is too big, maximum amount is `min(width, height) / 2`: +- **Box blur**: blurs the image. Requires blur amount to be provided. Blur amount is a `uint` value: ```golang - blurred, format, processingError := brille.BoxBlur(file, 5) + blurred, format, processingError := brille.BoxBlur(file, 7) ``` -- **Brightness**: adjust image bightness. Requires brightness amount to be provided. Brightness amount ranges from -255 (darker) to 255 (brighter): +- **Brightness**: adjusts image bightness. Requires brightness amount to be provided (`int`, -255 to 255): ```golang - brightened, format, processingError := brille.Brightness(file, 107) + brightened, format, processingError := brille.Brightness(file, 75) ``` -- **Color inversion**: invert image colors. +- **Color inversion**: inverts image colors: ```golang inverted, format, processingError := brille.ColorInversion(file) ``` -- **Contrast**: adjust image contrast. Requires contrast amount to be provided. Contrast amount ranges from -255 (less contrast) to 255 (more contrast): +- **Contrast**: adjusts image contrast. Requires contrast amount to be provided (`int`, -255 to 255): ```golang - contrastless, format, processingError := brille.Contrast(file, -40) + contrast, format, processingError := brille.Contrast(file, -45) ``` -- **Eight colors**: this filter leaves only eight colors present on the image (red, green, blue, yellow, cyan, magenta, white, black). +- **Eight colors**: this filter leaves only eight colors present on the image (red, green, blue, yellow, cyan, magenta, white, black): ```golang - indexedColors, format, processingError := brille.EightColors(file) + eightColors, format, processingError := brille.EightColors(file) ``` -- **Emboss filter**: a static edge detection filter that uses a 3x3 kernel. It can be used to outline edges on an image: +- **Emboss**: a static edge detection filter that uses a 3x3 kernel. It can be used to outline edges on an image: ```golang - embossed, format, processingError := brille.EmbossFilter(file) + embossed, format, processingError := brille.Emboss(file) ``` -- **Flip horizontal**: flip image horizontally, basically reflect the image in *X* axis. +- **Flip**: flips image horizontally or vertically. This filter requires a second argument - flip direction. Flip directions are available as `brille` module constants (FLIP_DIRECTION_HORIZONTAL and FLIP_DIRECTION_VERTICAL): ```golang - flippedX, format, processingError := brille.FlipHorizontal(file) - ``` - -- **Flip vertical**: flip image vertically, basically reflect the image in *Y* axis. - - ```golang - flippedY, format, processingError := brille.FlipVertical(file) + flipped, format, processingError := brille.Flip( + file, + brille.FLIP_DIRECTION_HORIZONTAL, + ) ``` -- **Gamma correction**: image gamma correction. Requires correction amount to be provided. Correction amount ranges from `0` to `3.99` (`float64`). By default image gamma equals to `1`, so providing a value less than `1` makes colors more intense, and values more than `1` decrease color intensity: +- **Gamma correction**: corrects image gamma. Requires correction amount to be provided (`float64`, 0 to 3.99). By default image gamma equals to 1, so providing a value less than that makes colors more intense, and larger values decrease color intensity: ```golang corrected, format, processingError := brille.GammaCorrection(file, 2.05) ``` -- **Grayscale**: turn colors into shades of gray. Requires grayscale type to be provided. There are 2 grayscale types available: `average` (or `mean`) and `luminocity` (or `weighted`). Both types are available as constants in `brille` module: +- **Grayscale**: turns colors into shades of gray. This filter requires a second argument - grayscale type. Grayscale types are available as `brille` module constants (GRAYSCALE_TYPE_AVERAGE and GRAYSCALE_TYPE_LUMINANCE): ```golang - grayAverage, format, processingError := brille.Grayscale( + grayscale, format, processingError := brille.Grayscale( file, - brille.GRAYSCALE_AVERAGE, - ) - - grayLuminocity, format, processingError := brille.Grayscale( - file, - brille.GRAYSCALE_LUMINOCITY, + brille.GRAYSCALE_TYPE_LUMINANCE, ) ``` -- **Hue rotation**: rotate image hue to change the colors. Requires an angle to be provided. Angle represents degree of a hue rotation, can be any `int` number: +- **Hue rotation**: rotates image hue. Requires an angle to be provided (`int`, any value): ```golang rotated, format, processingError := brille.HueRotate(file, 278) ``` -- **Kuwahara filter**: an edge detection filter with dynamic aperture size. Requires aperture size to be provided, but due to the perfomance reasons maximum aperture size is limited to 40. This filter is very slow, and will probably be optimized in the future: +- **Kuwahara**: an edge detection filter with dynamic kernel size. Requires radius to be provided (`uint`, any value). This filter is pretty slow, and will probably be optimized in the future: ```golang - kuwahara, format, processingError := brille.KuwaharaFilter(file, 9) + kuwahara, format, processingError := brille.Kuwahara(file, 5) ``` -- **Laplasian filter**: a static edge detection filter that uses a 3x3 kernel. It can be used to outline edges on an image: +- **Laplacian**: a static edge detection filter that uses a 3x3 kernel. It can be used to outline edges on an image: ```golang - laplasian, format, processingError := brille.LaplasianFilter(file) + laplacian, format, processingError := brille.Laplacian(file) ``` -- **Rotate image (fixed angle)**: rotate an image. Available fixed angeles are 90, 180 and 270 degrees (clockwise): +- **Rotate image (fixed angle)**: rotates an image clockwise (90, 180 or 270 degrees). This filter requires a second argument - rotation angle. Rotation angles are available as `brille` module constants (ROTATE_FIXED_90, ROTATE_FIXED_180 and ROTATE_FIXED_270): ```golang - rotated90, format, processingError := brille.Rotate90(file) - - rotated180, format, processingError := brille.Rotate180(file) - - rotated270, format, processingError := brille.Rotate270(file) + rotated270deg, format, processingError := brille.RotateFixed( + file, + brille.ROTATE_FIXED_270, + ) ``` - **Sepia**: sepia color filter. @@ -217,19 +208,19 @@ Full Fiber example is available at https://github.com/peterdee/filtering-backend sepia, format, processingError := brille.Sepia(file) ``` -- **Sharpen filter**: image sharpening. Requires an ammount to be provided. Effect amount ranges from 0 to 100: +- **Sharpen**: sharpens provided image. Requires an amount to be provided (`uint`, 0 to 100): ```golang - sharpen, format, processingError := brille.SharpenFilter(file, 77) + sharpen, format, processingError := brille.Sharpen(file, 50) ``` -- **Sobel filter**: a static edge detection filter that uses a 3x3 kernel. It can be used to outline edges on an image: +- **Sobel**: a static edge detection filter that uses a 3x3 kernel. It can be used to outline edges on an image: ```golang - sobel, format, processingError := brille.SobelFilter(file) + sobel, format, processingError := brille.Sobel(file) ``` -- **Solarize**: solarization affects image colors, partially inversing the colors. Requires a threshold to be provided. Threshold ranges from 0 to 255: +- **Solarize**: solarization affects image colors, partially inversing the colors. Requires a threshold to be provided (`uint8`, 0 to 255): ```golang solarized, format, processingError := brille.Solarize(file, 99) @@ -237,7 +228,7 @@ Full Fiber example is available at https://github.com/peterdee/filtering-backend ### Environment variables -- `BRILLE_JPEG_QUALITY` (`int`) - controls output quality for JPEG images, should be a number from 0 (low quality) to 100 (highest quality) +- `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. ### License From 17871ac5d5df6537075e1cba170a1a81555b80a1 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 8 Feb 2023 20:09:02 +0300 Subject: [PATCH 5/5] README --- README.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6862ba4..681dfea 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,15 @@ Minimal required Golang version: **v1.18** go get github.com/julyskies/brille@latest ``` +### Versioning + +Brille `v2.0.X` have a number of breaking changes compared to `v1.0.X`: +- some of the filtering functions were removed +- some of the filtering functions were renamed +- some of the constants were renamed + +Check `Available filters` section for more information regarding these changes. + ### Usage All of the Brille filter functions return 3 values: @@ -109,13 +118,13 @@ Full Fiber example is available at https://github.com/peterdee/filtering-backend ### Available filters -- **Binary**: converts an image to 1 bit black and white. Requires a threshold value (`uint8`, 0 to 255): +- **Binary**: converts an image to 1-bit black and white. Requires a threshold value (`uint8`, 0 to 255): ```golang binary, format, processingError := brille.Binary(file, 155) ``` -- **Box blur**: blurs the image. Requires blur amount to be provided. Blur amount is a `uint` value: +- **Box blur**: blurs the image. Requires blur radius to be provided (`uint`, any value): ```golang blurred, format, processingError := brille.BoxBlur(file, 7) @@ -151,7 +160,7 @@ Full Fiber example is available at https://github.com/peterdee/filtering-backend embossed, format, processingError := brille.Emboss(file) ``` -- **Flip**: flips image horizontally or vertically. This filter requires a second argument - flip direction. Flip directions are available as `brille` module constants (FLIP_DIRECTION_HORIZONTAL and FLIP_DIRECTION_VERTICAL): +- **Flip**: flips image horizontally or vertically. This filter requires a second argument - flip direction. Flip directions are available as `brille` module constants (FLIP_DIRECTION_HORIZONTAL is a horizontal mirroring and FLIP_DIRECTION_VERTICAL is vertical mirroring): ```golang flipped, format, processingError := brille.Flip( @@ -181,7 +190,7 @@ Full Fiber example is available at https://github.com/peterdee/filtering-backend rotated, format, processingError := brille.HueRotate(file, 278) ``` -- **Kuwahara**: an edge detection filter with dynamic kernel size. Requires radius to be provided (`uint`, any value). This filter is pretty slow, and will probably be optimized in the future: +- **Kuwahara**: an edge detection filter with dynamic radius. Requires radius to be provided (`uint`, any value). This filter is pretty slow, and will probably be optimized in the future: ```golang kuwahara, format, processingError := brille.Kuwahara(file, 5) @@ -193,7 +202,7 @@ Full Fiber example is available at https://github.com/peterdee/filtering-backend laplacian, format, processingError := brille.Laplacian(file) ``` -- **Rotate image (fixed angle)**: rotates an image clockwise (90, 180 or 270 degrees). This filter requires a second argument - rotation angle. Rotation angles are available as `brille` module constants (ROTATE_FIXED_90, ROTATE_FIXED_180 and ROTATE_FIXED_270): +- **Rotate image (fixed angles)**: rotates an image clockwise (90, 180 or 270 degrees). This filter requires a second argument - rotation angle. Rotation angles are available as `brille` module constants (ROTATE_FIXED_90, ROTATE_FIXED_180 and ROTATE_FIXED_270): ```golang rotated270deg, format, processingError := brille.RotateFixed(