2019-10-21 00:07:39 +00:00
|
|
|
// Copyright (c) 2019 David Vogel
|
|
|
|
//
|
|
|
|
// This software is released under the MIT License.
|
|
|
|
// https://opensource.org/licenses/MIT
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-10-25 17:45:23 +00:00
|
|
|
"flag"
|
|
|
|
"fmt"
|
2019-10-23 01:28:37 +00:00
|
|
|
"image"
|
|
|
|
"image/png"
|
2019-10-21 00:07:39 +00:00
|
|
|
"log"
|
2019-10-23 01:28:37 +00:00
|
|
|
"os"
|
2019-10-21 00:07:39 +00:00
|
|
|
"path/filepath"
|
2019-11-05 01:31:19 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
2019-10-25 17:45:23 +00:00
|
|
|
|
2019-11-05 01:31:19 +00:00
|
|
|
"github.com/cheggaaa/pb/v3"
|
2019-10-25 17:45:23 +00:00
|
|
|
"github.com/manifoldco/promptui"
|
2019-10-21 00:07:39 +00:00
|
|
|
)
|
|
|
|
|
2019-10-25 17:45:23 +00:00
|
|
|
var flagInputPath = flag.String("input", filepath.Join(".", "..", "..", "output"), "The source path of the image tiles to be stitched.")
|
|
|
|
var flagOutputPath = flag.String("output", filepath.Join(".", "output.png"), "The path and filename of the resulting stitched image.")
|
2019-11-01 01:40:21 +00:00
|
|
|
var flagScaleDivider = flag.Int("divide", 1, "A downscaling factor. 2 will produce an image with half the side lengths.")
|
2019-10-25 17:45:23 +00:00
|
|
|
var flagXMin = flag.Int("xmin", 0, "Left 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 flagYMax = flag.Int("ymax", 0, "Lower bound of the output rectangle. This coordinate is not included in the output.")
|
2019-11-05 01:31:19 +00:00
|
|
|
var flagPrerender = flag.Bool("prerender", false, "Pre renders the image in RAM before saving. Can speed things up if you have enough RAM.")
|
2019-10-21 00:07:39 +00:00
|
|
|
|
|
|
|
func main() {
|
2019-10-25 17:45:23 +00:00
|
|
|
flag.Parse()
|
|
|
|
|
|
|
|
// Query the user, if there were no cmd arguments given
|
|
|
|
if flag.NFlag() == 0 {
|
|
|
|
prompt := promptui.Prompt{
|
|
|
|
Label: "Enter downscaling factor:",
|
|
|
|
Default: fmt.Sprint(*flagScaleDivider),
|
|
|
|
AllowEdit: true,
|
|
|
|
Validate: func(s string) error {
|
|
|
|
var num int
|
|
|
|
_, err := fmt.Sscanf(s, "%d", &num)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if int(num) < 1 {
|
|
|
|
return fmt.Errorf("Number must be larger than 0")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
result, err := prompt.Run()
|
|
|
|
if err != nil {
|
|
|
|
log.Panicf("Error while getting user input: %v", err)
|
|
|
|
}
|
|
|
|
fmt.Sscanf(result, "%d", flagScaleDivider)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Query the user, if there were no cmd arguments given
|
|
|
|
if flag.NFlag() == 0 {
|
|
|
|
prompt := promptui.Prompt{
|
|
|
|
Label: "Enter input path:",
|
|
|
|
Default: *flagInputPath,
|
|
|
|
AllowEdit: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
result, err := prompt.Run()
|
|
|
|
if err != nil {
|
|
|
|
log.Panicf("Error while getting user input: %v", err)
|
|
|
|
}
|
|
|
|
*flagInputPath = result
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Printf("Starting to read tile information at \"%v\"", *flagInputPath)
|
|
|
|
tiles, err := loadImages(*flagInputPath, *flagScaleDivider)
|
2019-10-21 00:07:39 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
2019-10-25 17:45:23 +00:00
|
|
|
if len(tiles) == 0 {
|
|
|
|
log.Panicf("Got no tiles inside of %v", *flagInputPath)
|
|
|
|
}
|
2019-10-23 22:28:22 +00:00
|
|
|
log.Printf("Got %v tiles", len(tiles))
|
2019-10-21 00:07:39 +00:00
|
|
|
|
2019-10-24 19:11:23 +00:00
|
|
|
totalBounds := image.Rectangle{}
|
|
|
|
for i, tile := range tiles {
|
|
|
|
if i == 0 {
|
|
|
|
totalBounds = tile.Bounds()
|
|
|
|
} else {
|
|
|
|
totalBounds = totalBounds.Union(tile.Bounds())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
log.Printf("Total size of the possible output space is %v", totalBounds)
|
|
|
|
|
2019-10-24 01:05:42 +00:00
|
|
|
/*profFile, err := os.Create("cpu.prof")
|
2019-10-23 01:28:37 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Panicf("could not create CPU profile: %v", err)
|
|
|
|
}
|
2019-10-24 01:05:42 +00:00
|
|
|
defer profFile.Close()
|
|
|
|
if err := pprof.StartCPUProfile(profFile); err != nil {
|
2019-10-23 01:28:37 +00:00
|
|
|
log.Panicf("could not start CPU profile: %v", err)
|
|
|
|
}
|
|
|
|
defer pprof.StopCPUProfile()*/
|
|
|
|
|
2019-10-25 17:45:23 +00:00
|
|
|
// If the output rect is empty, use the rectangle that encloses all tiles
|
|
|
|
outputRect := image.Rect(*flagXMin, *flagYMin, *flagXMax, *flagYMax)
|
|
|
|
if outputRect.Empty() {
|
|
|
|
outputRect = totalBounds
|
|
|
|
}
|
|
|
|
|
|
|
|
// Query the user, if there were no cmd arguments given
|
|
|
|
if flag.NFlag() == 0 {
|
|
|
|
prompt := promptui.Prompt{
|
|
|
|
Label: "Enter output rectangle (xMin,yMin;xMax,yMax):",
|
|
|
|
Default: fmt.Sprintf("%d,%d;%d,%d", outputRect.Min.X, outputRect.Min.Y, outputRect.Max.X, outputRect.Max.Y),
|
|
|
|
AllowEdit: true,
|
|
|
|
Validate: func(s string) error {
|
|
|
|
var xMin, yMin, xMax, yMax int
|
|
|
|
_, err := fmt.Sscanf(s, "%d,%d;%d,%d", &xMin, &yMin, &xMax, &yMax)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
rect := image.Rect(xMin, yMin, xMax, yMax)
|
|
|
|
if rect.Empty() {
|
|
|
|
return fmt.Errorf("Rectangle must not be empty")
|
|
|
|
}
|
|
|
|
|
|
|
|
outputRect = rect
|
2019-10-24 01:05:42 +00:00
|
|
|
|
2019-10-25 17:45:23 +00:00
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
result, err := prompt.Run()
|
|
|
|
if err != nil {
|
|
|
|
log.Panicf("Error while getting user input: %v", err)
|
|
|
|
}
|
|
|
|
var xMin, yMin, xMax, yMax int
|
|
|
|
fmt.Sscanf(result, "%d,%d;%d,%d", &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 {
|
|
|
|
prompt := promptui.Prompt{
|
|
|
|
Label: "Enter output filename and path:",
|
|
|
|
Default: *flagOutputPath,
|
|
|
|
AllowEdit: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
result, err := prompt.Run()
|
|
|
|
if err != nil {
|
|
|
|
log.Panicf("Error while getting user input: %v", err)
|
|
|
|
}
|
|
|
|
*flagOutputPath = result
|
|
|
|
}
|
2019-10-23 01:28:37 +00:00
|
|
|
|
2019-11-04 21:44:35 +00:00
|
|
|
var outputImage image.Image
|
2019-11-05 01:31:19 +00:00
|
|
|
bar := pb.Full.New(0)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
done := make(chan bool)
|
|
|
|
|
|
|
|
if *flagPrerender {
|
2019-11-04 21:44:35 +00:00
|
|
|
log.Printf("Creating output image with a size of %v", outputRect.Size())
|
|
|
|
tempImage := image.NewRGBA(outputRect)
|
2019-10-23 22:28:22 +00:00
|
|
|
|
2019-11-05 01:31:19 +00:00
|
|
|
log.Printf("Stitching %v tiles into an image at %v", len(tiles), tempImage.Bounds())
|
|
|
|
if err := StitchGrid(tiles, tempImage, 512, bar); err != nil {
|
2019-11-04 21:44:35 +00:00
|
|
|
log.Panic(err)
|
|
|
|
}
|
2019-11-05 01:31:19 +00:00
|
|
|
bar.Finish()
|
|
|
|
|
|
|
|
outputImage = tempImage
|
|
|
|
} else {
|
|
|
|
tempImage := NewMedianBlendedImage(tiles, outputRect)
|
|
|
|
_, max := tempImage.Progress()
|
|
|
|
bar.SetTotal(int64(max)).Start().SetRefreshRate(1 * time.Second)
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
|
|
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-done:
|
|
|
|
value, _ := tempImage.Progress()
|
|
|
|
bar.SetCurrent(int64(value))
|
|
|
|
bar.Finish()
|
|
|
|
return
|
|
|
|
case <-ticker.C:
|
|
|
|
value, _ := tempImage.Progress()
|
|
|
|
bar.SetCurrent(int64(value))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
2019-11-04 21:44:35 +00:00
|
|
|
|
|
|
|
outputImage = tempImage
|
2019-10-23 01:28:37 +00:00
|
|
|
}
|
|
|
|
|
2019-10-23 22:28:22 +00:00
|
|
|
log.Printf("Creating output file \"%v\"", "output.png")
|
2019-10-23 01:28:37 +00:00
|
|
|
f, err := os.Create("output.png")
|
|
|
|
if err != nil {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := png.Encode(f, outputImage); err != nil {
|
|
|
|
f.Close()
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
|
|
|
|
2019-11-05 01:31:19 +00:00
|
|
|
if !*flagPrerender {
|
|
|
|
done <- true
|
|
|
|
wg.Wait()
|
|
|
|
}
|
|
|
|
|
2019-10-23 01:28:37 +00:00
|
|
|
if err := f.Close(); err != nil {
|
|
|
|
log.Panic(err)
|
2019-10-21 00:07:39 +00:00
|
|
|
}
|
2019-10-23 22:28:22 +00:00
|
|
|
log.Printf("Created output file \"%v\"", "output.png")
|
|
|
|
|
2019-10-21 00:07:39 +00:00
|
|
|
}
|