libvips / pyvips

python binding for libvips using cffi
MIT License
648 stars 50 forks source link

question: how do i split a tilesheet into multiple grid and merge them together? #357

Open scarf005 opened 2 years ago

scarf005 commented 2 years ago

I need to work with a tilesheet, composed of 10x10 tiles. for example, a 160x2560 image would contain 16x256=4096 tiles. after that, I need to merge those 4096 10x10 images back into 160x2560 image. I tried this:

#!/usr/bin/python3

from pathlib import Path
from pyvips import Image, Region

base = Path("ASCIITileset")
tiles = base / "ASCIITiles.png"
fallback = base / "fallback.png"
config = base / "tile_config.json"

image = Image.new_from_file(fallback, memory=True)  # type: ignore

def fetch(region: Region, x: int, y: int, size: int):
    return region.fetch(size * x, size * y, size, size)

patch_size = 10
width: int = image.width
height: int = image.height
rows, cols = height // patch_size, width // patch_size

region = Region.new(image)
print(f"{width=}, {height=}, {patch_size=}, {rows=}, {cols=}")
# res = image.grid(patch_size, rows, cols)
print(rows, cols)
Path("patches").mkdir(exist_ok=True)

for y in range(rows):
    for x in range(cols):
        print(f"({x}, {y})")
        patch = fetch(region, x, y, patch_size)
        img = Image.new_from_buffer(patch, "")
        img.write_to_file(f"patches/{y}-{x}.png")

but encountered with:

width=160, height=2560, patch_size=10, rows=256, cols=16
256 16
(0, 0)
Traceback (most recent call last):
  File "/home/scarf/repo/cata/tileset-tools/learnvips.py", line 43, in <module>
    img = Image.new_from_buffer(patch, "")
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/scarf/.asdf/installs/python/3.11.0rc2/lib/python3.11/site-packages/pyvips/vimage.py", line 384, in new_from_buffer
    pointer = vips_lib.vips_foreign_find_load_buffer(data, len(data))
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: initializer for ctype 'void *' must be a cdata pointer, not _cffi_backend.buffer
jcupitt commented 2 years ago

Hi @scarf005,

You can use dzsave to split an image up, and arrayjoin to reassemble it. For example:

$ vips dzsave ../astronauts.png x --depth one --tile-size 10 --overlap 0 
$ ls x_files/0 | wc
   8100    8100   87300

It made 8100 jpeg tiles (90 x 90). I can reassemble with:

$ cd x_files/0/
$ vips arrayjoin "$(ls *.jpeg | sort -t_ -k2g -k1g)" ../x.jpg --across 90
scarf005 commented 2 years ago

thank you, but would there be a way to do that using pyvips api? i need to use them in python script i'm writing.

jcupitt commented 2 years ago

Sure, it's the same. Eg. (untested):

image.dzsave("x", depth="one", tile_size=10, overlap=0)

And:

tiles = [pyvips.Image.new_from_file(f"{x}_{y}.jpeg") for y in range(down) for x in range(across)]
image = pyvips.Image.arrayjoin(tiles, across=90)

(I might have the x and y loops swapped, I always forget which way py list comprehensions do nesting)

scarf005 commented 2 years ago

Thank you, it works. however I've encountered thse problems:

image

  1. can't remove _files/0 part. for example if i run image.dzsave(Path("patch"), ...), it saves tiles in patch_files/0/.

could there be a way to save images at just patch/?

  1. custom formatting for tilenames? for example, it's 0_0.png, 0_1.png, and so on. would there be a way to format them? or should i manually rename after file generation? the reasoning is that i have a large list of names mapped into specific tileset index, for example

        { "id": ["player_female", "player_male"], "fg": 144, "rotates": false },
        { "id": ["t_open_air", "t_open_air_rooved"], "bg": 811 },
        { "id": "lighting_hidden", "fg": 1067, "bg": 1067, "rotates": false },

    fg and bg points at specific tile location. I'll have to rename the resulting filename accordingly.

  2. can it also horizontally split images? for example, let's day I have 120 500 pixel images. can i split it into 5 120 100 images, or would there be another method to split them?

  3. save the splitted tiles into memory? it would help a lot since my SSD is wearing down fast.

also, your example is correct since (...) for y in (...) for x in (...) is same as

for y in (...):
    for x in (...):
        (...)

for example:

In [3]: [(y, x) for y in range(2) for x in 'ab']
Out[3]: [(0, 'a'), 
         (0, 'b'), 
         (1, 'a'), 
         (1, 'b')]
jcupitt commented 2 years ago

It supports deepzoom, zoomify and gmaps naming convention, so z/x_y, nnn/z_x_y and z/y/x, but that's it. You'll need to run a second pass to rename the files.

It can write an uncompressed zip, which will save a lot of time and disc space, especially on windows. That might be worth checking. You can write to a zip in memory and then use any standard python zip handler to pull tiles out efficiently.

Square tiles only, sorry.