2

I'm trying to read a '.waltex' image, which is a 'walaber image'. It's basically just in raw image format. The problem is, it uses 'RGBA8888', 'RGBA4444', 'RGB565' and 'RGB5551' (all of which can be determined from the header), and I could not find a way to use these color specs in PIL.

I've tried doing this

from PIL import Image

with open('Carl.waltex', 'rb') as file:
    rawdata = file.read()

image = Image.frombytes('RGBA', (1024,1024), rawdata, 'raw', 'RGBA;4B')
image.show()

I've tried all the 16-bit raw modes in the last input, and I couldn't find a single one that worked. To be clear, this file is specifically 'RGBA4444' with little endian, 8 bytes per pixel.

If you need the file, then I can link it.

  • 1
    Please link the file so I can replicate the error – Pedro Rocha Feb 17 '23 at 23:50
  • Here's the file https://mega.nz/file/4B1C3CDC#gOZrbFnvOVtVaOwaP43ANXirc8zZ4bNB6J2Qx6Pa0qc – ego-lay atman-bay Feb 17 '23 at 23:56
  • 1
    What is this .waltex format? I've never heard of it and I can't find any information. How would you open it in your computer for example? – Pedro Rocha Feb 18 '23 at 00:01
  • It's a format used in where's my water games. It was created by one of the devs, Walaber. You wouldn't normally be able to read it on your computer, but if you go to https://rawpixels.net/ and load the file, then use these parameters, width: 1024, height: 1024, offset: 16, Pixel Format: RGBA, ignore alpha: False (unticked), bpp1:4, bpp2:4, bpp3:4, bpp4:4, Little Endian: True. Like I said, it's a raw pixel format, so I would think I'd be able to open it with PIL. – ego-lay atman-bay Feb 18 '23 at 00:07

2 Answers2

3

Updated Answer

I have made some changes to my original code so that:

  • you can pass a filename to read as parameter
  • it parses the header and checks the magic string and derives the format (RGBA8888 or RGBA4444) and height and width automatically
  • it now handles RGBA8888 like your newly-shared sample image

So, it looks like this:

#!/usr/bin/env python3

import struct
import sys
import numpy as np
from PIL import Image

def loadWaltex(filename):

    # Open input file in binary mode
    with open(filename, 'rb') as fd:
        # Read 16 byte header and extract metadata
        # https://zenhax.com/viewtopic.php?t=14164
        header = fd.read(16)
        magic, vers, fmt, w, h, _ = struct.unpack('4sBBHH6s', header)
        if magic != b'WALT':
            sys.exit(f'ERROR: {filename} does not start with "WALT" magic string')

        # Check if fmt=0 (RGBA8888) or fmt=3 (RGBA4444)
        if fmt == 0:
            fmtdesc = "RGBA8888"
            # Read remainder of file (part following header)
            data = np.fromfile(fd, dtype=np.uint8)
            R = data[0::4].reshape((h,w))
            G = data[1::4].reshape((h,w))
            B = data[2::4].reshape((h,w))
            A = data[3::4].reshape((h,w))
            # Stack the channels to make RGBA image
            RGBA = np.dstack((R,G,B,A))
        else:
            fmtdesc = "RGBA4444"
            # Read remainder of file (part following header)
            data = np.fromfile(fd, dtype=np.uint16).reshape((h,w))
            # Split the RGBA444 out from the uint16
            R = (data>>12) & 0xf
            G = (data>>8) & 0xf
            B = (data>>4) & 0xf
            A = data & 0xf
            # Stack the channels to make RGBA image
            RGBA = np.dstack((R,G,B,A)).astype(np.uint8) << 4

        # Debug info for user
        print(f'Filename: {filename}, version: {vers}, format: {fmtdesc} ({fmt}), w: {w}, h: {h}')
    
        # Make into PIL Image
        im = Image.fromarray(RGBA)
        return im

if __name__ == "__main__":
    # Load image specified by first parameter
    im = loadWaltex(sys.argv[1])
    im.save('result.png')

And when you run it with:

./decodeRGBA.py objects.waltex

You get:

enter image description here

The debug output for your two sample images is:

Filename: Carl.waltex, version: 1, format: RGBA4444 (3), w: 1024, h: 1024
Filename: objects.waltex, version: 1, format: RGBA8888 (0), w: 256, h: 1024

Original Answer

I find using Numpy is the easiest approach for this type of thing, and it is also highly performant:

#!/usr/bin/env python3

import numpy as np
from PIL import Image

# Define the known parameters of the image and read into Numpy array
h, w, offset = 1024, 1024, 16
data = np.fromfile('Carl.waltex', dtype=np.uint16, offset=offset).reshape((h,w))

# Split the RGBA4444 out from the uint16
R = (data >> 12) & 0xf
G = (data >>  8) & 0xf
B = (data >>  4) & 0xf
A =  data        & 0xf

# Stack the 4 individual channels to make an RGBA image
RGBA = np.dstack((R,G,B,A)).astype(np.uint8) << 4

# Make into PIL Image
im = Image.fromarray(RGBA)
im.save('result.png')

enter image description here


Note: Your image has 16 bytes of padding at the start. Sometimes that amount is variable. A useful technique in that case is to read the entire file, work out how many useful samples of pixel data there are (in your case 1024*1024), and then slice the data to take the last N samples - thereby ignoring any variable padding at the start. That would look like this:

# Define the known parameters of the image and read into Numpy array
h, w = 1024, 1024
data = np.fromfile('Carl.waltex', dtype=np.uint16)[-h*w:].reshape((h,w))

