Remove cleanup mode from stitcher

This commit is contained in:
David Vogel 2022-08-11 11:59:53 +02:00
parent f5693b96f1
commit c3f841a4ff
6 changed files with 3 additions and 226 deletions

View File

@ -32,9 +32,9 @@ example list of files:
A downscaling factor. 2 will produce an image with half the side lengths. Defaults to 1. A downscaling factor. 2 will produce an image with half the side lengths. Defaults to 1.
- `input string` - `input string`
The source path of the image tiles to be stitched. Defaults to "./..//..//output") 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". 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". The path to the player-path.json file. This contains the tracked path of the player. Defaults to "./../../output/player-path.json".
- `output string` - `output string`
The path and filename of the resulting stitched image. Defaults to "output.png". 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. Lower bound of the output rectangle. This coordinate is not included in the output.
- `ymin int` - `ymin int`
Upper bound of the output rectangle. This coordinate is included in the output. 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: 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 ./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: To enter the parameters inside of the program:
``` Shell Session ``` Shell Session

View File

@ -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. image image.Image // Either a rectangle or an RGBA image. The bounds of this image are determined by the filename.
imageMutex *sync.RWMutex // imageMutex *sync.RWMutex //
imageUsedFlag bool // Flag signalling, that the image was used recently. 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. // NewImageTile returns an image tile object that represents the image at the given path.

View File

@ -7,12 +7,7 @@ package main
import ( import (
"fmt" "fmt"
"image"
"path/filepath" "path/filepath"
"runtime"
"sync"
"github.com/cheggaaa/pb/v3"
) )
// LoadImageTiles "loads" all images in the directory at the given path. // 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 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
}

View File

@ -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 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 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 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() { func main() {
log.Printf("Noita MapCapture stitching tool v%s", version) log.Printf("Noita MapCapture stitching tool v%s", version)
@ -198,63 +197,6 @@ func main() {
outputRect = image.Rect(xMin, yMin, xMax, yMax) 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. // Query the user, if there were no cmd arguments given.
if flag.NFlag() == 0 { if flag.NFlag() == 0 {
prompt := promptui.Prompt{ prompt := promptui.Prompt{

View File

@ -31,35 +31,6 @@ func getImageFileDimension(imagePath string) (int, int, error) {
return image.Width, image.Height, nil 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) { 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 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++ { for x := divideFloor(rect.Min.X, gridSize); x < divideCeil(rect.Max.X, gridSize); x++ {

2
go.mod
View File

@ -11,7 +11,6 @@ require (
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/tdewolff/canvas v0.0.0-20220627195642-6566432f4b20 github.com/tdewolff/canvas v0.0.0-20220627195642-6566432f4b20
golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75 golang.org/x/exp v0.0.0-20220713135740-79cabaa25d75
golang.org/x/image v0.0.0-20220617043117-41969df76e82
) )
require ( require (
@ -36,6 +35,7 @@ require (
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/tdewolff/minify/v2 v2.11.10 // indirect github.com/tdewolff/minify/v2 v2.11.10 // indirect
github.com/tdewolff/parse/v2 v2.6.0 // 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/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect