Add cleanup mode to stitcher

This commit is contained in:
David Vogel 2019-11-30 18:28:17 +01:00
parent 6703074900
commit 1d832ebad1
4 changed files with 192 additions and 2 deletions

View File

@ -30,7 +30,8 @@ example list of files:
- Or run the program with parameters: - Or run the program with parameters:
- `divide int` - `divide int`
A downscaling factor. 2 will produce an image with half the side lengths. (default 1) 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` - `output string`
The path and filename of the resulting stitched image. (default "output.png") The path and filename of the resulting stitched image. (default "output.png")
- `xmax int` - `xmax int`
@ -43,6 +44,8 @@ example list of files:
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.
- `prerender` - `prerender`
Pre renders the image in RAM before saving. Can speed things up if you have enough RAM. 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: 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 ./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

@ -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. 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.
} }
func (it *imageTile) GetImage() (*image.RGBA, error) { func (it *imageTile) GetImage() (*image.RGBA, error) {

View File

@ -104,7 +104,7 @@ func Stitch(tiles []imageTile, destImage *image.RGBA) error {
return nil 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. // Additionally it runs the workload multithreaded.
func StitchGrid(tiles []imageTile, destImage *image.RGBA, gridSize int, bar *pb.ProgressBar) (errResult error) { func StitchGrid(tiles []imageTile, destImage *image.RGBA, gridSize int, bar *pb.ProgressBar) (errResult error) {
//workloads := gridifyRectangle(destImage.Bounds(), gridSize) //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
}

View File

@ -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 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 flagPrerender = flag.Bool("prerender", false, "Pre renders the image in RAM before saving. Can speed things up if you have enough RAM.") 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() { func main() {
flag.Parse() flag.Parse()
@ -142,6 +143,63 @@ 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{