mirror of
https://github.com/Myzel394/simple-seam-carving-in-go.git
synced 2025-06-18 15:35:30 +02:00
feat: Add optimal seam finding
This commit is contained in:
parent
fbc1acbeee
commit
b71c386950
@ -16,25 +16,25 @@ func getEnergy(
|
|||||||
secondPixel color.Color,
|
secondPixel color.Color,
|
||||||
) float64 {
|
) float64 {
|
||||||
firstRed, firstGreen, firstBlue, _ := firstPixel.RGBA()
|
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, _ := secondPixel.RGBA()
|
||||||
secondRed, secondGreen, secondBlue = secondRed >> 8, secondGreen >> 8, secondBlue >> 8
|
secondRed, secondGreen, secondBlue = secondRed>>8, secondGreen>>8, secondBlue>>8
|
||||||
|
|
||||||
return float64(
|
return float64(
|
||||||
math.Abs(float64(firstRed - secondRed)) +
|
math.Abs(float64(firstRed-secondRed)) +
|
||||||
math.Abs(float64(firstGreen - secondGreen)) +
|
math.Abs(float64(firstGreen-secondGreen)) +
|
||||||
math.Abs(float64(firstBlue - secondBlue)),
|
math.Abs(float64(firstBlue-secondBlue)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate a 0-255 grayscale value from a color using the NTSC formula
|
// Calculate a 0-255 grayscale value from a color using the NTSC formula
|
||||||
func colorToGray(color color.Color) float64 {
|
func colorToGray(color color.Color) float64 {
|
||||||
red, green, blue, _ := color.RGBA()
|
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) +
|
return 0.299*float64(red) +
|
||||||
0.587 * float64(green) +
|
0.587*float64(green) +
|
||||||
0.114 * float64(blue)
|
0.114*float64(blue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sumSlice(slice []float64) float64 {
|
func sumSlice(slice []float64) float64 {
|
||||||
@ -48,35 +48,35 @@ func sumSlice(slice []float64) float64 {
|
|||||||
func (image *ImageAnalyzer) CalculateEnergyAt(x int, y int) float64 {
|
func (image *ImageAnalyzer) CalculateEnergyAt(x int, y int) float64 {
|
||||||
northPixel := image.At(
|
northPixel := image.At(
|
||||||
x,
|
x,
|
||||||
max(0, y - 1),
|
max(0, y-1),
|
||||||
)
|
)
|
||||||
northEastPixel := image.At(
|
northEastPixel := image.At(
|
||||||
min(image.Bounds().Max.X, x + 1),
|
min(image.Bounds().Max.X, x+1),
|
||||||
max(0, y - 1),
|
max(0, y-1),
|
||||||
)
|
)
|
||||||
eastPixel := image.At(
|
eastPixel := image.At(
|
||||||
min(image.Bounds().Max.X, x + 1),
|
min(image.Bounds().Max.X, x+1),
|
||||||
y,
|
y,
|
||||||
)
|
)
|
||||||
southEastPixel := image.At(
|
southEastPixel := image.At(
|
||||||
min(image.Bounds().Max.X, x + 1),
|
min(image.Bounds().Max.X, x+1),
|
||||||
min(image.Bounds().Max.Y, y + 1),
|
min(image.Bounds().Max.Y, y+1),
|
||||||
)
|
)
|
||||||
southPixel := image.At(
|
southPixel := image.At(
|
||||||
x,
|
x,
|
||||||
min(image.Bounds().Max.Y, y + 1),
|
min(image.Bounds().Max.Y, y+1),
|
||||||
)
|
)
|
||||||
southWestPixel := image.At(
|
southWestPixel := image.At(
|
||||||
max(0, x - 1),
|
max(0, x-1),
|
||||||
min(image.Bounds().Max.Y, y + 1),
|
min(image.Bounds().Max.Y, y+1),
|
||||||
)
|
)
|
||||||
westPixel := image.At(
|
westPixel := image.At(
|
||||||
max(0, x - 1),
|
max(0, x-1),
|
||||||
y,
|
y,
|
||||||
)
|
)
|
||||||
northWestPixel := image.At(
|
northWestPixel := image.At(
|
||||||
max(0, x - 1),
|
max(0, x-1),
|
||||||
max(0, y - 1),
|
max(0, y-1),
|
||||||
)
|
)
|
||||||
|
|
||||||
thisPixel := image.At(
|
thisPixel := image.At(
|
||||||
|
142
imageutils/seam.go
Normal file
142
imageutils/seam.go
Normal file
@ -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 <y row> -> 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
|
||||||
|
}
|
17
main.go
17
main.go
@ -6,12 +6,13 @@ import (
|
|||||||
"image/png"
|
"image/png"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"myzel394.app/image-stuff/imageutils"
|
"myzel394.app/image-stuff/imageutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Read image
|
// Read image
|
||||||
reader, _ := os.Open("./assets/water2.png")
|
reader, _ := os.Open("./assets/surfer.png")
|
||||||
rawImage, _, _ := image.Decode(reader)
|
rawImage, _, _ := image.Decode(reader)
|
||||||
readImage := imageutils.ImageAnalyzer{Image: rawImage}
|
readImage := imageutils.ImageAnalyzer{Image: rawImage}
|
||||||
|
|
||||||
@ -20,16 +21,26 @@ func main() {
|
|||||||
height := bounds.Max.Y
|
height := bounds.Max.Y
|
||||||
writeImage := image.NewRGBA(bounds)
|
writeImage := image.NewRGBA(bounds)
|
||||||
|
|
||||||
|
seams := imageutils.NewImageSeams()
|
||||||
|
seams.CreateSeamsFromRectangle(bounds)
|
||||||
|
|
||||||
// Main action
|
// Main action
|
||||||
for x := range width {
|
for x := range width {
|
||||||
for y := range height {
|
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)
|
writeImage.Set(x, y, color)
|
||||||
|
|
||||||
|
seams.SetCostForNode(x, y, energy)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seams.CreateOptimizedRoutes()
|
||||||
|
lowestSeam := seams.GetLowestSeam()
|
||||||
|
lowestSeam.WriteSeamChainToImage(writeImage)
|
||||||
|
|
||||||
// Out image
|
// Out image
|
||||||
writer, _ := os.Create("image.png")
|
writer, _ := os.Create("image.png")
|
||||||
png.Encode(writer, writeImage)
|
png.Encode(writer, writeImage)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user