diff --git a/bin/stitch/README.md b/bin/stitch/README.md index 44c591e..5836f12 100644 --- a/bin/stitch/README.md +++ b/bin/stitch/README.md @@ -30,7 +30,8 @@ example list of files: - Or run the program with parameters: - `divide int` A downscaling factor. 2 will produce an image with half the side lengths. (default 1) - - `input string`The source path of the image tiles to be stitched. (default "..\\..\\output") + - `input string` + The source path of the image tiles to be stitched. (default "..\\..\\output") - `output string` The path and filename of the resulting stitched image. (default "output.png") - `xmax int` @@ -43,6 +44,8 @@ example list of files: Upper bound of the output rectangle. This coordinate is included in the output. - `prerender` Pre renders the image in RAM before saving. Can speed things up if you have enough RAM. + - `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: @@ -50,6 +53,12 @@ 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/imagetile.go b/bin/stitch/imagetile.go index 8593a05..2cf474c 100644 --- a/bin/stitch/imagetile.go +++ b/bin/stitch/imagetile.go @@ -26,6 +26,8 @@ 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. } func (it *imageTile) GetImage() (*image.RGBA, error) { diff --git a/bin/stitch/imagetiles.go b/bin/stitch/imagetiles.go index 9fb379f..7f1376e 100644 --- a/bin/stitch/imagetiles.go +++ b/bin/stitch/imagetiles.go @@ -104,7 +104,7 @@ func Stitch(tiles []imageTile, destImage *image.RGBA) error { return nil } -// StitchGrid calls stitch, but divides the workload into a grid of chunks. +// StitchGrid calls Stitch, but divides the workload into a grid of chunks. // Additionally it runs the workload multithreaded. func StitchGrid(tiles []imageTile, destImage *image.RGBA, gridSize int, bar *pb.ProgressBar) (errResult error) { //workloads := gridifyRectangle(destImage.Bounds(), gridSize) @@ -207,3 +207,124 @@ func drawMedianBlended(images []*image.RGBA, destImage *image.RGBA) { } } } + +// 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.OffsetBounds().Overlaps(bounds) { + tilePtr := &tiles[i] + intersectTiles = append(intersectTiles, tilePtr) + img, err := tilePtr.GetImage() + if err != nil { + return fmt.Errorf("Couldn't get image: %w", err) + } + 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) + } + } + + 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/stitch.go b/bin/stitch/stitch.go index 5be0e01..8b7eacb 100644 --- a/bin/stitch/stitch.go +++ b/bin/stitch/stitch.go @@ -28,6 +28,7 @@ var flagYMin = flag.Int("ymin", 0, "Upper bound of the output rectangle. This co 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 flagPrerender = flag.Bool("prerender", false, "Pre renders the image in RAM before saving. Can speed things up if you have enough RAM.") +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() { flag.Parse() @@ -142,6 +143,63 @@ 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{