mirror of
https://github.com/Dadido3/noita-mapcap.git
synced 2024-11-22 21:17:33 +00:00
Add cleanup mode to stitcher
This commit is contained in:
parent
6703074900
commit
1d832ebad1
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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{
|
||||||
|
Loading…
Reference in New Issue
Block a user