diff --git a/bin/stitch/imagetiles.go b/bin/stitch/imagetiles.go index ab3189e..6fdfe0f 100644 --- a/bin/stitch/imagetiles.go +++ b/bin/stitch/imagetiles.go @@ -9,9 +9,6 @@ import ( "fmt" "image" "image/color" - "log" - "math" - "math/rand" "path/filepath" "regexp" "runtime" @@ -22,19 +19,6 @@ import ( "github.com/schollz/progressbar/v2" ) -const tileAlignmentSearchRadius = 5 - -type tileAlignment struct { - offset image.Point // Contains the offset of the tile a, so that it aligns pixel perfect with tile b -} - -type tileAlignmentKeys struct { - a, b *imageTile -} - -// tilePairs contains image pairs and their alignment. -type tilePairs map[tileAlignmentKeys]tileAlignment - var regexFileParse = regexp.MustCompile(`^(-?\d+),(-?\d+).png$`) func loadImages(path string, scaleDivider int) ([]imageTile, error) { @@ -80,136 +64,9 @@ func loadImages(path string, scaleDivider int) ([]imageTile, error) { return imageTiles, nil } -// AlignTilePair returns the pixel delta for the first tile, so that it aligns perfectly with the second. -// This function will load images if needed. -func AlignTilePair(tileA, tileB *imageTile, searchRadius int) (image.Point, error) { - imgA, err := tileA.GetImage() - if err != nil { - return image.Point{}, err - } - imgB, err := tileB.GetImage() - if err != nil { - return image.Point{}, err - } - - bestPoint := image.Point{} - bestValue := math.Inf(1) - - for y := -searchRadius; y <= searchRadius; y++ { - for x := -searchRadius; x <= searchRadius; x++ { - point := image.Point{x, y} // Offset of the first image. - - value := getImageDifferenceValue(imgA, imgB, point) - if bestValue > value { - bestValue, bestPoint = value, point - } - } - } - - return bestPoint, nil -} - -func (tp tilePairs) AlignTiles(tiles []*imageTile) error { - - n := len(tiles) - maxOperations, operations := (n-1)*(n)/2, 0 - - // Compare all n tiles with each other. (`(n-1)*(n)/2` comparisons) - for i, tileA := range tiles { - for j := i + 1; j < len(tiles); j++ { - tileB := tiles[j] - - _, ok := tp[tileAlignmentKeys{tileA, tileB}] - if !ok { - // Entry doesn't exist yet. Determine tile pair alignment. - offset, err := AlignTilePair(tileA, tileB, tileAlignmentSearchRadius) - if err != nil { - return fmt.Errorf("Failed to align tile pair %v %v: %w", tileA, tileB, err) - } - - operations++ - log.Printf("(%v/%v)Got alignment for pair %v %v. Offset = %v", operations, maxOperations, tileA, tileB, offset) - - // Store tile alignment pair, also reversed. - tp[tileAlignmentKeys{tileA, tileB}] = tileAlignment{offset: offset} - tp[tileAlignmentKeys{tileB, tileA}] = tileAlignment{offset: offset.Mul(-1)} - - } - } - } - - // Silly and hacky method to determine the minimal error. - // TODO: Use some mixed integer method or something similar to optimize the tile alignment - - // The error function returns the x and y error. The axes are optimized independent of each other later on. - errorFunction := func(tiles []*imageTile) (image.Point, error) { - errorValue := image.Point{} - - for i, tileA := range tiles { - for j := i + 1; j < len(tiles); j++ { - tileB := tiles[j] - - tileAlignment, ok := tp[tileAlignmentKeys{tileA, tileB}] - if !ok { - return image.Point{}, fmt.Errorf("Offset of the tile pair %v %v is missing", tileA, tileB) - } - - // The error is the difference between the needed offset, and the actual offsets - tempErrorValue := pointAbs(tileAlignment.offset.Sub(tileA.offset).Add(tileB.offset)) - - errorValue = errorValue.Add(tempErrorValue) - } - } - return errorValue, nil - } - - errorValue, err := errorFunction(tiles) - if err != nil { - return fmt.Errorf("Failed to calculate error value: %w", err) - } - // Randomly select tiles, and move them in the direction where the error value is lower. - // The "gradient" is basically caluclated by try and error. - for i := 0; i < len(tiles)*tileAlignmentSearchRadius*5; i++ { - tile := tiles[rand.Intn(len(tiles))] - - // Calculate error value for positive shifting. - tile.offset = tile.offset.Add(image.Point{1, 1}) - plusErrorValue, err := errorFunction(tiles) - if err != nil { - return fmt.Errorf("Failed to calculate error value: %w", err) - } - - // Calculate error value for negative shifting. - tile.offset = tile.offset.Add(image.Point{-2, -2}) - minusErrorValue, err := errorFunction(tiles) - if err != nil { - return fmt.Errorf("Failed to calculate error value: %w", err) - } - - // Reset tile movement. - tile.offset = tile.offset.Add(image.Point{1, 1}) - - // Move this tile towards the smaller error value. - if plusErrorValue.X < errorValue.X { - tile.offset = tile.offset.Add(image.Point{1, 0}) - } - if minusErrorValue.X < errorValue.X { - tile.offset = tile.offset.Add(image.Point{-1, 0}) - } - if plusErrorValue.Y < errorValue.Y { - tile.offset = tile.offset.Add(image.Point{0, 1}) - } - if minusErrorValue.Y < errorValue.Y { - tile.offset = tile.offset.Add(image.Point{0, -1}) - } - } - - // TODO: Move images in a way that the majority of images is positioned equal to their original position - - return nil -} - -func (tp tilePairs) Stitch(tiles []imageTile, destImage *image.RGBA) error { +// Stitch takes a list of tiles and stitches them together. +// The destImage shouldn't be too large, or it gets too slow. +func Stitch(tiles []imageTile, destImage *image.RGBA) error { intersectTiles := []*imageTile{} images := []*image.RGBA{} @@ -225,19 +82,12 @@ func (tp tilePairs) Stitch(tiles []imageTile, destImage *image.RGBA) error { } imgCopy := *img imgCopy.Rect = imgCopy.Rect.Add(tile.offset).Inset(4) // Reduce image bounds by 4 pixels on each side, because otherwise there will be artifacts. - images = append(images, &imgCopy) + images = append(images, &imgCopy) // TODO: Fix transparent pixels at the output image border because of Inset } } //log.Printf("intersectTiles: %v", intersectTiles) - // Align those tiles - /*if err := tp.alignTiles(intersectTiles); err != nil { - return fmt.Errorf("Failed to align tiles: %w", err) - }*/ - - // TODO: Add working aligning algorithm - /*for _, intersectTile := range intersectTiles { intersectTile.loadImage() draw.Draw(destImage, destImage.Bounds(), intersectTile.image, destImage.Bounds().Min, draw.Over) @@ -254,7 +104,7 @@ func (tp tilePairs) Stitch(tiles []imageTile, destImage *image.RGBA) error { // StitchGrid calls stitch, but divides the workload into a grid of chunks. // Additionally it runs the workload multithreaded. -func (tp tilePairs) StitchGrid(tiles []imageTile, destImage *image.RGBA, gridSize int) (errResult error) { +func StitchGrid(tiles []imageTile, destImage *image.RGBA, gridSize int) (errResult error) { //workloads := gridifyRectangle(destImage.Bounds(), gridSize) workloads, err := hilbertifyRectangle(destImage.Bounds(), gridSize) if err != nil { @@ -272,7 +122,7 @@ func (tp tilePairs) StitchGrid(tiles []imageTile, destImage *image.RGBA, gridSiz go func() { defer wg.Done() for workload := range wc { - if err := tp.Stitch(tiles, destImage.SubImage(workload).(*image.RGBA)); err != nil { + if err := Stitch(tiles, destImage.SubImage(workload).(*image.RGBA)); err != nil { errResult = err // This will not stop execution, but at least one of any errors is returned. } bar.Add(1) @@ -318,6 +168,7 @@ func drawMedianBlended(images []*image.RGBA, destImage *image.RGBA) { // If there were no images to get data from, ignore the pixel. if !found { + //destImage.SetRGBA(ix, iy, color.RGBA{}) continue } diff --git a/bin/stitch/stitch.go b/bin/stitch/stitch.go index a616ca4..5e81924 100644 --- a/bin/stitch/stitch.go +++ b/bin/stitch/stitch.go @@ -3,8 +3,6 @@ // This software is released under the MIT License. // https://opensource.org/licenses/MIT -// TODO: Fix transparent pixels at the output image border - package main import ( @@ -159,8 +157,7 @@ func main() { outputImage := image.NewRGBA(outputRect) log.Printf("Stitching %v tiles into an image at %v", len(tiles), outputImage.Bounds()) - tp := make(tilePairs) - if err := tp.StitchGrid(tiles, outputImage, 512); err != nil { + if err := StitchGrid(tiles, outputImage, 512); err != nil { log.Panic(err) }