diff --git a/imageutils/image_analyzer.go b/imageutils/image_analyzer.go index f126fd0..6ae56ad 100644 --- a/imageutils/image_analyzer.go +++ b/imageutils/image_analyzer.go @@ -16,25 +16,25 @@ func getEnergy( secondPixel color.Color, ) float64 { firstRed, firstGreen, firstBlue, _ := firstPixel.RGBA() - firstRed, firstGreen, firstBlue = firstRed >> 8, firstGreen >> 8, firstBlue >> 8 + firstRed, firstGreen, firstBlue = firstRed>>8, firstGreen>>8, firstBlue>>8 secondRed, secondGreen, secondBlue, _ := secondPixel.RGBA() - secondRed, secondGreen, secondBlue = secondRed >> 8, secondGreen >> 8, secondBlue >> 8 + secondRed, secondGreen, secondBlue = secondRed>>8, secondGreen>>8, secondBlue>>8 return float64( - math.Abs(float64(firstRed - secondRed)) + - math.Abs(float64(firstGreen - secondGreen)) + - math.Abs(float64(firstBlue - secondBlue)), + math.Abs(float64(firstRed-secondRed)) + + math.Abs(float64(firstGreen-secondGreen)) + + math.Abs(float64(firstBlue-secondBlue)), ) } // Calculate a 0-255 grayscale value from a color using the NTSC formula func colorToGray(color color.Color) float64 { red, green, blue, _ := color.RGBA() - red, green, blue = red >> 8, green >> 8, blue >> 8 + red, green, blue = red>>8, green>>8, blue>>8 - return 0.299 * float64(red) + - 0.587 * float64(green) + - 0.114 * float64(blue) + return 0.299*float64(red) + + 0.587*float64(green) + + 0.114*float64(blue) } func sumSlice(slice []float64) float64 { @@ -48,35 +48,35 @@ func sumSlice(slice []float64) float64 { func (image *ImageAnalyzer) CalculateEnergyAt(x int, y int) float64 { northPixel := image.At( x, - max(0, y - 1), + max(0, y-1), ) northEastPixel := image.At( - min(image.Bounds().Max.X, x + 1), - max(0, y - 1), + min(image.Bounds().Max.X, x+1), + max(0, y-1), ) eastPixel := image.At( - min(image.Bounds().Max.X, x + 1), + min(image.Bounds().Max.X, x+1), y, ) southEastPixel := image.At( - min(image.Bounds().Max.X, x + 1), - min(image.Bounds().Max.Y, y + 1), + min(image.Bounds().Max.X, x+1), + min(image.Bounds().Max.Y, y+1), ) southPixel := image.At( x, - min(image.Bounds().Max.Y, y + 1), + min(image.Bounds().Max.Y, y+1), ) southWestPixel := image.At( - max(0, x - 1), - min(image.Bounds().Max.Y, y + 1), + max(0, x-1), + min(image.Bounds().Max.Y, y+1), ) westPixel := image.At( - max(0, x - 1), + max(0, x-1), y, ) northWestPixel := image.At( - max(0, x - 1), - max(0, y - 1), + max(0, x-1), + max(0, y-1), ) thisPixel := image.At( @@ -105,6 +105,6 @@ func (image *ImageAnalyzer) CalculateEnergyAt(x int, y int) float64 { return math.Sqrt( math.Pow(g_x, 2) + - math.Pow(g_y, 2), + math.Pow(g_y, 2), ) } diff --git a/imageutils/seam.go b/imageutils/seam.go new file mode 100644 index 0000000..fb5b56e --- /dev/null +++ b/imageutils/seam.go @@ -0,0 +1,142 @@ +package imageutils + +import ( + "fmt" + "image" + "image/color" +) + +type SeamNode struct { + // if null = root node + PreviosNode *SeamNode + X int + Y int + ThisCost uint + AccumulatedCosts uint +} + +func (s SeamNode) String() string { + if s.PreviosNode == nil { + return fmt.Sprintf("(%d, %d; %d)", s.X, s.Y, s.AccumulatedCosts) + } else { + return fmt.Sprintf("(%d, %d; %d) -> %v", s.X, s.Y, s.AccumulatedCosts, s.PreviosNode) + } +} + +func (s SeamNode) WriteSeamChainToImage(img *image.RGBA) { + img.Set( + s.X, + s.Y, + color.RGBA{255, 0, 0, 1}, + ) + + if s.PreviosNode != nil { + s.PreviosNode.WriteSeamChainToImage(img) + } +} + +type Position struct { + X int + Y int +} + +type ImageSeams struct { + // map of -> SeamNodes + Seams [][]*SeamNode +} + +func NewImageSeams() ImageSeams { + return ImageSeams{ + Seams: make([][]*SeamNode, 0), + } +} + +func (i *ImageSeams) CreateSeamsFromRectangle(rect image.Rectangle) { + for y := range rect.Max.Y { + rowSeams := make([]*SeamNode, 0) + + for x := range rect.Max.X { + rowSeams = append(rowSeams, &SeamNode{ + X: x, + Y: y, + }) + } + + i.Seams = append(i.Seams, rowSeams) + } +} + +// Get the nodes above a given x, y coordinate. +// Get the left and right neighbors, if there are any. +func (i *ImageSeams) GetNodesAbove(x int, y int) []*SeamNode { + if y == 0 { + return nil + } + + rowSeams := i.Seams[y-1] + + seams := make([]*SeamNode, 0, 3) + + // Left node + if x != 0 { + seams = append(seams, rowSeams[x-1]) + } + // Middle node + seams = append(seams, rowSeams[x]) + // Right node + if x < (len(rowSeams) - 1) { + seams = append(seams, rowSeams[x+1]) + } + + return seams +} + +// Find the best node that's above the current one at `(x, y)` +func (i *ImageSeams) FindBestNodeAbove(x int, y int) *SeamNode { + nodes := i.GetNodesAbove(x, y) + lowestCostNode := nodes[0] + + for _, node := range nodes { + if node.AccumulatedCosts < lowestCostNode.AccumulatedCosts { + lowestCostNode = node + } + } + + return lowestCostNode +} + +func (i *ImageSeams) CreateOptimizedRoutesForRow(y int) { + row := i.Seams[y] + + for x, node := range row { + bestNode := i.FindBestNodeAbove(x, y) + + node.PreviosNode = bestNode + node.AccumulatedCosts = bestNode.AccumulatedCosts + node.ThisCost + } +} + +func (i *ImageSeams) CreateOptimizedRoutes() { + for y := 1; y < len(i.Seams); y++ { + i.CreateOptimizedRoutesForRow(y) + } +} + +func (i *ImageSeams) GetLowestSeam() *SeamNode { + lastRow := i.Seams[len(i.Seams)-1] + lowestNode := lastRow[0] + + for _, node := range lastRow { + if node.AccumulatedCosts < lowestNode.AccumulatedCosts { + lowestNode = node + } + } + + return lowestNode +} + +func (i *ImageSeams) SetCostForNode(x int, y int, cost uint) { + node := i.Seams[y][x] + node.ThisCost = cost + node.AccumulatedCosts = cost +} diff --git a/main.go b/main.go index bee338c..ee5a305 100644 --- a/main.go +++ b/main.go @@ -6,12 +6,13 @@ import ( "image/png" _ "image/png" "os" + "myzel394.app/image-stuff/imageutils" ) func main() { // Read image - reader, _ := os.Open("./assets/water2.png") + reader, _ := os.Open("./assets/surfer.png") rawImage, _, _ := image.Decode(reader) readImage := imageutils.ImageAnalyzer{Image: rawImage} @@ -20,16 +21,26 @@ func main() { height := bounds.Max.Y writeImage := image.NewRGBA(bounds) + seams := imageutils.NewImageSeams() + seams.CreateSeamsFromRectangle(bounds) + // Main action for x := range width { for y := range height { - energy := uint8(readImage.CalculateEnergyAt(x, y)) + energy := uint(readImage.CalculateEnergyAt(x, y)) + hexColor := uint8(energy) - color := color.RGBA{energy, energy, energy, 255} + color := color.RGBA{hexColor, hexColor, hexColor, 255} writeImage.Set(x, y, color) + + seams.SetCostForNode(x, y, energy) } } + seams.CreateOptimizedRoutes() + lowestSeam := seams.GetLowestSeam() + lowestSeam.WriteSeamChainToImage(writeImage) + // Out image writer, _ := os.Create("image.png") png.Encode(writer, writeImage)