Skip to content

Commit

Permalink
Several changes
Browse files Browse the repository at this point in the history
- Add compatibility for newest Noita beta
- Modify STREAMING_CHUNK_TARGET, GRID_MAX_UPDATES_PER_FRAME and GRID_MIN_UPDATES_PER_FRAME magic numbers for a more robust capturing process
- Add LimitGroup to util.go
- Add webp-level command line flag to define the webp compression level
- Rework progress bar to make it work in DZI export mode
- Refactor image exporter functions
- Use LimitGroup to make DZI export multithreaded
- Add BlendMethodFast which doesn't mix tile pixels
- Up Go version to 1.22
- Use Dadido3/go-libwebp for WebP encoding
  • Loading branch information
Dadido3 committed Feb 7, 2024
1 parent 47d5700 commit b1a1087
Show file tree
Hide file tree
Showing 15 changed files with 301 additions and 69 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: ^1.21
go-version: ^1.22

- name: Check out code into the Go module directory
uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: ^1.21
go-version: ^1.22

- name: Check out code into the Go module directory
uses: actions/checkout@v2
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"basicfont",
"bytecode",
"cheggaaa",
"Dadido",
"dofile",
"dont",
"Downscales",
Expand All @@ -26,6 +27,7 @@
"Lanczos",
"lann",
"ldflags",
"libwebp",
"linearize",
"longleg",
"lowram",
Expand Down Expand Up @@ -57,6 +59,7 @@
"Vogel",
"Voronoi",
"webp",
"wepb",
"xmax",
"xmin",
"ymax",
Expand Down
4 changes: 3 additions & 1 deletion bin/stitch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ example list of files:
If set to 1, only the newest tile will be used for any resulting pixel.
Use 1 to prevent ghosting and blurry objects.
- `input string`
The source path of the image tiles to be stitched. Defaults to "./..//..//output")
The source path of the image tiles to be stitched. Defaults to "./..//..//output"
- `entities string`
The path to the `entities.json` file. This contains Noita specific entity data. Defaults to "./../../output/entities.json".
- `player-path string`
Expand All @@ -48,6 +48,8 @@ example list of files:
The size of the resulting deep zoom image (DZI) tiles in pixels. Defaults to 512.
- `dzi-tile-overlap`
The number of additional pixels around every deep zoom image (DZI) tile. Defaults to 2.
- `wepb-level`
Compression level of WebP files, from 0 (fast) to 9 (slow, best compression). Defaults to 8.
- `xmax int`
Right bound of the output rectangle. This coordinate is not included in the output.
- `xmin int`
Expand Down
19 changes: 17 additions & 2 deletions bin/stitch/blend-methods.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2022 David Vogel
// Copyright (c) 2022-2024 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
Expand All @@ -8,6 +8,7 @@ package main
import (
"image"
"image/color"
"image/draw"
"math"
"sort"
)
Expand Down Expand Up @@ -106,7 +107,7 @@ func (b BlendMethodVoronoi) Draw(tiles []*ImageTile, destImage *image.RGBA) {
images = append(images, tile.GetImage())
}

// Create arrays to be reused every pixel.
// Create color variables reused every pixel.
var col color.RGBA
var centerDistSqrMin int

Expand Down Expand Up @@ -147,3 +148,17 @@ func (b BlendMethodVoronoi) Draw(tiles []*ImageTile, destImage *image.RGBA) {
}
}
}

// BlendMethodFast just draws all tiles into the destination image.
// No mixing is done, and this is very fast when there is no or minimal tile overlap.
type BlendMethodFast struct{}

// Draw implements the StitchedImageBlendMethod interface.
func (b BlendMethodFast) Draw(tiles []*ImageTile, destImage *image.RGBA) {
for _, tile := range tiles {
if image := tile.GetImage(); image != nil {
bounds := image.Bounds()
draw.Draw(destImage, bounds, image, bounds.Min, draw.Src)
}
}
}
63 changes: 57 additions & 6 deletions bin/stitch/dzi.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ import (
"log"
"os"
"path/filepath"
"runtime"
"strconv"
"sync"
"sync/atomic"
"time"

"github.com/cheggaaa/pb/v3"
)

