mirror of
https://github.com/Dadido3/noita-mapcap.git
synced 2024-11-18 17:17:31 +00:00
Refactor and improve stitcher
- Replace MedianBlendedImage with StitchedImage, a general implementation of a stitcher - Don't use hilbert curve when regenerating cache image - Cut workload rectangles to be always inside the cache image boundaries - Rename stitch.go to main.go - Add interface for overlays - Change how overlays are handled and drawn - Reduce error returns to simplify a lot of code - Add several blend functions - Remove offset field from image tile
This commit is contained in:
parent
7a4dbeddf1
commit
3a73e13fb7
200
bin/stitch/blend-functions.go
Normal file
200
bin/stitch/blend-functions.go
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
// Copyright (c) 2022 David Vogel
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BlendFuncMedian takes the given tiles and median blends them into destImage.
|
||||||
|
func BlendFuncMedian(tiles []*ImageTile, destImage *image.RGBA) {
|
||||||
|
bounds := destImage.Bounds()
|
||||||
|
|
||||||
|
// List of images corresponding with every tile.
|
||||||
|
// Can contain empty/nil entries for images that failed to load.
|
||||||
|
images := []*image.RGBA{}
|
||||||
|
for _, tile := range tiles {
|
||||||
|
images = append(images, tile.GetImage())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create arrays to be reused every pixel.
|
||||||
|
rListEmpty, gListEmpty, bListEmpty := make([]int, 0, len(tiles)), make([]int, 0, len(tiles)), make([]int, 0, len(tiles))
|
||||||
|
|
||||||
|
for iy := bounds.Min.Y; iy < bounds.Max.Y; iy++ {
|
||||||
|
for ix := bounds.Min.X; ix < bounds.Max.X; ix++ {
|
||||||
|
rList, gList, bList := rListEmpty, gListEmpty, bListEmpty
|
||||||
|
point := image.Point{ix, iy}
|
||||||
|
found := false
|
||||||
|
|
||||||
|
// Iterate through all images and create a list of colors.
|
||||||
|
for _, img := range images {
|
||||||
|
if img != nil {
|
||||||
|
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))
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there were no images to get data from, ignore the pixel.
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort colors.
|
||||||
|
sort.Ints(rList)
|
||||||
|
sort.Ints(gList)
|
||||||
|
sort.Ints(bList)
|
||||||
|
|
||||||
|
// Take the middle element of each color.
|
||||||
|
var r, g, b uint8
|
||||||
|
if l := len(rList); l%2 == 0 {
|
||||||
|
// Even.
|
||||||
|
r = uint8((rList[l/2-1] + rList[l/2]) / 2)
|
||||||
|
} else {
|
||||||
|
// Odd.
|
||||||
|
r = uint8(rList[(l-1)/2])
|
||||||
|
}
|
||||||
|
if l := len(gList); l%2 == 0 {
|
||||||
|
// Even.
|
||||||
|
g = uint8((gList[l/2-1] + gList[l/2]) / 2)
|
||||||
|
} else {
|
||||||
|
// Odd.
|
||||||
|
g = uint8(gList[(l-1)/2])
|
||||||
|
}
|
||||||
|
if l := len(bList); l%2 == 0 {
|
||||||
|
// Even.
|
||||||
|
b = uint8((bList[l/2-1] + bList[l/2]) / 2)
|
||||||
|
} else {
|
||||||
|
// Odd.
|
||||||
|
b = uint8(bList[(l-1)/2])
|
||||||
|
}
|
||||||
|
|
||||||
|
destImage.SetRGBA(ix, iy, color.RGBA{r, g, b, 255})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlendNewestPixel takes the given tiles and only draws the newest pixel (based on file modification time) of any overlapping tiles.
|
||||||
|
func BlendNewestPixel(tiles []*ImageTile, destImage *image.RGBA) {
|
||||||
|
bounds := destImage.Bounds()
|
||||||
|
|
||||||
|
// Sort tiles by date.
|
||||||
|
sort.Slice(tiles, func(i, j int) bool { return tiles[i].modTime.After(tiles[j].modTime) })
|
||||||
|
|
||||||
|
// List of images corresponding with every tile.
|
||||||
|
// Can contain empty/nil entries for images that failed to load.
|
||||||
|
images := []*image.RGBA{}
|
||||||
|
for _, tile := range tiles {
|
||||||
|
images = append(images, tile.GetImage())
|
||||||
|
}
|
||||||
|
|
||||||
|
for iy := bounds.Min.Y; iy < bounds.Max.Y; iy++ {
|
||||||
|
for ix := bounds.Min.X; ix < bounds.Max.X; ix++ {
|
||||||
|
point := image.Point{ix, iy}
|
||||||
|
found := false
|
||||||
|
|
||||||
|
// Look for first valid pixel in stack of tiles.
|
||||||
|
var col color.RGBA
|
||||||
|
for _, img := range images {
|
||||||
|
if img != nil {
|
||||||
|
if point.In(img.Bounds()) {
|
||||||
|
col = img.RGBAAt(point.X, point.Y)
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there were no images to get data from, ignore the pixel.
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
destImage.SetRGBA(ix, iy, col)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlendNewestPixelsMedian takes the given tiles and median blends the n newest pixels (based on file modification time) of any overlapping tiles.
|
||||||
|
// n is some hardcoded value inside this function.
|
||||||
|
func BlendNewestPixelsMedian(tiles []*ImageTile, destImage *image.RGBA) {
|
||||||
|
bounds := destImage.Bounds()
|
||||||
|
|
||||||
|
// Sort tiles by date.
|
||||||
|
sort.Slice(tiles, func(i, j int) bool { return tiles[i].modTime.After(tiles[j].modTime) })
|
||||||
|
|
||||||
|
// List of images corresponding with every tile.
|
||||||
|
// Can contain empty/nil entries for images that failed to load.
|
||||||
|
images := []*image.RGBA{}
|
||||||
|
for _, tile := range tiles {
|
||||||
|
images = append(images, tile.GetImage())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create arrays to be reused every pixel.
|
||||||
|
rListEmpty, gListEmpty, bListEmpty := make([]int, 0, len(tiles)), make([]int, 0, len(tiles)), make([]int, 0, len(tiles))
|
||||||
|
|
||||||
|
for iy := bounds.Min.Y; iy < bounds.Max.Y; iy++ {
|
||||||
|
for ix := bounds.Min.X; ix < bounds.Max.X; ix++ {
|
||||||
|
rList, gList, bList := rListEmpty, gListEmpty, bListEmpty
|
||||||
|
point := image.Point{ix, iy}
|
||||||
|
count := 0
|
||||||
|
|
||||||
|
// Iterate through all images and create a list of colors.
|
||||||
|
for _, img := range images {
|
||||||
|
if img != nil {
|
||||||
|
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))
|
||||||
|
count++
|
||||||
|
if count == 9 { // Max. number of tiles to median blend.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there were no images to get data from, ignore the pixel.
|
||||||
|
if count == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort colors.
|
||||||
|
sort.Ints(rList)
|
||||||
|
sort.Ints(gList)
|
||||||
|
sort.Ints(bList)
|
||||||
|
|
||||||
|
// Take the middle element of each color.
|
||||||
|
var r, g, b uint8
|
||||||
|
if l := len(rList); l%2 == 0 {
|
||||||
|
// Even.
|
||||||
|
r = uint8((rList[l/2-1] + rList[l/2]) / 2)
|
||||||
|
} else {
|
||||||
|
// Odd.
|
||||||
|
r = uint8(rList[(l-1)/2])
|
||||||
|
}
|
||||||
|
if l := len(gList); l%2 == 0 {
|
||||||
|
// Even.
|
||||||
|
g = uint8((gList[l/2-1] + gList[l/2]) / 2)
|
||||||
|
} else {
|
||||||
|
// Odd.
|
||||||
|
g = uint8(gList[(l-1)/2])
|
||||||
|
}
|
||||||
|
if l := len(bList); l%2 == 0 {
|
||||||
|
// Even.
|
||||||
|
b = uint8((bList[l/2-1] + bList[l/2]) / 2)
|
||||||
|
} else {
|
||||||
|
// Odd.
|
||||||
|
b = uint8(bList[(l-1)/2])
|
||||||
|
}
|
||||||
|
|
||||||
|
destImage.SetRGBA(ix, iy, color.RGBA{r, g, b, 255})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,10 +7,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
|
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
//var entityDisplayFontFamily = canvas.NewFontFamily("times")
|
//var entityDisplayFontFamily = canvas.NewFontFamily("times")
|
||||||
@ -103,13 +105,15 @@ type Component struct {
|
|||||||
Members map[string]any `json:"members"`
|
Members map[string]any `json:"members"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadEntities(path string) ([]Entity, error) {
|
type Entities []Entity
|
||||||
|
|
||||||
|
func LoadEntities(path string) (Entities, error) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []Entity
|
var result Entities
|
||||||
|
|
||||||
jsonDec := json.NewDecoder(file)
|
jsonDec := json.NewDecoder(file)
|
||||||
if err := jsonDec.Decode(&result); err != nil {
|
if err := jsonDec.Decode(&result); err != nil {
|
||||||
@ -236,3 +240,32 @@ func (e Entity) Draw(c *canvas.Context) {
|
|||||||
//text := canvas.NewTextLine(entityDisplayFontFace, fmt.Sprintf("%s\n%s", e.Name, e.Filename), canvas.Left)
|
//text := canvas.NewTextLine(entityDisplayFontFace, fmt.Sprintf("%s\n%s", e.Name, e.Filename), canvas.Left)
|
||||||
//c.DrawText(x, y, text)
|
//c.DrawText(x, y, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e Entities) Draw(destImage *image.RGBA) {
|
||||||
|
destRect := destImage.Bounds()
|
||||||
|
|
||||||
|
// Same as destImage, but top left is translated to (0, 0).
|
||||||
|
originImage := destImage.SubImage(destRect).(*image.RGBA)
|
||||||
|
originImage.Rect = originImage.Rect.Sub(destRect.Min)
|
||||||
|
|
||||||
|
c := canvas.New(float64(destRect.Dx()), float64(destRect.Dy()))
|
||||||
|
ctx := canvas.NewContext(c)
|
||||||
|
ctx.SetCoordSystem(canvas.CartesianIV)
|
||||||
|
ctx.SetCoordRect(canvas.Rect{X: -float64(destRect.Min.X), Y: -float64(destRect.Min.Y), W: float64(destRect.Dx()), H: float64(destRect.Dy())}, float64(destRect.Dx()), float64(destRect.Dy()))
|
||||||
|
|
||||||
|
// Set drawing style.
|
||||||
|
ctx.Style = playerPathDisplayStyle
|
||||||
|
|
||||||
|
for _, entity := range e {
|
||||||
|
// Check if entity origin is near or around the current image rectangle.
|
||||||
|
entityOrigin := image.Point{int(entity.Transform.X), int(entity.Transform.Y)}
|
||||||
|
if entityOrigin.In(destRect.Inset(-512)) {
|
||||||
|
entity.Draw(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theoretically we would need to linearize imgRGBA first, but DefaultColorSpace assumes that the color space is linear already.
|
||||||
|
r := rasterizer.FromImage(originImage, canvas.DPMM(1.0), canvas.DefaultColorSpace)
|
||||||
|
c.Render(r)
|
||||||
|
r.Close() // This just transforms the image's luminance curve back from linear into non linear.
|
||||||
|
}
|
159
bin/stitch/image-tile.go
Normal file
159
bin/stitch/image-tile.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
// Copyright (c) 2019-2022 David Vogel
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
_ "image/png"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ImageTileFileRegex = regexp.MustCompile(`^(-?\d+),(-?\d+).png$`)
|
||||||
|
|
||||||
|
type ImageTile struct {
|
||||||
|
fileName string
|
||||||
|
modTime time.Time
|
||||||
|
|
||||||
|
scaleDivider int // Downscales the coordinates and images on the fly.
|
||||||
|
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewImageTile returns an image tile object that represents the image at the given path.
|
||||||
|
// This will not load the image into RAM.
|
||||||
|
func NewImageTile(path string, scaleDivider int) (ImageTile, error) {
|
||||||
|
if scaleDivider < 1 {
|
||||||
|
return ImageTile{}, fmt.Errorf("invalid scale of %v", scaleDivider)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseName := filepath.Base(path)
|
||||||
|
result := ImageTileFileRegex.FindStringSubmatch(baseName)
|
||||||
|
var x, y int
|
||||||
|
if parsed, err := strconv.ParseInt(result[1], 10, 0); err == nil {
|
||||||
|
x = int(parsed)
|
||||||
|
} else {
|
||||||
|
return ImageTile{}, fmt.Errorf("error parsing %q to integer: %w", result[1], err)
|
||||||
|
}
|
||||||
|
if parsed, err := strconv.ParseInt(result[2], 10, 0); err == nil {
|
||||||
|
y = int(parsed)
|
||||||
|
} else {
|
||||||
|
return ImageTile{}, fmt.Errorf("error parsing %q to integer: %w", result[2], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
width, height, err := getImageFileDimension(path)
|
||||||
|
if err != nil {
|
||||||
|
return ImageTile{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var modTime time.Time
|
||||||
|
fileInfo, err := os.Lstat(path)
|
||||||
|
if err == nil {
|
||||||
|
modTime = fileInfo.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ImageTile{
|
||||||
|
fileName: path,
|
||||||
|
modTime: modTime,
|
||||||
|
scaleDivider: scaleDivider,
|
||||||
|
image: image.Rect(x/scaleDivider, y/scaleDivider, (x+width)/scaleDivider, (y+height)/scaleDivider),
|
||||||
|
imageMutex: &sync.RWMutex{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetImage returns an image.Image that contains the tile pixel data.
|
||||||
|
// This will not return errors in case something went wrong, but will just return nil.
|
||||||
|
// All errors are written to stdout.
|
||||||
|
func (it *ImageTile) GetImage() *image.RGBA {
|
||||||
|
it.imageMutex.RLock()
|
||||||
|
|
||||||
|
it.imageUsedFlag = true // Race condition may happen on this flag, but doesn't matter here.
|
||||||
|
|
||||||
|
// Check if the image is already loaded.
|
||||||
|
if img, ok := it.image.(*image.RGBA); ok {
|
||||||
|
it.imageMutex.RUnlock()
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
it.imageMutex.RUnlock()
|
||||||
|
// It's possible that the image got changed in between here.
|
||||||
|
it.imageMutex.Lock()
|
||||||
|
defer it.imageMutex.Unlock()
|
||||||
|
|
||||||
|
// Check again if the image is already loaded.
|
||||||
|
if img, ok := it.image.(*image.RGBA); ok {
|
||||||
|
return img
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store rectangle of the old image.
|
||||||
|
oldRect := it.image.Bounds()
|
||||||
|
|
||||||
|
file, err := os.Open(it.fileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Couldn't load file %q: %v.", it.fileName, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
img, _, err := image.Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Couldn't decode image %q: %v.", it.fileName, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if it.scaleDivider > 1 {
|
||||||
|
img = resize.Resize(uint(oldRect.Dx()), uint(oldRect.Dy()), img, resize.NearestNeighbor)
|
||||||
|
}
|
||||||
|
|
||||||
|
imgRGBA, ok := img.(*image.RGBA)
|
||||||
|
if !ok {
|
||||||
|
log.Printf("Expected an RGBA image for %q, got %T instead.", it.fileName, img)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
imgRGBA.Rect = imgRGBA.Rect.Add(oldRect.Min)
|
||||||
|
|
||||||
|
it.image = imgRGBA
|
||||||
|
|
||||||
|
// Free the image after some time.
|
||||||
|
go func() {
|
||||||
|
for it.imageUsedFlag {
|
||||||
|
it.imageUsedFlag = false
|
||||||
|
time.Sleep(1000 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.imageMutex.Lock()
|
||||||
|
defer it.imageMutex.Unlock()
|
||||||
|
it.image = it.image.Bounds()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return imgRGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
// The scaled image boundaries.
|
||||||
|
// This matches exactly to what GetImage() returns.
|
||||||
|
func (it *ImageTile) Bounds() image.Rectangle {
|
||||||
|
it.imageMutex.RLock()
|
||||||
|
defer it.imageMutex.RUnlock()
|
||||||
|
|
||||||
|
return it.image.Bounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (it *ImageTile) String() string {
|
||||||
|
return fmt.Sprintf("{ImageTile: %q}", it.fileName)
|
||||||
|
}
|
162
bin/stitch/image-tiles.go
Normal file
162
bin/stitch/image-tiles.go
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// Copyright (c) 2019-2022 David Vogel
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/cheggaaa/pb/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadImageTiles "loads" all images in the directory at the given path.
|
||||||
|
func LoadImageTiles(path string, scaleDivider int) ([]ImageTile, error) {
|
||||||
|
if scaleDivider < 1 {
|
||||||
|
return nil, fmt.Errorf("invalid scale of %v", scaleDivider)
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageTiles []ImageTile
|
||||||
|
|
||||||
|
files, err := filepath.Glob(filepath.Join(path, "*.png"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
imageTile, err := NewImageTile(file, scaleDivider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
imageTiles = append(imageTiles, imageTile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageTiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Bounds().Overlaps(bounds) {
|
||||||
|
tilePtr := &tiles[i]
|
||||||
|
img := tilePtr.GetImage()
|
||||||
|
if img == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
intersectTiles = append(intersectTiles, tilePtr)
|
||||||
|
imgCopy := *img
|
||||||
|
//imgCopy.Rect = imgCopy.Rect
|
||||||
|
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
|
||||||
|
}
|
@ -1,156 +0,0 @@
|
|||||||
// Copyright (c) 2019-2022 David Vogel
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
_ "image/png"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/nfnt/resize"
|
|
||||||
"github.com/tdewolff/canvas"
|
|
||||||
"github.com/tdewolff/canvas/renderers/rasterizer"
|
|
||||||
)
|
|
||||||
|
|
||||||
type imageTile struct {
|
|
||||||
fileName string
|
|
||||||
|
|
||||||
scaleDivider int // Downscales the coordinates and images on the fly.
|
|
||||||
|
|
||||||
offset image.Point // Correction offset of the image, so that it aligns pixel perfect with other images. Determined by image matching.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
entities []Entity // List of entities that may lie on or near this image tile.
|
|
||||||
playerPath *PlayerPath // Contains the player path.
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *imageTile) GetImage() (*image.RGBA, error) {
|
|
||||||
it.imageMutex.RLock()
|
|
||||||
|
|
||||||
it.imageUsedFlag = true // Race condition may happen on this flag, but doesn't matter here.
|
|
||||||
|
|
||||||
// Check if the image is already loaded.
|
|
||||||
if img, ok := it.image.(*image.RGBA); ok {
|
|
||||||
it.imageMutex.RUnlock()
|
|
||||||
return img, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
it.imageMutex.RUnlock()
|
|
||||||
// It's possible that the image got changed in between here.
|
|
||||||
it.imageMutex.Lock()
|
|
||||||
defer it.imageMutex.Unlock()
|
|
||||||
|
|
||||||
// Check again if the image is already loaded.
|
|
||||||
if img, ok := it.image.(*image.RGBA); ok {
|
|
||||||
return img, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store rectangle of the old image.
|
|
||||||
oldRect := it.image.Bounds()
|
|
||||||
|
|
||||||
file, err := os.Open(it.fileName)
|
|
||||||
if err != nil {
|
|
||||||
return &image.RGBA{}, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
img, _, err := image.Decode(file)
|
|
||||||
if err != nil {
|
|
||||||
return &image.RGBA{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if it.scaleDivider > 1 {
|
|
||||||
img = resize.Resize(uint(oldRect.Dx()), uint(oldRect.Dy()), img, resize.NearestNeighbor)
|
|
||||||
}
|
|
||||||
|
|
||||||
imgRGBA, ok := img.(*image.RGBA)
|
|
||||||
if !ok {
|
|
||||||
return &image.RGBA{}, fmt.Errorf("expected an RGBA image, got %T instead", img)
|
|
||||||
}
|
|
||||||
|
|
||||||
scaledRect := imgRGBA.Rect.Add(oldRect.Min)
|
|
||||||
|
|
||||||
// Draw entities.
|
|
||||||
// tdewolff/canvas doesn't respect the image boundaries, so we have to draw on the image before we move its rectangle.
|
|
||||||
if len(it.entities) > 0 {
|
|
||||||
c := canvas.New(float64(imgRGBA.Rect.Dx()), float64(imgRGBA.Rect.Dy()))
|
|
||||||
ctx := canvas.NewContext(c)
|
|
||||||
ctx.SetCoordSystem(canvas.CartesianIV)
|
|
||||||
ctx.SetCoordRect(canvas.Rect{X: -float64(oldRect.Min.X), Y: -float64(oldRect.Min.Y), W: float64(imgRGBA.Rect.Dx()), H: float64(imgRGBA.Rect.Dy())}, float64(imgRGBA.Rect.Dx()), float64(imgRGBA.Rect.Dy()))
|
|
||||||
for _, entity := range it.entities {
|
|
||||||
// Check if entity origin is near or around the current image rectangle.
|
|
||||||
entityOrigin := image.Point{int(entity.Transform.X), int(entity.Transform.Y)}
|
|
||||||
if entityOrigin.In(scaledRect.Inset(-512)) {
|
|
||||||
entity.Draw(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theoretically we would need to linearize imgRGBA first, but DefaultColorSpace assumes that the color space is linear already.
|
|
||||||
r := rasterizer.FromImage(imgRGBA, canvas.DPMM(1.0), canvas.DefaultColorSpace)
|
|
||||||
c.Render(r)
|
|
||||||
r.Close() // This just transforms the image's luminance curve back from linear into non linear.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw player path.
|
|
||||||
if it.playerPath != nil {
|
|
||||||
c := canvas.New(float64(imgRGBA.Rect.Dx()), float64(imgRGBA.Rect.Dy()))
|
|
||||||
ctx := canvas.NewContext(c)
|
|
||||||
ctx.SetCoordSystem(canvas.CartesianIV)
|
|
||||||
ctx.SetCoordRect(canvas.Rect{X: -float64(oldRect.Min.X), Y: -float64(oldRect.Min.Y), W: float64(imgRGBA.Rect.Dx()), H: float64(imgRGBA.Rect.Dy())}, float64(imgRGBA.Rect.Dx()), float64(imgRGBA.Rect.Dy()))
|
|
||||||
|
|
||||||
it.playerPath.Draw(ctx, scaledRect)
|
|
||||||
|
|
||||||
// Theoretically we would need to linearize imgRGBA first, but DefaultColorSpace assumes that the color space is linear already.
|
|
||||||
r := rasterizer.FromImage(imgRGBA, canvas.DPMM(1.0), canvas.DefaultColorSpace)
|
|
||||||
c.Render(r)
|
|
||||||
r.Close() // This just transforms the image's luminance curve back from linear into non linear.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore the position of the image rectangle.
|
|
||||||
imgRGBA.Rect = scaledRect
|
|
||||||
|
|
||||||
it.image = imgRGBA
|
|
||||||
|
|
||||||
// Free the image after some time.
|
|
||||||
go func() {
|
|
||||||
for it.imageUsedFlag {
|
|
||||||
it.imageUsedFlag = false
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
|
||||||
|
|
||||||
it.imageMutex.Lock()
|
|
||||||
defer it.imageMutex.Unlock()
|
|
||||||
it.image = it.image.Bounds()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return imgRGBA, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *imageTile) OffsetBounds() image.Rectangle {
|
|
||||||
it.imageMutex.RLock()
|
|
||||||
defer it.imageMutex.RUnlock()
|
|
||||||
|
|
||||||
return it.image.Bounds().Add(it.offset)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *imageTile) Bounds() image.Rectangle {
|
|
||||||
it.imageMutex.RLock()
|
|
||||||
defer it.imageMutex.RUnlock()
|
|
||||||
|
|
||||||
return it.image.Bounds()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (it *imageTile) String() string {
|
|
||||||
return fmt.Sprintf("<ImageTile \"%v\">", it.fileName)
|
|
||||||
}
|
|
@ -1,333 +0,0 @@
|
|||||||
// Copyright (c) 2019-2022 David Vogel
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"log"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/cheggaaa/pb/v3"
|
|
||||||
)
|
|
||||||
|
|
||||||
var regexFileParse = regexp.MustCompile(`^(-?\d+),(-?\d+).png$`)
|
|
||||||
|
|
||||||
func loadImages(path string, entities []Entity, playerPath *PlayerPath, scaleDivider int) ([]imageTile, error) {
|
|
||||||
var imageTiles []imageTile
|
|
||||||
|
|
||||||
if scaleDivider < 1 {
|
|
||||||
return nil, fmt.Errorf("invalid scale of %v", scaleDivider)
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := filepath.Glob(filepath.Join(path, "*.png"))
|
|
||||||
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 {
|
|
||||||
return nil, fmt.Errorf("error parsing %v to integer: %w", result[1], err)
|
|
||||||
}
|
|
||||||
if parsed, err := strconv.ParseInt(result[2], 10, 0); err == nil {
|
|
||||||
y = int(parsed)
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("error parsing %v to integer: %w", result[2], err)
|
|
||||||
}
|
|
||||||
|
|
||||||
width, height, err := getImageFileDimension(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
imageTiles = append(imageTiles, imageTile{
|
|
||||||
fileName: file,
|
|
||||||
scaleDivider: scaleDivider,
|
|
||||||
image: image.Rect(x/scaleDivider, y/scaleDivider, (x+width)/scaleDivider, (y+height)/scaleDivider),
|
|
||||||
imageMutex: &sync.RWMutex{},
|
|
||||||
entities: entities,
|
|
||||||
playerPath: playerPath,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageTiles, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
//intersectTiles := []*imageTile{}
|
|
||||||
images := []*image.RGBA{}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
if tile.OffsetBounds().Overlaps(destImage.Bounds()) {
|
|
||||||
tilePtr := &tiles[i]
|
|
||||||
img, err := tilePtr.GetImage()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("couldn't load image tile %s: %v", tile.String(), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
//intersectTiles = append(intersectTiles, tilePtr)
|
|
||||||
imgCopy := *img
|
|
||||||
imgCopy.Rect = imgCopy.Rect.Add(tile.offset)
|
|
||||||
images = append(images, &imgCopy)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//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))
|
|
||||||
}*/
|
|
||||||
|
|
||||||
drawMedianBlended(images, destImage)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
workloads, err := hilbertifyRectangle(destImage.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 := Stitch(tiles, destImage.SubImage(workload).(*image.RGBA)); 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawMedianBlended(images []*image.RGBA, destImage *image.RGBA) {
|
|
||||||
bounds := destImage.Bounds()
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
|
|
||||||
for iy := bounds.Min.Y; iy < bounds.Max.Y; iy++ {
|
|
||||||
for ix := bounds.Min.X; ix < bounds.Max.X; ix++ {
|
|
||||||
rList, gList, bList := rListEmpty, gListEmpty, bListEmpty
|
|
||||||
point := image.Point{ix, iy}
|
|
||||||
found := false
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there were no images to get data from, ignore the pixel.
|
|
||||||
if !found {
|
|
||||||
//destImage.SetRGBA(ix, iy, color.RGBA{})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort colors.
|
|
||||||
sort.Ints(rList)
|
|
||||||
sort.Ints(gList)
|
|
||||||
sort.Ints(bList)
|
|
||||||
|
|
||||||
// Take the middle element of each color.
|
|
||||||
var r, g, b uint8
|
|
||||||
if len(rList)%2 == 0 {
|
|
||||||
// Even
|
|
||||||
r = uint8((rList[len(rList)/2-1] + rList[len(rList)/2]) / 2)
|
|
||||||
} else {
|
|
||||||
// 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)
|
|
||||||
} else {
|
|
||||||
// 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)
|
|
||||||
} else {
|
|
||||||
// Odd
|
|
||||||
b = uint8(bList[(len(bList)-1)/2])
|
|
||||||
}
|
|
||||||
|
|
||||||
destImage.SetRGBA(ix, iy, color.RGBA{r, g, b, 255})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
log.Printf("Couldn't load image tile %s: %v", tile.String(), err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
intersectTiles = append(intersectTiles, tilePtr)
|
|
||||||
imgCopy := *img
|
|
||||||
imgCopy.Rect = imgCopy.Rect.Add(tile.offset)
|
|
||||||
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
|
|
||||||
}
|
|
@ -36,6 +36,8 @@ func main() {
|
|||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
var overlays []StitchedImageOverlay
|
||||||
|
|
||||||
// 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{
|
||||||
@ -94,12 +96,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load entities if requested.
|
// Load entities if requested.
|
||||||
entities, err := loadEntities(*flagEntitiesInputPath)
|
entities, err := LoadEntities(*flagEntitiesInputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to load entities: %v", err)
|
log.Printf("Failed to load entities: %v", err)
|
||||||
}
|
}
|
||||||
if len(entities) > 0 {
|
if len(entities) > 0 {
|
||||||
log.Printf("Got %v entities.", len(entities))
|
log.Printf("Got %v entities.", len(entities))
|
||||||
|
overlays = append(overlays, entities) // Add entities to overlay drawing list.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query the user, if there were no cmd arguments given.
|
// Query the user, if there were no cmd arguments given.
|
||||||
@ -118,16 +121,17 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load player path if requested.
|
// Load player path if requested.
|
||||||
playerPath, err := loadPlayerPath(*flagPlayerPathInputPath)
|
playerPath, err := LoadPlayerPath(*flagPlayerPathInputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to load player path: %v", err)
|
log.Printf("Failed to load player path: %v", err)
|
||||||
}
|
}
|
||||||
if playerPath != nil && len(playerPath.PathElements) > 0 {
|
if len(playerPath) > 0 {
|
||||||
log.Printf("Got %v player path entries.", len(playerPath.PathElements))
|
log.Printf("Got %v player path entries.", len(playerPath))
|
||||||
|
overlays = append(overlays, playerPath) // Add player path to overlay drawing list.
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Starting to read tile information at \"%v\"", *flagInputPath)
|
log.Printf("Starting to read tile information at \"%v\"", *flagInputPath)
|
||||||
tiles, err := loadImages(*flagInputPath, entities, playerPath, *flagScaleDivider)
|
tiles, err := LoadImageTiles(*flagInputPath, *flagScaleDivider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panic(err)
|
log.Panic(err)
|
||||||
}
|
}
|
||||||
@ -266,15 +270,18 @@ func main() {
|
|||||||
*flagOutputPath = result
|
*flagOutputPath = result
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputImage image.Image
|
|
||||||
bar := pb.Full.New(0)
|
bar := pb.Full.New(0)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
done := make(chan bool)
|
done := make(chan struct{})
|
||||||
|
|
||||||
tempImage := NewMedianBlendedImage(tiles, outputRect)
|
outputImage, err := NewStitchedImage(tiles, outputRect, BlendNewestPixelsMedian, 512, overlays)
|
||||||
_, max := tempImage.Progress()
|
if err != nil {
|
||||||
|
log.Panicf("NewStitchedImage() failed: %v", err)
|
||||||
|
}
|
||||||
|
_, max := outputImage.Progress()
|
||||||
bar.SetTotal(int64(max)).Start().SetRefreshRate(1 * time.Second)
|
bar.SetTotal(int64(max)).Start().SetRefreshRate(1 * time.Second)
|
||||||
|
|
||||||
|
// Query progress and draw progress bar.
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
@ -283,19 +290,17 @@ func main() {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
value, _ := tempImage.Progress()
|
value, _ := outputImage.Progress()
|
||||||
bar.SetCurrent(int64(value))
|
bar.SetCurrent(int64(value))
|
||||||
bar.Finish()
|
bar.Finish()
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
value, _ := tempImage.Progress()
|
value, _ := outputImage.Progress()
|
||||||
bar.SetCurrent(int64(value))
|
bar.SetCurrent(int64(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
outputImage = tempImage
|
|
||||||
|
|
||||||
log.Printf("Creating output file \"%v\"", *flagOutputPath)
|
log.Printf("Creating output file \"%v\"", *flagOutputPath)
|
||||||
f, err := os.Create(*flagOutputPath)
|
f, err := os.Create(*flagOutputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -307,7 +312,7 @@ func main() {
|
|||||||
log.Panic(err)
|
log.Panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
done <- true
|
done <- struct{}{}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if err := f.Close(); err != nil {
|
if err := f.Close(); err != nil {
|
@ -1,90 +0,0 @@
|
|||||||
// Copyright (c) 2019-2020 David Vogel
|
|
||||||
//
|
|
||||||
// This software is released under the MIT License.
|
|
||||||
// https://opensource.org/licenses/MIT
|
|
||||||
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
"image/color"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MedianBlendedImageRowHeight defines the height of the cached output image.
|
|
||||||
const MedianBlendedImageRowHeight = 256
|
|
||||||
|
|
||||||
// MedianBlendedImage combines several imageTile to a single RGBA image.
|
|
||||||
type MedianBlendedImage struct {
|
|
||||||
tiles []imageTile
|
|
||||||
bounds image.Rectangle
|
|
||||||
|
|
||||||
cachedRow *image.RGBA
|
|
||||||
queryCounter int
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMedianBlendedImage creates a new image from several single image tiles.
|
|
||||||
func NewMedianBlendedImage(tiles []imageTile, bounds image.Rectangle) *MedianBlendedImage {
|
|
||||||
return &MedianBlendedImage{
|
|
||||||
tiles: tiles,
|
|
||||||
bounds: bounds,
|
|
||||||
cachedRow: &image.RGBA{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ColorModel returns the Image's color model.
|
|
||||||
func (mbi *MedianBlendedImage) ColorModel() color.Model {
|
|
||||||
return color.RGBAModel
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bounds returns the domain for which At can return non-zero color.
|
|
||||||
// The bounds do not necessarily contain the point (0, 0).
|
|
||||||
func (mbi *MedianBlendedImage) Bounds() image.Rectangle {
|
|
||||||
return mbi.bounds
|
|
||||||
}
|
|
||||||
|
|
||||||
// At returns the color of the pixel at (x, y).
|
|
||||||
// At(Bounds().Min.X, Bounds().Min.Y) returns the upper-left pixel of the grid.
|
|
||||||
// At(Bounds().Max.X-1, Bounds().Max.Y-1) returns the lower-right one.
|
|
||||||
func (mbi *MedianBlendedImage) At(x, y int) color.Color {
|
|
||||||
p := image.Point{x, y}
|
|
||||||
|
|
||||||
// Assume that every pixel is only queried once
|
|
||||||
mbi.queryCounter++
|
|
||||||
|
|
||||||
if !p.In(mbi.cachedRow.Bounds()) {
|
|
||||||
// Need to create a new row image
|
|
||||||
rect := mbi.Bounds()
|
|
||||||
rect.Min.Y = divideFloor(y, MedianBlendedImageRowHeight) * MedianBlendedImageRowHeight
|
|
||||||
rect.Max.Y = rect.Min.Y + MedianBlendedImageRowHeight
|
|
||||||
|
|
||||||
if !p.In(rect) {
|
|
||||||
return color.RGBA{}
|
|
||||||
}
|
|
||||||
|
|
||||||
mbi.cachedRow = image.NewRGBA(rect)
|
|
||||||
|
|
||||||
// TODO: Don't use hilbert curve here
|
|
||||||
if err := StitchGrid(mbi.tiles, mbi.cachedRow, 512, nil); err != nil {
|
|
||||||
log.Printf("StitchGrid failed: %v", err)
|
|
||||||
return color.RGBA{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mbi.cachedRow.RGBAAt(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Opaque returns whether the image is fully opaque.
|
|
||||||
//
|
|
||||||
// For more speed and smaller file size, MedianBlendedImage will be marked as non-transparent.
|
|
||||||
// This will speed up image saving by 2x, as there is no need to iterate over the whole image to find a single non opaque pixel.
|
|
||||||
func (mbi *MedianBlendedImage) Opaque() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Progress returns the approximate progress of any process that scans the image from top to bottom.
|
|
||||||
func (mbi *MedianBlendedImage) Progress() (value, max int) {
|
|
||||||
size := mbi.Bounds().Size()
|
|
||||||
|
|
||||||
return mbi.queryCounter, size.X * size.Y
|
|
||||||
}
|
|
@ -13,6 +13,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/tdewolff/canvas"
|
"github.com/tdewolff/canvas"
|
||||||
|
"github.com/tdewolff/canvas/renderers/rasterizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var playerPathDisplayStyle = canvas.Style{
|
var playerPathDisplayStyle = canvas.Style{
|
||||||
@ -34,52 +35,66 @@ type PlayerPathElement struct {
|
|||||||
Polymorphed bool `json:"polymorphed"`
|
Polymorphed bool `json:"polymorphed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PlayerPath struct {
|
type PlayerPath []PlayerPathElement
|
||||||
PathElements []PlayerPathElement
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadPlayerPath(path string) (*PlayerPath, error) {
|
func LoadPlayerPath(path string) (PlayerPath, error) {
|
||||||
file, err := os.Open(path)
|
file, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []PlayerPathElement
|
var result PlayerPath
|
||||||
|
|
||||||
jsonDec := json.NewDecoder(file)
|
jsonDec := json.NewDecoder(file)
|
||||||
if err := jsonDec.Decode(&result); err != nil {
|
if err := jsonDec.Decode(&result); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &PlayerPath{PathElements: result}, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p PlayerPath) Draw(c *canvas.Context, imgRect image.Rectangle) {
|
func (p PlayerPath) Draw(destImage *image.RGBA) {
|
||||||
// Set drawing style.
|
destRect := destImage.Bounds()
|
||||||
c.Style = playerPathDisplayStyle
|
|
||||||
|
|
||||||
for _, pathElement := range p.PathElements {
|
// Same as destImage, but top left is translated to (0, 0).
|
||||||
|
originImage := destImage.SubImage(destRect).(*image.RGBA)
|
||||||
|
originImage.Rect = originImage.Rect.Sub(destRect.Min)
|
||||||
|
|
||||||
|
c := canvas.New(float64(destRect.Dx()), float64(destRect.Dy()))
|
||||||
|
ctx := canvas.NewContext(c)
|
||||||
|
ctx.SetCoordSystem(canvas.CartesianIV)
|
||||||
|
ctx.SetCoordRect(canvas.Rect{X: -float64(destRect.Min.X), Y: -float64(destRect.Min.Y), W: float64(destRect.Dx()), H: float64(destRect.Dy())}, float64(destRect.Dx()), float64(destRect.Dy()))
|
||||||
|
|
||||||
|
// Set drawing style.
|
||||||
|
ctx.Style = playerPathDisplayStyle
|
||||||
|
|
||||||
|
for _, pathElement := range p {
|
||||||
from, to := pathElement.From, pathElement.To
|
from, to := pathElement.From, pathElement.To
|
||||||
|
|
||||||
// Only draw if the path may cross the image rectangle.
|
// Only draw if the path may cross the image rectangle.
|
||||||
pathRect := image.Rectangle{image.Point{int(from[0]), int(from[1])}, image.Point{int(to[0]), int(to[1])}}.Canon().Inset(int(-playerPathDisplayStyle.StrokeWidth) - 1)
|
pathRect := image.Rectangle{image.Point{int(from[0]), int(from[1])}, image.Point{int(to[0]), int(to[1])}}.Canon().Inset(int(-playerPathDisplayStyle.StrokeWidth) - 1)
|
||||||
if pathRect.Overlaps(imgRect) {
|
if pathRect.Overlaps(destRect) {
|
||||||
path := &canvas.Path{}
|
path := &canvas.Path{}
|
||||||
path.MoveTo(from[0], from[1])
|
path.MoveTo(from[0], from[1])
|
||||||
path.LineTo(to[0], to[1])
|
path.LineTo(to[0], to[1])
|
||||||
|
|
||||||
if pathElement.Polymorphed {
|
if pathElement.Polymorphed {
|
||||||
// Set stroke color to typically polymorph color.
|
// Set stroke color to typically polymorph color.
|
||||||
c.Style.StrokeColor = color.RGBA{127, 50, 83, 127}
|
ctx.Style.StrokeColor = color.RGBA{127, 50, 83, 127}
|
||||||
} else {
|
} else {
|
||||||
// Set stroke color depending on HP level.
|
// Set stroke color depending on HP level.
|
||||||
hpFactor := math.Max(math.Min(pathElement.HP/pathElement.MaxHP, 1), 0)
|
hpFactor := math.Max(math.Min(pathElement.HP/pathElement.MaxHP, 1), 0)
|
||||||
hpFactorInv := 1 - hpFactor
|
hpFactorInv := 1 - hpFactor
|
||||||
r, g, b, a := uint8((0*hpFactor+1*hpFactorInv)*127), uint8((1*hpFactor+0*hpFactorInv)*127), uint8(0), uint8(127)
|
r, g, b, a := uint8((0*hpFactor+1*hpFactorInv)*127), uint8((1*hpFactor+0*hpFactorInv)*127), uint8(0), uint8(127)
|
||||||
c.Style.StrokeColor = color.RGBA{r, g, b, a}
|
ctx.Style.StrokeColor = color.RGBA{r, g, b, a}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.DrawPath(0, 0, path)
|
ctx.DrawPath(0, 0, path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Theoretically we would need to linearize imgRGBA first, but DefaultColorSpace assumes that the color space is linear already.
|
||||||
|
r := rasterizer.FromImage(originImage, canvas.DPMM(1.0), canvas.DefaultColorSpace)
|
||||||
|
c.Render(r)
|
||||||
|
r.Close() // This just transforms the image's luminance curve back from linear into non linear.
|
||||||
}
|
}
|
||||||
|
174
bin/stitch/stitched-image.go
Normal file
174
bin/stitch/stitched-image.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
// Copyright (c) 2022 David Vogel
|
||||||
|
//
|
||||||
|
// This software is released under the MIT License.
|
||||||
|
// https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StitchedImageCacheGridSize defines the worker chunk size when the cache image is regenerated.
|
||||||
|
// TODO: Find optimal grid size that works good for tiles with lots and few overlap
|
||||||
|
var StitchedImageCacheGridSize = 512
|
||||||
|
|
||||||
|
// StitchedImageBlendFunc implements how all the tiles are blended together.
|
||||||
|
// This is called when a new cache image needs to be generated.
|
||||||
|
type StitchedImageBlendFunc func(tiles []*ImageTile, destImage *image.RGBA)
|
||||||
|
|
||||||
|
type StitchedImageOverlay interface {
|
||||||
|
Draw(*image.RGBA)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StitchedImage combines several ImageTile objects into a single RGBA image.
|
||||||
|
// The way the images are combined/blended is defined by the blendFunc.
|
||||||
|
type StitchedImage struct {
|
||||||
|
tiles []ImageTile
|
||||||
|
bounds image.Rectangle
|
||||||
|
blendFunc StitchedImageBlendFunc
|
||||||
|
overlays []StitchedImageOverlay
|
||||||
|
|
||||||
|
cacheHeight int
|
||||||
|
cacheImage *image.RGBA
|
||||||
|
|
||||||
|
queryCounter int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStitchedImage creates a new image from several single image tiles.
|
||||||
|
func NewStitchedImage(tiles []ImageTile, bounds image.Rectangle, blendFunc StitchedImageBlendFunc, cacheHeight int, overlays []StitchedImageOverlay) (*StitchedImage, error) {
|
||||||
|
if bounds.Empty() {
|
||||||
|
return nil, fmt.Errorf("given boundaries are empty")
|
||||||
|
}
|
||||||
|
if blendFunc == nil {
|
||||||
|
return nil, fmt.Errorf("no blending function given")
|
||||||
|
}
|
||||||
|
if cacheHeight <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid cache height of %d pixels", cacheHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &StitchedImage{
|
||||||
|
tiles: tiles,
|
||||||
|
bounds: bounds,
|
||||||
|
blendFunc: blendFunc,
|
||||||
|
overlays: overlays,
|
||||||
|
cacheHeight: cacheHeight,
|
||||||
|
cacheImage: &image.RGBA{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorModel returns the Image's color model.
|
||||||
|
func (si *StitchedImage) ColorModel() color.Model {
|
||||||
|
return color.RGBAModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds returns the domain for which At can return non-zero color.
|
||||||
|
// The bounds do not necessarily contain the point (0, 0).
|
||||||
|
func (si *StitchedImage) Bounds() image.Rectangle {
|
||||||
|
return si.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// At returns the color of the pixel at (x, y).
|
||||||
|
//
|
||||||
|
// This is optimized to be read line by line (scanning), it will be much slower with random access.
|
||||||
|
//
|
||||||
|
// For the `Progress()` method to work correctly, every pixel should be queried exactly once.
|
||||||
|
//
|
||||||
|
// At(Bounds().Min.X, Bounds().Min.Y) // returns the top-left pixel of the image.
|
||||||
|
// At(Bounds().Max.X-1, Bounds().Max.Y-1) // returns the bottom-right pixel.
|
||||||
|
//
|
||||||
|
// This is not thread safe, don't call from several goroutines!
|
||||||
|
func (si *StitchedImage) At(x, y int) color.Color {
|
||||||
|
p := image.Point{x, y}
|
||||||
|
|
||||||
|
// Assume that every pixel is only queried once.
|
||||||
|
si.queryCounter++
|
||||||
|
|
||||||
|
// Check if cached image needs to be regenerated.
|
||||||
|
if !p.In(si.cacheImage.Bounds()) {
|
||||||
|
rect := si.Bounds()
|
||||||
|
// TODO: Redo how the cache image rect is generated
|
||||||
|
rect.Min.Y = divideFloor(y, si.cacheHeight) * si.cacheHeight
|
||||||
|
rect.Max.Y = rect.Min.Y + si.cacheHeight
|
||||||
|
|
||||||
|
si.regenerateCache(rect)
|
||||||
|
}
|
||||||
|
|
||||||
|
return si.cacheImage.RGBAAt(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opaque returns whether the image is fully opaque.
|
||||||
|
//
|
||||||
|
// For more speed and smaller file size, StitchedImage will be marked as non-transparent.
|
||||||
|
// This will speed up image saving by 2x, as there is no need to iterate over the whole image to find a single non opaque pixel.
|
||||||
|
func (si *StitchedImage) Opaque() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress returns the approximate progress of any process that scans the image from top to bottom.
|
||||||
|
func (si *StitchedImage) Progress() (value, max int) {
|
||||||
|
size := si.Bounds().Size()
|
||||||
|
|
||||||
|
return si.queryCounter, size.X * size.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
// regenerateCache will regenerate the cache image at the given rectangle.
|
||||||
|
func (si *StitchedImage) regenerateCache(rect image.Rectangle) {
|
||||||
|
cacheImage := image.NewRGBA(rect)
|
||||||
|
|
||||||
|
// List of tiles that intersect with the to be generated cache image.
|
||||||
|
intersectingTiles := []*ImageTile{}
|
||||||
|
for i, tile := range si.tiles {
|
||||||
|
if tile.Bounds().Overlaps(rect) {
|
||||||
|
tilePtr := &si.tiles[i]
|
||||||
|
intersectingTiles = append(intersectingTiles, tilePtr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start worker threads.
|
||||||
|
workerQueue := make(chan image.Rectangle)
|
||||||
|
waitGroup := sync.WaitGroup{}
|
||||||
|
for i := 0; i < runtime.NumCPU(); i++ {
|
||||||
|
waitGroup.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer waitGroup.Done()
|
||||||
|
for workload := range workerQueue {
|
||||||
|
// List of tiles that intersect with the workload chunk.
|
||||||
|
workloadTiles := []*ImageTile{}
|
||||||
|
|
||||||
|
// Get only the tiles that intersect with the destination image bounds.
|
||||||
|
for _, tile := range intersectingTiles {
|
||||||
|
if tile.Bounds().Overlaps(workload) {
|
||||||
|
workloadTiles = append(workloadTiles, tile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blend tiles into image at the workload rectangle.
|
||||||
|
si.blendFunc(workloadTiles, cacheImage.SubImage(workload).(*image.RGBA))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Divide rect into chunks and push to workers.
|
||||||
|
for _, chunk := range gridifyRectangle(rect, StitchedImageCacheGridSize) {
|
||||||
|
workerQueue <- chunk
|
||||||
|
}
|
||||||
|
close(workerQueue)
|
||||||
|
|
||||||
|
// Wait until all worker threads are done.
|
||||||
|
waitGroup.Wait()
|
||||||
|
|
||||||
|
// Draw overlays.
|
||||||
|
for _, overlay := range si.overlays {
|
||||||
|
if overlay != nil {
|
||||||
|
overlay.Draw(cacheImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cached image.
|
||||||
|
si.cacheImage = cacheImage
|
||||||
|
}
|
@ -64,8 +64,9 @@ func gridifyRectangle(rect image.Rectangle, gridSize int) (result []image.Rectan
|
|||||||
for y := divideFloor(rect.Min.Y, gridSize); y < divideCeil(rect.Max.Y, gridSize); y++ {
|
for y := divideFloor(rect.Min.Y, gridSize); y < divideCeil(rect.Max.Y, gridSize); y++ {
|
||||||
for x := divideFloor(rect.Min.X, gridSize); x < divideCeil(rect.Max.X, gridSize); x++ {
|
for x := divideFloor(rect.Min.X, gridSize); x < divideCeil(rect.Max.X, gridSize); x++ {
|
||||||
tempRect := image.Rect(x*gridSize, y*gridSize, (x+1)*gridSize, (y+1)*gridSize)
|
tempRect := image.Rect(x*gridSize, y*gridSize, (x+1)*gridSize, (y+1)*gridSize)
|
||||||
if tempRect.Overlaps(rect) {
|
intersection := tempRect.Intersect(rect)
|
||||||
result = append(result, tempRect)
|
if !intersection.Empty() {
|
||||||
|
result = append(result, intersection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user