mirror of
https://github.com/Dadido3/noita-mapcap.git
synced 2024-11-18 17:17:31 +00:00
Add first sketch of Deep Zoom Image exporter
This commit is contained in:
parent
915da73845
commit
7a6915480b
153
bin/stitch/dzi.go
Normal file
153
bin/stitch/dzi.go
Normal file
@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2023 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
dziTileSize = 512 // The (maximum) width and height of a tile in pixels, not including the overlap.
|
||||
dziOverlap = 1 // The amount of additional pixels on every side of every tile. The real (max) width/height of an image is `2*overlap + tileSize`.
|
||||
)
|
||||
|
||||
type DZI struct {
|
||||
stitchedImage *StitchedImage
|
||||
|
||||
fileExtension string
|
||||
|
||||
tileSize int // The (maximum) width and height of a tile in pixels, not including the overlap.
|
||||
overlap int // The amount of additional pixels on every side of every tile. The real (max) width/height of an image is `2*overlap + tileSize`.
|
||||
|
||||
maxZoomLevel int // The maximum zoom level that is needed.
|
||||
}
|
||||
|
||||
func NewDZI(stitchedImage *StitchedImage) DZI {
|
||||
dzi := DZI{
|
||||
stitchedImage: stitchedImage,
|
||||
|
||||
fileExtension: ".png",
|
||||
|
||||
overlap: dziOverlap,
|
||||
tileSize: dziTileSize,
|
||||
}
|
||||
|
||||
width, height := stitchedImage.bounds.Dx(), stitchedImage.bounds.Dy()
|
||||
|
||||
// Calculate max zoom level and stuff.
|
||||
neededLength := max(width, height)
|
||||
var sideLength int = 1
|
||||
var level int
|
||||
for sideLength < neededLength {
|
||||
level += 1
|
||||
sideLength *= 2
|
||||
}
|
||||
dzi.maxZoomLevel = level
|
||||
//dzi.maxZoomLevelLength = sideLength
|
||||
|
||||
return dzi
|
||||
}
|
||||
|
||||
// ExportDZIDescriptor exports the descriptive JSON file at the given path.
|
||||
func (d DZI) ExportDZIDescriptor(outputPath string) error {
|
||||
log.Printf("Creating DZI descriptor %q.", outputPath)
|
||||
|
||||
f, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// Prepare data that describes the layout of the image files.
|
||||
var dziDescriptor struct {
|
||||
Image struct {
|
||||
XMLNS string `json:"xmlns"`
|
||||
Format string
|
||||
Overlap string
|
||||
TileSize string
|
||||
Size struct {
|
||||
Width string
|
||||
Height string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dziDescriptor.Image.XMLNS = "http://schemas.microsoft.com/deepzoom/2008"
|
||||
dziDescriptor.Image.Format = "png"
|
||||
dziDescriptor.Image.Overlap = strconv.Itoa(d.overlap)
|
||||
dziDescriptor.Image.TileSize = strconv.Itoa(d.tileSize)
|
||||
dziDescriptor.Image.Size.Width = strconv.Itoa(d.stitchedImage.bounds.Dx())
|
||||
dziDescriptor.Image.Size.Height = strconv.Itoa(d.stitchedImage.bounds.Dy())
|
||||
|
||||
jsonEnc := json.NewEncoder(f)
|
||||
return jsonEnc.Encode(dziDescriptor)
|
||||
}
|
||||
|
||||
// ExportDZITiles exports the single image tiles for every zoom level.
|
||||
func (d DZI) ExportDZITiles(outputDir string) error {
|
||||
log.Printf("Creating DZI tiles in %q.", outputDir)
|
||||
|
||||
// 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.
|
||||
|
||||
// The current stitched image we are working with.
|
||||
stitchedImage := d.stitchedImage
|
||||
|
||||
for zoomLevel := d.maxZoomLevel; zoomLevel >= 0; zoomLevel-- {
|
||||
|
||||
levelBasePath := filepath.Join(outputDir, fmt.Sprintf("%d", zoomLevel))
|
||||
if err := os.Mkdir(levelBasePath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create zoom level base directory %q: %w", levelBasePath, err)
|
||||
}
|
||||
|
||||
// Store list of tiles, so that we can reuse them in the next step for the smaller zoom level.
|
||||
imageTiles := ImageTiles{}
|
||||
|
||||
// Export tiles.
|
||||
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 := exportPNG(img, filePath); err != nil {
|
||||
return fmt.Errorf("failed to export PNG: %w", err)
|
||||
}
|
||||
|
||||
scaleDivider := 2
|
||||
imageTiles = append(imageTiles, ImageTile{
|
||||
fileName: filePath,
|
||||
modTime: time.Now(),
|
||||
scaleDivider: scaleDivider,
|
||||
image: image.Rect(DivideFloor(img.Bounds().Min.X, scaleDivider), DivideFloor(img.Bounds().Min.Y, scaleDivider), DivideCeil(img.Bounds().Max.X, scaleDivider), DivideCeil(img.Bounds().Max.Y, scaleDivider)),
|
||||
imageMutex: &sync.RWMutex{},
|
||||
invalidationChan: make(chan struct{}, 1),
|
||||
timeoutChan: make(chan struct{}, 1),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run NewStitchedImage(): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
38
bin/stitch/export-dzi.go
Normal file
38
bin/stitch/export-dzi.go
Normal file
@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2023 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func exportDZI(stitchedImage *StitchedImage, outputPath string) error {
|
||||
descriptorPath := outputPath
|
||||
extension := filepath.Ext(outputPath)
|
||||
outputTilesPath := strings.TrimSuffix(outputPath, extension) + "_files"
|
||||
|
||||
dzi := NewDZI(stitchedImage)
|
||||
|
||||
// Create base directory of all DZI files.
|
||||
if err := os.MkdirAll(outputTilesPath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
// Export DZI descriptor.
|
||||
if err := dzi.ExportDZIDescriptor(descriptorPath); err != nil {
|
||||
return fmt.Errorf("failed to export DZI descriptor: %w", err)
|
||||
}
|
||||
|
||||
// Export DZI tiles.
|
||||
if err := dzi.ExportDZITiles(outputTilesPath); err != nil {
|
||||
return fmt.Errorf("failed to export DZI tiles: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2019-2022 David Vogel
|
||||
// Copyright (c) 2019-2023 David Vogel
|
||||
//
|
||||
// This software is released under the MIT License.
|
||||
// https://opensource.org/licenses/MIT
|
||||
@ -276,6 +276,10 @@ func main() {
|
||||
if err := exportJPEG(stitchedImage, *flagOutputPath); err != nil {
|
||||
log.Panicf("Export of JPEG file failed: %v", err)
|
||||
}
|
||||
case ".dzi":
|
||||
if err := exportDZI(stitchedImage, *flagOutputPath); err != nil {
|
||||
log.Panicf("Export of DZI file failed: %v", err)
|
||||
}
|
||||
default:
|
||||
log.Panicf("Unknown output format %q.", fileExtension)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user