2022-07-16 14:59:32 +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 (
|
|
|
|
"fmt"
|
|
|
|
"image"
|
2019-10-23 01:28:37 +00:00
|
|
|
"image/color"
|
2022-07-16 14:59:32 +00:00
|
|
|
"log"
|
2019-10-21 00:07:39 +00:00
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
2019-10-23 22:28:22 +00:00
|
|
|
"runtime"
|
2019-10-23 01:28:37 +00:00
|
|
|
"sort"
|
2019-10-21 00:07:39 +00:00
|
|
|
"strconv"
|
2019-10-23 22:28:22 +00:00
|
|
|
"sync"
|
|
|
|
|
2019-11-05 01:31:19 +00:00
|
|
|
"github.com/cheggaaa/pb/v3"
|
2019-10-21 00:07:39 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
var regexFileParse = regexp.MustCompile(`^(-?\d+),(-?\d+).png$`)
|
|
|
|
|
2019-10-24 19:11:23 +00:00
|
|
|
func loadImages(path string, scaleDivider int) ([]imageTile, error) {
|
2019-10-21 00:07:39 +00:00
|
|
|
var imageTiles []imageTile
|
|
|
|
|
2019-10-24 19:11:23 +00:00
|
|
|
if scaleDivider < 1 {
|
2022-07-16 15:29:26 +00:00
|
|
|
return nil, fmt.Errorf("invalid scale of %v", scaleDivider)
|
2019-10-24 19:11:23 +00:00
|
|
|
}
|
|
|
|
|
2019-10-25 17:45:23 +00:00
|
|
|
files, err := filepath.Glob(filepath.Join(path, "*.png"))
|
2019-10-21 00:07:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, file := range files {
|
|
|
|
baseName := filepath.Base(file)
|
|
|
|
result := regexFileParse.FindStringSubmatch(baseName)
|
|
|
|
var x, y int
|
|
|
|
if parsed, err := strconv.ParseInt(result[1], 10, 0); err == nil {
|
|
|
|
x = int(parsed)
|
|
|
|
} else {
|
2022-07-16 15:29:26 +00:00
|
|
|
return nil, fmt.Errorf("error parsing %v to integer: %w", result[1], err)
|
2019-10-21 00:07:39 +00:00
|
|
|
}
|
|
|
|
if parsed, err := strconv.ParseInt(result[2], 10, 0); err == nil {
|
|
|
|
y = int(parsed)
|
|
|
|
} else {
|
2022-07-16 15:29:26 +00:00
|
|
|
return nil, fmt.Errorf("error parsing %v to integer: %w", result[2], err)
|
2019-10-21 00:07:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
width, height, err := getImageFileDimension(file)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
imageTiles = append(imageTiles, imageTile{
|
2019-10-24 19:11:23 +00:00
|
|
|
fileName: file,
|
|
|
|
scaleDivider: scaleDivider,
|
|
|
|
image: image.Rect(x/scaleDivider, y/scaleDivider, (x+width)/scaleDivider, (y+height)/scaleDivider),
|
|
|
|
imageMutex: &sync.RWMutex{},
|
2019-10-21 00:07:39 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return imageTiles, nil
|
|
|
|
}
|
2019-10-23 01:28:37 +00:00
|
|
|
|
2019-11-04 19:52:47 +00:00
|
|
|
// Stitch takes a list of tiles and stitches them together.
|
|
|
|
// The destImage shouldn't be too large, or it gets too slow.
|
|
|
|
func Stitch(tiles []imageTile, destImage *image.RGBA) error {
|
2022-07-16 14:59:32 +00:00
|
|
|
//intersectTiles := []*imageTile{}
|
2019-10-24 01:05:42 +00:00
|
|
|
images := []*image.RGBA{}
|
2019-10-23 01:28:37 +00:00
|
|
|
|
|
|
|
// Get only the tiles that intersect with the destination image bounds.
|
|
|
|
// Ignore alignment here, doesn't matter if an image overlaps a few pixels anyways.
|
|
|
|
for i, tile := range tiles {
|
2019-10-23 22:28:22 +00:00
|
|
|
if tile.OffsetBounds().Overlaps(destImage.Bounds()) {
|
2019-10-24 01:05:42 +00:00
|
|
|
tilePtr := &tiles[i]
|
|
|
|
img, err := tilePtr.GetImage()
|
|
|
|
if err != nil {
|
2022-07-16 15:29:26 +00:00
|
|
|
log.Printf("couldn't load image tile %s: %v", tile.String(), err)
|
2022-07-16 14:59:32 +00:00
|
|
|
continue
|
2019-10-24 01:05:42 +00:00
|
|
|
}
|
2022-07-16 14:59:32 +00:00
|
|
|
//intersectTiles = append(intersectTiles, tilePtr)
|
2019-10-24 01:05:42 +00:00
|
|
|
imgCopy := *img
|
2020-10-17 15:27:26 +00:00
|
|
|
imgCopy.Rect = imgCopy.Rect.Add(tile.offset)
|
2019-11-05 01:31:19 +00:00
|
|
|
images = append(images, &imgCopy)
|
2019-10-23 01:28:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//log.Printf("intersectTiles: %v", intersectTiles)
|
|
|
|
|
|
|
|
/*for _, intersectTile := range intersectTiles {
|
|
|
|
intersectTile.loadImage()
|
|
|
|
draw.Draw(destImage, destImage.Bounds(), intersectTile.image, destImage.Bounds().Min, draw.Over)
|
|
|
|
}*/
|
|
|
|
|
|
|
|
/*for _, intersectTile := range intersectTiles {
|
|
|
|
drawLabel(destImage, intersectTile.image.Bounds().Min.X, intersectTile.image.Bounds().Min.Y, fmt.Sprintf("%v", intersectTile.fileName))
|
|
|
|
}*/
|
|
|
|
|
2019-10-24 01:05:42 +00:00
|
|
|
drawMedianBlended(images, destImage)
|
|
|
|
|
|
|
|
return nil
|
2019-10-23 01:28:37 +00:00
|
|
|
}
|
|
|
|
|
2019-11-30 17:28:17 +00:00
|
|
|
// StitchGrid calls Stitch, but divides the workload into a grid of chunks.
|
2019-10-23 22:28:22 +00:00
|
|
|
// Additionally it runs the workload multithreaded.
|
2019-11-05 01:31:19 +00:00
|
|
|
func StitchGrid(tiles []imageTile, destImage *image.RGBA, gridSize int, bar *pb.ProgressBar) (errResult error) {
|
2019-10-24 19:11:23 +00:00
|
|
|
//workloads := gridifyRectangle(destImage.Bounds(), gridSize)
|
|
|
|
workloads, err := hilbertifyRectangle(destImage.Bounds(), gridSize)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-10-23 22:28:22 +00:00
|
|
|
|
2019-11-05 01:31:19 +00:00
|
|
|
if bar != nil {
|
|
|
|
bar.SetTotal(int64(len(workloads))).Start()
|
|
|
|
}
|
2019-10-23 22:28:22 +00:00
|
|
|
|
|
|
|
// Start worker threads
|
|
|
|
wc := make(chan image.Rectangle)
|
|
|
|
wg := sync.WaitGroup{}
|
2019-10-25 17:45:23 +00:00
|
|
|
for i := 0; i < runtime.NumCPU()*2; i++ {
|
2019-10-23 22:28:22 +00:00
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
for workload := range wc {
|
2019-11-04 19:52:47 +00:00
|
|
|
if err := Stitch(tiles, destImage.SubImage(workload).(*image.RGBA)); err != nil {
|
2019-10-23 22:28:22 +00:00
|
|
|
errResult = err // This will not stop execution, but at least one of any errors is returned.
|
|
|
|
}
|
2019-11-05 01:31:19 +00:00
|
|
|
if bar != nil {
|
|
|
|
bar.Increment()
|
|
|
|
}
|
2019-10-23 22:28:22 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
2019-10-23 01:28:37 +00:00
|
|
|
|
2019-10-23 22:28:22 +00:00
|
|
|
// Push workload to worker threads
|
|
|
|
for _, workload := range workloads {
|
|
|
|
wc <- workload
|
2019-10-23 01:28:37 +00:00
|
|
|
}
|
|
|
|
|
2019-10-23 22:28:22 +00:00
|
|
|
// Wait until all worker threads are done
|
|
|
|
close(wc)
|
|
|
|
wg.Wait()
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-10-24 01:05:42 +00:00
|
|
|
func drawMedianBlended(images []*image.RGBA, destImage *image.RGBA) {
|
2019-10-23 22:28:22 +00:00
|
|
|
bounds := destImage.Bounds()
|
|
|
|
|
2019-10-24 01:05:42 +00:00
|
|
|
// Create arrays to be reused every pixel
|
|
|
|
rListEmpty, gListEmpty, bListEmpty := make([]int, 0, len(images)), make([]int, 0, len(images)), make([]int, 0, len(images))
|
|
|
|
|
2019-10-23 01:28:37 +00:00
|
|
|
for iy := bounds.Min.Y; iy < bounds.Max.Y; iy++ {
|
|
|
|
for ix := bounds.Min.X; ix < bounds.Max.X; ix++ {
|
2019-10-24 01:05:42 +00:00
|
|
|
rList, gList, bList := rListEmpty, gListEmpty, bListEmpty
|
2019-10-23 01:28:37 +00:00
|
|
|
point := image.Point{ix, iy}
|
2019-10-23 22:28:22 +00:00
|
|
|
found := false
|
2019-10-23 01:28:37 +00:00
|
|
|
|
2019-10-24 01:05:42 +00:00
|
|
|
// Iterate through all images and create a list of colors.
|
|
|
|
for _, img := range images {
|
|
|
|
if point.In(img.Bounds()) {
|
|
|
|
col := img.RGBAAt(point.X, point.Y)
|
|
|
|
rList, gList, bList = append(rList, int(col.R)), append(gList, int(col.G)), append(bList, int(col.B))
|
2019-10-23 22:28:22 +00:00
|
|
|
found = true
|
2019-10-23 01:28:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-24 01:05:42 +00:00
|
|
|
// If there were no images to get data from, ignore the pixel.
|
2019-10-23 22:28:22 +00:00
|
|
|
if !found {
|
2019-11-04 19:52:47 +00:00
|
|
|
//destImage.SetRGBA(ix, iy, color.RGBA{})
|
2019-10-23 22:28:22 +00:00
|
|
|
continue
|
|
|
|
}
|
2019-10-23 01:28:37 +00:00
|
|
|
|
2019-10-24 01:05:42 +00:00
|
|
|
// Sort colors.
|
|
|
|
sort.Ints(rList)
|
|
|
|
sort.Ints(gList)
|
|
|
|
sort.Ints(bList)
|
2019-10-23 01:28:37 +00:00
|
|
|
|
2019-10-23 22:28:22 +00:00
|
|
|
// Take the middle element of each color.
|
2019-10-23 01:28:37 +00:00
|
|
|
var r, g, b uint8
|
|
|
|
if len(rList)%2 == 0 {
|
|
|
|
// Even
|
|
|
|
r = uint8((rList[len(rList)/2-1] + rList[len(rList)/2]) / 2)
|
2019-10-23 22:28:22 +00:00
|
|
|
} else {
|
2019-10-23 01:28:37 +00:00
|
|
|
// Odd
|
|
|
|
r = uint8(rList[(len(rList)-1)/2])
|
|
|
|
}
|
|
|
|
if len(gList)%2 == 0 {
|
|
|
|
// Even
|
|
|
|
g = uint8((gList[len(gList)/2-1] + gList[len(gList)/2]) / 2)
|
2019-10-23 22:28:22 +00:00
|
|
|
} else {
|
2019-10-23 01:28:37 +00:00
|
|
|
// Odd
|
|
|
|
g = uint8(gList[(len(gList)-1)/2])
|
|
|
|
}
|
|
|
|
if len(bList)%2 == 0 {
|
|
|
|
// Even
|
|
|
|
b = uint8((bList[len(bList)/2-1] + bList[len(bList)/2]) / 2)
|
2019-10-23 22:28:22 +00:00
|
|
|
} else {
|
2019-10-23 01:28:37 +00:00
|
|
|
// Odd
|
|
|
|
b = uint8(bList[(len(bList)-1)/2])
|
|
|
|
}
|
|
|
|
|
|
|
|
destImage.SetRGBA(ix, iy, color.RGBA{r, g, b, 255})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-11-30 17:28:17 +00:00
|
|
|
|
|
|
|
// 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]
|
|
|
|
img, err := tilePtr.GetImage()
|
|
|
|
if err != nil {
|
2022-07-16 14:59:32 +00:00
|
|
|
log.Printf("Couldn't load image tile %s: %v", tile.String(), err)
|
|
|
|
continue
|
2019-11-30 17:28:17 +00:00
|
|
|
}
|
2022-07-16 14:59:32 +00:00
|
|
|
intersectTiles = append(intersectTiles, tilePtr)
|
2019-11-30 17:28:17 +00:00
|
|
|
imgCopy := *img
|
2020-10-17 15:27:26 +00:00
|
|
|
imgCopy.Rect = imgCopy.Rect.Add(tile.offset)
|
2019-11-30 17:28:17 +00:00
|
|
|
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
|
|
|
|
}
|