diff --git a/bin/stitch/README.md b/bin/stitch/README.md index 3dd097e..44f1256 100644 --- a/bin/stitch/README.md +++ b/bin/stitch/README.md @@ -32,9 +32,9 @@ example list of files: A downscaling factor. 2 will produce an image with half the side lengths. Defaults to 1. - `input string` The source path of the image tiles to be stitched. Defaults to "./..//..//output") - - `entities` + - `entities string` The path to the `entities.json` file. This contains Noita specific entity data. Defaults to "./../../output/entities.json". - - `player-path` + - `player-path string` The path to the player-path.json file. This contains the tracked path of the player. Defaults to "./../../output/player-path.json". - `output string` The path and filename of the resulting stitched image. Defaults to "output.png". @@ -46,8 +46,6 @@ example list of files: Lower bound of the output rectangle. This coordinate is not included in the output. - `ymin int` Upper bound of the output rectangle. This coordinate is included in the output. - - `cleanup float` - Enables cleanup mode with the given float as threshold. This will **DELETE** images from the input folder; no stitching will be done in this mode. A good value to start with is `0.999`, which deletes images where the sum of the min-max difference of each sub-pixel overlapping with other images is less than 99.9%% of the maximum possible sum of pixel differences. To output the 100x100 area that is centered at the origin use: @@ -55,12 +53,6 @@ To output the 100x100 area that is centered at the origin use: ./stitch -divide 1 -xmin -50 -xmax 50 -ymin -50 -ymax 50 ``` -To remove images that would cause artifacts (You should recapture the deleted images afterwards): - -``` Shell Session -./stitch -cleanup 0.999 -``` - To enter the parameters inside of the program: ``` Shell Session diff --git a/bin/stitch/image-tile.go b/bin/stitch/image-tile.go index 90586af..a132c51 100644 --- a/bin/stitch/image-tile.go +++ b/bin/stitch/image-tile.go @@ -31,8 +31,6 @@ type ImageTile struct { image image.Image // Either a rectangle or an RGBA image. The bounds of this image are determined by the filename. imageMutex *sync.RWMutex // imageUsedFlag bool // Flag signalling, that the image was used recently. - - pixelErrorSum uint64 // Sum of the difference between the (sub)pixels of all overlapping images. 0 Means that all overlapping images are identical. } // NewImageTile returns an image tile object that represents the image at the given path. diff --git a/bin/stitch/image-tiles.go b/bin/stitch/image-tiles.go index 77ce761..c8dcb31 100644 --- a/bin/stitch/image-tiles.go +++ b/bin/stitch/image-tiles.go @@ -7,12 +7,7 @@ package main import ( "fmt" - "image" "path/filepath" - "runtime" - "sync" - - "github.com/cheggaaa/pb/v3" ) // LoadImageTiles "loads" all images in the directory at the given path. @@ -39,124 +34,3 @@ func LoadImageTiles(path string, scaleDivider int) ([]ImageTile, error) { return imageTiles, nil } - -// Compare takes a list of tiles and compares them pixel by pixel. -// The resulting pixel difference sum is stored in each tile. -func Compare(tiles []ImageTile, bounds image.Rectangle) error { - intersectTiles := []*ImageTile{} - images := []*image.RGBA{} - - // Get only the tiles that intersect with the bounds. - // Ignore alignment here, doesn't matter if an image overlaps a few pixels anyways. - for i, tile := range tiles { - if tile.Bounds().Overlaps(bounds) { - tilePtr := &tiles[i] - img := tilePtr.GetImage() - if img == nil { - continue - } - intersectTiles = append(intersectTiles, tilePtr) - imgCopy := *img - //imgCopy.Rect = imgCopy.Rect - images = append(images, &imgCopy) - } - } - - tempTilesEmpty := make([]*ImageTile, 0, len(intersectTiles)) - - for iy := bounds.Min.Y; iy < bounds.Max.Y; iy++ { - for ix := bounds.Min.X; ix < bounds.Max.X; ix++ { - var rMin, rMax, gMin, gMax, bMin, bMax uint8 - point := image.Point{ix, iy} - found := false - tempTiles := tempTilesEmpty - - // Iterate through all images and find min and max subpixel values. - for i, img := range images { - if point.In(img.Bounds()) { - tempTiles = append(tempTiles, intersectTiles[i]) - col := img.RGBAAt(point.X, point.Y) - if !found { - found = true - rMin, rMax, gMin, gMax, bMin, bMax = col.R, col.R, col.G, col.G, col.B, col.B - } else { - if rMin > col.R { - rMin = col.R - } - if rMax < col.R { - rMax = col.R - } - if gMin > col.G { - gMin = col.G - } - if gMax < col.G { - gMax = col.G - } - if bMin > col.B { - bMin = col.B - } - if bMax < col.B { - bMax = col.B - } - } - } - } - - // If there were no images to get data from, ignore the pixel. - if !found { - continue - } - - // Write the error value back into the tiles (Only those that contain the point point) - for _, tile := range tempTiles { - tile.pixelErrorSum += uint64(rMax-rMin) + uint64(gMax-gMin) + uint64(bMax-bMin) - } - - } - } - - return nil -} - -// CompareGrid calls Compare, but divides the workload into a grid of chunks. -// Additionally it runs the workload multithreaded. -func CompareGrid(tiles []ImageTile, bounds image.Rectangle, gridSize int, bar *pb.ProgressBar) (errResult error) { - //workloads := gridifyRectangle(destImage.Bounds(), gridSize) - workloads, err := hilbertifyRectangle(bounds, gridSize) - if err != nil { - return err - } - - if bar != nil { - bar.SetTotal(int64(len(workloads))).Start() - } - - // Start worker threads - wc := make(chan image.Rectangle) - wg := sync.WaitGroup{} - for i := 0; i < runtime.NumCPU()*2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for workload := range wc { - if err := Compare(tiles, workload); err != nil { - errResult = err // This will not stop execution, but at least one of any errors is returned. - } - if bar != nil { - bar.Increment() - } - } - }() - } - - // Push workload to worker threads - for _, workload := range workloads { - wc <- workload - } - - // Wait until all worker threads are done - close(wc) - wg.Wait() - - return -} diff --git a/bin/stitch/main.go b/bin/stitch/main.go index e3ea395..a125902 100644 --- a/bin/stitch/main.go +++ b/bin/stitch/main.go @@ -29,7 +29,6 @@ var flagXMin = flag.Int("xmin", 0, "Left bound of the output rectangle. This coo var flagYMin = flag.Int("ymin", 0, "Upper bound of the output rectangle. This coordinate is included in the output.") var flagXMax = flag.Int("xmax", 0, "Right bound of the output rectangle. This coordinate is not included in the output.") var flagYMax = flag.Int("ymax", 0, "Lower bound of the output rectangle. This coordinate is not included in the output.") -var flagCleanupThreshold = flag.Float64("cleanup", 0, "Enable cleanup mode with the given threshold. This will DELETE images from the input folder, no stitching will be done in this mode. A good value to start with is 0.999, which deletes images where the sum of the min-max difference of each sub-pixel overlapping with other images is less than 99.9%% of the maximum possible sum of pixel differences.") func main() { log.Printf("Noita MapCapture stitching tool v%s", version) @@ -198,63 +197,6 @@ func main() { outputRect = image.Rect(xMin, yMin, xMax, yMax) } - // Query the user, if there were no cmd arguments given. - /*if flag.NFlag() == 0 { - fmt.Println("\nYou can now define a cleanup threshold. This mode will DELETE input images based on their similarity with other overlapping input images. The range is from 0, where no images are deleted, to 1 where all images will be deleted. A good value to get rid of most artifacts is 0.999. If you enter a threshold above 0, the program will not stitch, but DELETE some of your input images. If you want to stitch, enter 0.") - prompt := promptui.Prompt{ - Label: "Enter cleanup threshold:", - Default: strconv.FormatFloat(*flagCleanupThreshold, 'f', -1, 64), - AllowEdit: true, - Validate: func(s string) error { - result, err := strconv.ParseFloat(s, 64) - if err != nil { - return err - } - - if result < 0 || result > 1 { - return fmt.Errorf("Number %v outside of valid range [0;1]", result) - } - - return nil - }, - } - - result, err := prompt.Run() - if err != nil { - log.Panicf("Error while getting user input: %v", err) - } - *flagCleanupThreshold, err = strconv.ParseFloat(result, 64) - if err != nil { - log.Panicf("Error while parsing user input: %v", err) - } - }*/ - - if *flagCleanupThreshold < 0 || *flagCleanupThreshold > 1 { - log.Panicf("Cleanup threshold (%v) outside of valid range [0;1]", *flagCleanupThreshold) - } - if *flagCleanupThreshold > 0 { - bar := pb.Full.New(0) - - log.Printf("Cleaning up %v tiles at %v", len(tiles), outputRect) - if err := CompareGrid(tiles, outputRect, 512, bar); err != nil { - log.Panic(err) - } - bar.Finish() - - for _, tile := range tiles { - pixelErrorSumNormalized := float64(tile.pixelErrorSum) / float64(tile.Bounds().Size().X*tile.Bounds().Size().Y*3*255) - if 1-pixelErrorSumNormalized <= *flagCleanupThreshold { - os.Remove(tile.fileName) - log.Printf("Tile %v has matching factor of %f. Deleted file!", &tile, 1-pixelErrorSumNormalized) - } else { - log.Printf("Tile %v has matching factor of %f", &tile, 1-pixelErrorSumNormalized) - } - - } - - return - } - // Query the user, if there were no cmd arguments given. if flag.NFlag() == 0 { prompt := promptui.Prompt{ diff --git a/bin/stitch/util.go b/bin/stitch/util.go index 7a6e5fd..8632e60 100644 --- a/bin/stitch/util.go +++ b/bin/stitch/util.go @@ -31,35 +31,6 @@ func getImageFileDimension(imagePath string) (int, int, error) { return image.Width, image.Height, nil } -// getImageDifferenceValue returns the average quadratic difference of the (sub)pixels. -// 0 means the images are identical, +inf means that the images don't intersect. -func getImageDifferenceValue(a, b *image.RGBA, offsetA image.Point) float64 { - intersection := a.Bounds().Add(offsetA).Intersect(b.Bounds()) - - if intersection.Empty() { - return math.Inf(1) - } - - aSub := a.SubImage(intersection.Sub(offsetA)).(*image.RGBA) - bSub := b.SubImage(intersection).(*image.RGBA) - - intersectionWidth := intersection.Dx() * 4 - intersectionHeight := intersection.Dy() - - var value int64 - - for iy := 0; iy < intersectionHeight; iy++ { - aSlice := aSub.Pix[iy*aSub.Stride : iy*aSub.Stride+intersectionWidth] - bSlice := bSub.Pix[iy*bSub.Stride : iy*bSub.Stride+intersectionWidth] - for ix := 0; ix < intersectionWidth; ix += 3 { - diff := int64(aSlice[ix]) - int64(bSlice[ix]) - value += diff * diff - } - } - - return float64(value) / float64(intersectionWidth*intersectionHeight) -} - func gridifyRectangle(rect image.Rectangle, gridSize int) (result []image.Rectangle) { for y := divideFloor(rect.Min.Y, gridSize); y < divideCeil(rect.Max.Y, gridSize); y++ { for x := divideFloor(rect.Min.X, gridSize); x < divideCeil(rect.Max.X, gridSize); x++ { diff --git a/go.mod b/go.mod index b940e9b..0128dc4 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/tdewolff/canvas v0.0.0-20220627195642-6566432f4b20 golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75 - golang.org/x/image v0.0.0-20220617043117-41969df76e82 ) require ( @@ -36,6 +35,7 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/tdewolff/minify/v2 v2.11.10 // indirect github.com/tdewolff/parse/v2 v2.6.0 // indirect + golang.org/x/image v0.0.0-20220617043117-41969df76e82 // indirect golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect golang.org/x/text v0.3.7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect