2022-07-16 15:29:26 +00:00
// Copyright (c) 2019-2022 David Vogel
2019-10-21 00:07:39 +00:00
//
// 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
2022-07-30 10:23:40 +00:00
"github.com/1lann/promptui"
2019-11-05 01:31:19 +00:00
"github.com/cheggaaa/pb/v3"
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." )
2022-08-10 19:04:17 +00:00
var flagEntitiesInputPath = flag . String ( "entities" , filepath . Join ( "." , ".." , ".." , "output" , "entities.json" ) , "The path to the entities.json file." )
var flagPlayerPathInputPath = flag . String ( "player-path" , filepath . Join ( "." , ".." , ".." , "output" , "player-path.json" ) , "The path to the player-path.json file." )
2019-10-25 17:45:23 +00:00
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-30 17:28:17 +00:00
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." )
2019-10-21 00:07:39 +00:00
func main ( ) {
2022-07-16 21:22:18 +00:00
log . Printf ( "Noita MapCapture stitching tool v%s" , version )
2019-10-25 17:45:23 +00:00
flag . Parse ( )
2022-08-11 09:10:07 +00:00
var overlays [ ] StitchedImageOverlay
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
2019-10-25 17:45:23 +00:00
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 {
2022-07-16 15:29:26 +00:00
return fmt . Errorf ( "number must be larger than 0" )
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 )
}
fmt . Sscanf ( result , "%d" , flagScaleDivider )
}
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
2019-10-25 17:45:23 +00:00
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
}
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
if flag . NFlag ( ) == 0 {
prompt := promptui . Prompt {
Label : "Enter \"entities.json\" path:" ,
Default : * flagEntitiesInputPath ,
AllowEdit : true ,
}
result , err := prompt . Run ( )
if err != nil {
log . Panicf ( "Error while getting user input: %v" , err )
}
* flagEntitiesInputPath = result
}
// Load entities if requested.
2022-08-11 09:10:07 +00:00
entities , err := LoadEntities ( * flagEntitiesInputPath )
2022-08-08 21:05:58 +00:00
if err != nil {
log . Printf ( "Failed to load entities: %v" , err )
}
if len ( entities ) > 0 {
log . Printf ( "Got %v entities." , len ( entities ) )
2022-08-11 09:10:07 +00:00
overlays = append ( overlays , entities ) // Add entities to overlay drawing list.
2022-08-08 21:05:58 +00:00
}
2022-08-10 18:41:57 +00:00
// Query the user, if there were no cmd arguments given.
if flag . NFlag ( ) == 0 {
prompt := promptui . Prompt {
Label : "Enter \"player-path.json\" path:" ,
Default : * flagPlayerPathInputPath ,
AllowEdit : true ,
}
result , err := prompt . Run ( )
if err != nil {
log . Panicf ( "Error while getting user input: %v" , err )
}
* flagPlayerPathInputPath = result
}
// Load player path if requested.
2022-08-11 09:10:07 +00:00
playerPath , err := LoadPlayerPath ( * flagPlayerPathInputPath )
2022-08-10 18:41:57 +00:00
if err != nil {
log . Printf ( "Failed to load player path: %v" , err )
}
2022-08-11 09:10:07 +00:00
if len ( playerPath ) > 0 {
log . Printf ( "Got %v player path entries." , len ( playerPath ) )
overlays = append ( overlays , playerPath ) // Add player path to overlay drawing list.
2022-08-10 18:41:57 +00:00
}
2019-10-25 17:45:23 +00:00
log . Printf ( "Starting to read tile information at \"%v\"" , * flagInputPath )
2022-08-11 09:10:07 +00:00
tiles , err := LoadImageTiles ( * 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 ( ) * /
2022-08-08 21:05:58 +00:00
// If the output rect is empty, use the rectangle that encloses all tiles.
2019-10-25 17:45:23 +00:00
outputRect := image . Rect ( * flagXMin , * flagYMin , * flagXMax , * flagYMax )
if outputRect . Empty ( ) {
outputRect = totalBounds
}
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
2019-10-25 17:45:23 +00:00
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 ( ) {
2022-07-16 15:29:26 +00:00
return fmt . Errorf ( "rectangle must not be empty" )
2019-10-25 17:45:23 +00:00
}
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 )
}
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
2019-11-30 17:28:17 +00:00
/ * 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
}
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
2019-10-25 17:45:23 +00:00
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-05 01:31:19 +00:00
bar := pb . Full . New ( 0 )
var wg sync . WaitGroup
2022-08-11 09:10:07 +00:00
done := make ( chan struct { } )
2019-11-05 01:31:19 +00:00
2022-08-11 09:47:18 +00:00
blendMethod := BlendMethodMedian {
LimitToNew : 1 , // Limit median blending to the n newest tiles by file modification time.
}
outputImage , err := NewStitchedImage ( tiles , outputRect , blendMethod , 512 , overlays )
2022-08-11 09:10:07 +00:00
if err != nil {
log . Panicf ( "NewStitchedImage() failed: %v" , err )
}
_ , max := outputImage . Progress ( )
2022-08-10 19:04:17 +00:00
bar . SetTotal ( int64 ( max ) ) . Start ( ) . SetRefreshRate ( 1 * time . Second )
2022-08-11 09:10:07 +00:00
// Query progress and draw progress bar.
2022-08-10 19:04:17 +00:00
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
ticker := time . NewTicker ( 1 * time . Second )
for {
select {
case <- done :
2022-08-11 09:10:07 +00:00
value , _ := outputImage . Progress ( )
2022-08-10 19:04:17 +00:00
bar . SetCurrent ( int64 ( value ) )
bar . Finish ( )
return
case <- ticker . C :
2022-08-11 09:10:07 +00:00
value , _ := outputImage . Progress ( )
2022-08-10 19:04:17 +00:00
bar . SetCurrent ( int64 ( value ) )
2019-11-05 01:31:19 +00:00
}
2022-08-10 19:04:17 +00:00
}
} ( )
2019-11-04 21:44:35 +00:00
2019-12-21 21:25:47 +00:00
log . Printf ( "Creating output file \"%v\"" , * flagOutputPath )
f , err := os . Create ( * flagOutputPath )
2019-10-23 01:28:37 +00:00
if err != nil {
log . Panic ( err )
}
if err := png . Encode ( f , outputImage ) ; err != nil {
f . Close ( )
log . Panic ( err )
}
2022-08-11 09:10:07 +00:00
done <- struct { } { }
2022-08-10 19:04:17 +00:00
wg . Wait ( )
2019-11-05 01:31:19 +00:00
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-12-21 21:25:47 +00:00
log . Printf ( "Created output file \"%v\"" , * flagOutputPath )
2019-10-23 22:28:22 +00:00
2019-10-21 00:07:39 +00:00
}