Skip to content

Commit

Permalink
Add cleanup mode to stitcher
Browse files Browse the repository at this point in the history
  • Loading branch information
Dadido3 committed Nov 30, 2019
1 parent 6703074 commit 1d832eb
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 2 deletions.
11 changes: 10 additions & 1 deletion bin/stitch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -43,13 +44,21 @@ 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:

``` Shell Session
./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
Expand Down
2 changes: 2 additions & 0 deletions bin/stitch/imagetile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
123 changes: 122 additions & 1 deletion bin/stitch/imagetiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
58 changes: 58 additions & 0 deletions bin/stitch/stitch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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{
Expand Down

1 comment on commit 1d832eb

@noxifoxi
Copy link

@noxifoxi noxifoxi commented on 1d832eb Dec 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn, you are fast.
I'm so slow in "producing" code...

Please sign in to comment.