If you don't like Numpy and prefer messing about with lists and structs, you can get exactly the same result like this:

#!/usr/bin/env python3

import struct
from PIL import Image

# Define the known parameters of the image
h, w, offset = 1024, 1024, 16
data = open('Carl.waltex', 'rb').read()[offset:]

# Unpack into bunch of h*w unsigned shorts
uint16s = struct.unpack("H" * h *w, data)

# Build a list of RGBA tuples
pixels = []
for RGBA4444 in uint16s:
    R = (RGBA4444 >> 8) & 0xf0
    G = (RGBA4444 >> 4) & 0xf0
    B =  RGBA4444       & 0xf0
    A = ( RGBA4444      & 0xf) << 4
    pixels.append((R,G,B,A))

# Push the list of RGBA tuples into an empty image
RGBA = Image.new('RGBA', (w,h))
RGBA.putdata(pixels)
RGBA.save('result.png')

Note that the Numpy approach is 60x faster than the list-based approach:

Numpy: 3.6 ms ± 73.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
listy: 213 ms ± 712 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)

Note: These images and the waltex file format seem to be from the games "Where's My Water?" and "Where's My Perry?". I got some hints as to the header format from ZENHAX.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • I know there's a 16 byte offset at the beginning, that's the header. The header is where I can get the format, and size of the image. But anyway, thanks. I'll try that out. – ego-lay atman-bay Feb 18 '23 at 15:51
  • It wasn't clear from your question that you wanted code to decode the header, so do you? If so, it's pretty simple and detailed here https://zenhax.com/viewtopic.php?t=14164 – Mark Setchell Feb 18 '23 at 17:16
  • I know how to decode the header, I found some info about it a while ago, so I know the structure. If you want to know, here it is```"WALT" magic string, version number (1 byte), image format (1 byte), image width (2-byte unsigned short), image height (2-byte unsigned short),padding (6-bytes)``` – ego-lay atman-bay Feb 18 '23 at 17:21
  • Cool, see if you can do that bit yourself and if not, I'll add it into my answer tomorrow. – Mark Setchell Feb 18 '23 at 17:26
  • oh, I actually already have code to read the file, the problem is, it loops through every byte, which takes a long time, and I knew there's a faster way, that's why I wanted to use Pillow to read the image. – ego-lay atman-bay Feb 18 '23 at 17:43
  • Cool, well hopefully the 3ms it takes with Numpy will prove a satisfactory answer for you! – Mark Setchell Feb 18 '23 at 17:56
  • I'm now running into the issue with reading a big endian rgba8888 image (waltex images can be different formats). – ego-lay atman-bay Feb 18 '23 at 19:31
  • You need to share a big-endian example then please. And state how you know it's big-endian too please. – Mark Setchell Feb 18 '23 at 19:55
  • I know it's big endian because I used my previous (slow) script to read it, using big endian to make it look like it does in the game it comes from. Here's the link to an rgba8888 big endian file (the header is not big endian though) https://mega.nz/file/YIsR1SJI#EHCHdUWd1G80qZ7oAbjeh81u5hlv5FVUmlyR4Nmqqaw (this one is 256x1024) – ego-lay atman-bay Feb 18 '23 at 21:20
  • It's late here in the UK, I'll try and take a look tomorrow. – Mark Setchell Feb 18 '23 at 21:55
1

Now that I have a better idea of how your Waltex files work, I attempted to write a custom PIL Plugin for them - a new experience for me. I've put it as a different answer because the approach is very different.

You use it very simply like this:

from PIL import Image
import WaltexImagePlugin

im = Image.open('objects.waltex')
im.show()

You need to save the following as WaltexImagePlugin.py in the directory beside your main Python program:

from PIL import Image, ImageFile
import struct

def _accept(prefix):
    return prefix[:4] == b"WALT"

class WaltexImageFile(ImageFile.ImageFile):

    format = "Waltex"
    format_description = "Waltex texture image"

    def _open(self):
        header = self.fp.read(HEADER_LENGTH)
        magic, vers, fmt, w, h, _ = struct.unpack('4sBBHH6s', header)

        # size in pixels (width, height)
        self._size = w, h

        # mode setting
        self.mode = 'RGBA'

        # Decoder
        if fmt == 0:
            # RGBA8888
            # Just use built-in raw decoder
            self.tile = [("raw", (0, 0) + self.size, HEADER_LENGTH, (self.mode, 
0, 1))]
        elif fmt == 3:
            # RGBA4444
            # Use raw decoder with custom RGBA;4B unpacker
            self.tile = [("raw", (0, 0) + self.size, HEADER_LENGTH, ('RGBA;4B', 
0, 1))]


Image.register_open(WaltexImageFile.format, WaltexImageFile, _accept)

Image.register_extensions(
    WaltexImageFile.format,
    [
        ".waltex"
    ],
)

HEADER_LENGTH = 16

It works perfectly for your RGBA888 images, but cannot quite handle the byte ordering of your RGBA444 file, so you need to reverse it for those images. I used this:

...
...
im = Image.open(...)

# Split channels and recombine in correct order
a, b, c, d = im.split()
im = Image.merge((c,d,a,b))

If anyone knows how to use something in the Unpack.c file to do this correctly, please ping me. Thank you.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • Thanks. This was actually along the lines of what I initially tried doing. I just couldn't find out how to do it. Oh, and I'm actually putting this into a python package, so I'm going to be loading it in another file anyway. – ego-lay atman-bay Feb 21 '23 at 02:48