type DZI struct {
Expand Down Expand Up @@ -99,9 +103,49 @@ func (d DZI) ExportDZIDescriptor(outputPath string) error {
}

// ExportDZITiles exports the single image tiles for every zoom level.
func (d DZI) ExportDZITiles(outputDir string) error {
func (d DZI) ExportDZITiles(outputDir string, bar *pb.ProgressBar, webPLevel int) error {
log.Printf("Creating DZI tiles in %q.", outputDir)

const scaleDivider = 2

var exportedTiles atomic.Int64

// If there is a progress bar, start a goroutine that regularly updates it.
// We will base that on the number of exported tiles.
if bar != nil {

// Count final number of tiles.
bounds := d.stitchedImage.bounds
var finalTiles int64
for zoomLevel := d.maxZoomLevel; zoomLevel >= 0; zoomLevel-- {
for iY := 0; iY <= (bounds.Dy()-1)/d.tileSize; iY++ {
for iX := 0; iX <= (bounds.Dx()-1)/d.tileSize; iX++ {
finalTiles++
}
}
bounds = image.Rect(DivideFloor(bounds.Min.X, scaleDivider), DivideFloor(bounds.Min.Y, scaleDivider), DivideCeil(bounds.Max.X, scaleDivider), DivideCeil(bounds.Max.Y, scaleDivider))
}
bar.SetRefreshRate(250 * time.Millisecond).SetTotal(finalTiles).Start()

done := make(chan struct{})
defer func() {
done <- struct{}{}
bar.SetCurrent(bar.Total()).Finish()
}()

go func() {
ticker := time.NewTicker(250 * time.Millisecond)
for {
select {
case <-done:
return
case <-ticker.C:
bar.SetCurrent(exportedTiles.Load())
}
}
}()
}

// Start with the highest zoom level (Where every world pixel is exactly mapped into one image pixel).
// Generate all tiles for this level, and then stitch another image (scaled down by a factor of 2) based on the previously generated tiles.
// Repeat this process until we have generated level 0.
Expand All @@ -120,18 +164,24 @@ func (d DZI) ExportDZITiles(outputDir string) error {
imageTiles := ImageTiles{}

// Export tiles.
lg := NewLimitGroup(runtime.NumCPU())
for iY := 0; iY <= (stitchedImage.bounds.Dy()-1)/d.tileSize; iY++ {
for iX := 0; iX <= (stitchedImage.bounds.Dx()-1)/d.tileSize; iX++ {
rect := image.Rect(iX*d.tileSize, iY*d.tileSize, iX*d.tileSize+d.tileSize, iY*d.tileSize+d.tileSize)
rect = rect.Add(stitchedImage.bounds.Min)
rect = rect.Inset(-d.overlap)
img := stitchedImage.SubStitchedImage(rect)
filePath := filepath.Join(levelBasePath, fmt.Sprintf("%d_%d%s", iX, iY, d.fileExtension))
if err := exportWebPSilent(img, filePath); err != nil {
return fmt.Errorf("failed to export WebP: %w", err)
}

scaleDivider := 2
lg.Add(1)
go func() {
defer lg.Done()
if err := exportWebP(img, filePath, webPLevel); err != nil {
log.Printf("Failed to export WebP: %v", err)
}
exportedTiles.Add(1)
}()

imageTiles = append(imageTiles, ImageTile{
fileName: filePath,
modTime: time.Now(),
Expand All @@ -143,11 +193,12 @@ func (d DZI) ExportDZITiles(outputDir string) error {
})
}
}
lg.Wait()

// Create new stitched image from the previously exported tiles.
// The tiles are already created in a way, that they are scaled down by a factor of 2.
var err error
stitchedImage, err = NewStitchedImage(imageTiles, imageTiles.Bounds(), BlendMethodMedian{BlendTileLimit: 0}, 128, nil)
stitchedImage, err = NewStitchedImage(imageTiles, imageTiles.Bounds(), BlendMethodFast{}, 128, nil)
if err != nil {
return fmt.Errorf("failed to run NewStitchedImage(): %w", err)
}
Expand Down
8 changes: 5 additions & 3 deletions bin/stitch/export-dzi.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023 David Vogel
// Copyright (c) 2023-2024 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
Expand All @@ -10,9 +10,11 @@ import (
"os"
"path/filepath"
"strings"

"github.com/cheggaaa/pb/v3"
)

func exportDZI(stitchedImage *StitchedImage, outputPath string, dziTileSize, dziOverlap int) error {
func exportDZIStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar, dziTileSize, dziOverlap int, webPLevel int) error {
descriptorPath := outputPath
extension := filepath.Ext(outputPath)
outputTilesPath := strings.TrimSuffix(outputPath, extension) + "_files"
Expand All @@ -30,7 +32,7 @@ func exportDZI(stitchedImage *StitchedImage, outputPath string, dziTileSize, dzi
}

// Export DZI tiles.
if err := dzi.ExportDZITiles(outputTilesPath); err != nil {
if err := dzi.ExportDZITiles(outputTilesPath, bar, webPLevel); err != nil {
return fmt.Errorf("failed to export DZI tiles: %w", err)
}

Expand Down
39 changes: 34 additions & 5 deletions bin/stitch/export-jpeg.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023 David Vogel
// Copyright (c) 2023-2024 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
Expand All @@ -11,15 +11,44 @@ import (
"image/jpeg"
"log"
"os"
"time"

"github.com/cheggaaa/pb/v3"
)

func exportJPEG(stitchedImage image.Image, outputPath string) error {
func exportJPEGStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar) error {
log.Printf("Creating output file %q.", outputPath)

return exportJPEGSilent(stitchedImage, outputPath)
// If there is a progress bar, start a goroutine that regularly updates it.
// We will base the progress on the number of pixels read from the stitched image.
if bar != nil {
_, max := stitchedImage.Progress()
bar.SetRefreshRate(250 * time.Millisecond).SetTotal(int64(max)).Start()

done := make(chan struct{})
defer func() {
done <- struct{}{}
bar.SetCurrent(bar.Total()).Finish()
}()

go func() {
ticker := time.NewTicker(250 * time.Millisecond)
for {
select {
case <-done:
return
case <-ticker.C:
value, max := stitchedImage.Progress()
bar.SetCurrent(int64(value)).SetTotal(int64(max))
}
}
}()
}

return exportJPEG(stitchedImage, outputPath)
}

func exportJPEGSilent(stitchedImage image.Image, outputPath string) error {
func exportJPEG(img image.Image, outputPath string) error {
f, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
Expand All @@ -30,7 +59,7 @@ func exportJPEGSilent(stitchedImage image.Image, outputPath string) error {
Quality: 80,
}

if err := jpeg.Encode(f, stitchedImage, options); err != nil {
if err := jpeg.Encode(f, img, options); err != nil {
return fmt.Errorf("failed to encode image %q: %w", outputPath, err)
}

Expand Down
39 changes: 34 additions & 5 deletions bin/stitch/export-png.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2023 David Vogel
// Copyright (c) 2023-2024 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
Expand All @@ -11,15 +11,44 @@ import (
"image/png"
"log"
"os"
"time"

"github.com/cheggaaa/pb/v3"
)

func exportPNG(stitchedImage image.Image, outputPath string) error {
func exportPNGStitchedImage(stitchedImage *StitchedImage, outputPath string, bar *pb.ProgressBar) error {
log.Printf("Creating output file %q.", outputPath)

return exportPNGSilent(stitchedImage, outputPath)
// If there is a progress bar, start a goroutine that regularly updates it.
// We will base the progress on the number of pixels read from the stitched image.
if bar != nil {
_, max := stitchedImage.Progress()
bar.SetRefreshRate(250 * time.Millisecond).SetTotal(int64(max)).Start()

done := make(chan struct{})
defer func() {
done <- struct{}{}
bar.SetCurrent(bar.Total()).Finish()
}()

go func() {
ticker := time.NewTicker(250 * time.Millisecond)
for {
select {
case <-done:
return
case <-ticker.C:
value, max := stitchedImage.Progress()
bar.SetCurrent(int64(value)).SetTotal(int64(max))
}
}
}()
}

return exportPNG(stitchedImage, outputPath)
}

func exportPNGSilent(stitchedImage image.Image, outputPath string) error {
func exportPNG(img image.Image, outputPath string) error {
f, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
Expand All @@ -30,7 +59,7 @@ func exportPNGSilent(stitchedImage image.Image, outputPath string) error {
CompressionLevel: png.DefaultCompression,
}

if err := encoder.Encode(f, stitchedImage); err != nil {
if err := encoder.Encode(f, img); err != nil {
return fmt.Errorf("failed to encode image %q: %w", outputPath, err)
}

Expand Down
Loading

0 comments on commit b1a1087

Please sign in to comment.