如何使用Pillow读取一个原始的RGBA4444图像?

huangapple go评论59阅读模式
英文:

How would I read a raw RGBA4444 image using Pillow?

问题

我尝试读取一个'.waltex'图像,这是一种'walaber图像'。它基本上是以原始图像格式存在的。问题是它使用'RGBA8888'、'RGBA4444'、'RGB565'和'RGB5551'(这些都可以从文件头中确定),但我找不到在PIL中使用这些颜色规格的方法。

我尝试过以下方法:

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()

我尝试了所有16位原始模式,但都没有找到适用的。要明确一下,这个文件具体是'RGBA4444',采用小端序,每像素8个字节。

如果你需要这个文件,我可以提供链接。

英文:

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

得分: 3

我已经对原始代码进行了一些更改,以便:

  • 您可以传递要读取的文件名作为参数
  • 它解析标头并检查魔术字符串,自动推导格式(RGBA8888或RGBA4444)、高度和宽度
  • 它现在处理RGBA8888,就像您最新分享的示例图像一样

所以,代码如下所示:

#!/usr/bin/env python3

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

def loadWaltex(filename):

    # 以二进制模式打开输入文件
    with open(filename, 'rb') as fd:
        # 读取16字节的标头并提取元数据
        # 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')

        # 检查格式是否为0(RGBA8888)或3(RGBA4444)
        if fmt == 0:
            fmtdesc = "RGBA8888"
            # 读取文件余下的部分(标头后面的部分)
            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))
            # 堆叠通道以生成RGBA图像
            RGBA = np.dstack((R, G, B, A))
        else:
            fmtdesc = "RGBA4444"
            # 读取文件余下的部分(标头后面的部分)
            data = np.fromfile(fd, dtype=np.uint16).reshape((h, w))
            # 从uint16中提取RGBA4444
            R = (data >> 12) & 0xf
            G = (data >> 8) & 0xf
            B = (data >> 4) & 0xf
            A = data & 0xf
            # 堆叠通道以生成RGBA图像
            RGBA = np.dstack((R, G, B, A)).astype(np.uint8) << 4

        # 用户的调试信息
        print(f'Filename: {filename}, version: {vers}, format: {fmtdesc} ({fmt}), w: {w}, h: {h}')
    
        # 转化为PIL图像
        im = Image.fromarray(RGBA)
        return im

if __name__ == "__main__":
    # 加载由第一个参数指定的图像
    im = loadWaltex(sys.argv[1])
    im.save('result.png')

当您运行它时:

./decodeRGBA.py objects.waltex

您将得到:

如何使用Pillow读取一个原始的RGBA4444图像?

对于您的两个示例图像,调试输出如下:

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

原始答案

我发现使用Numpy是处理这种情况的最简单方法,而且性能非常好:

#!/usr/bin/env python3

import numpy as np
from PIL import Image

# 定义图像的已知参数并读入Numpy数组
h, w, offset = 1024, 1024, 16
data = np.fromfile('Carl.waltex', dtype=np.uint16, offset=offset).reshape((h,w))

# 从uint16中提取RGBA4444
R = (data >> 12) & 0xf
G = (data >> 8) & 0xf
B = (data >> 4) & 0xf
A = data & 0xf

# 堆叠4个单独的通道以生成RGBA图像
RGBA = np.dstack((R, G, B, A)).astype(np.uint8) << 4

# 转化为PIL图像
im = Image.fromarray(RGBA)
im.save('result.png')

如何使用Pillow读取一个原始的RGBA4444图像?

注意: 您的图像在开头有16字节的填充。有时候这个数量是可变的。在这种情况下的一个有用技巧是读取整个文件,计算出有多少个有效的像素数据样本(在您的情况下是1024 * 1024),然后切片数据以取最后N个样本,从而忽略开头的可变填充。这将看起来像这样:

# 定义图像的已知参数并读入Numpy数组
h, w = 1024, 1024
data = np.fromfile('Carl.waltex', dtype=np.uint16)[-h*w:].reshape((h,w))

如果您不喜欢Numpy,更喜欢使用列表和结构来处理,可以使用以下代码获得完全相同的结果:

#!/usr/bin/env python3

import struct
from PIL import Image

# 定义图像的已知参数
h, w, offset = 1024, 1024, 16
data = open('Carl.waltex', 'rb').read()[offset:]

# 解包成一堆h*w个无符号短整数
uint16s = struct.unpack("H" * h * w, data)

# 创建一个RGBA元组的列表
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))

# 将RGBA元组的列表推入空图像中
RGBA = Image.new('RGBA', (w,h))
RGBA.putdata(pixels)
RGBA.save('result.png')

请注意,与基于Numpy的方法相比,基于列表的方法速度要

英文:

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, &#39;rb&#39;) 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(&#39;4sBBHH6s&#39;, header)
if magic != b&#39;WALT&#39;:
sys.exit(f&#39;ERROR: {filename} does not start with &quot;WALT&quot; magic string&#39;)
#&#160;Check if fmt=0 (RGBA8888) or fmt=3 (RGBA4444)
if fmt == 0:
fmtdesc = &quot;RGBA8888&quot;
#&#160;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))
#&#160;Stack the channels to make RGBA image
RGBA = np.dstack((R,G,B,A))
else:
fmtdesc = &quot;RGBA4444&quot;
#&#160;Read remainder of file (part following header)
data = np.fromfile(fd, dtype=np.uint16).reshape((h,w))
#&#160;Split the RGBA444 out from the uint16
R = (data&gt;&gt;12) &amp; 0xf
G = (data&gt;&gt;8) &amp; 0xf
B = (data&gt;&gt;4) &amp; 0xf
A = data &amp; 0xf
#&#160;Stack the channels to make RGBA image
RGBA = np.dstack((R,G,B,A)).astype(np.uint8) &lt;&lt; 4
#&#160;Debug info for user
print(f&#39;Filename: {filename}, version: {vers}, format: {fmtdesc} ({fmt}), w: {w}, h: {h}&#39;)
# Make into PIL Image
im = Image.fromarray(RGBA)
return im
if __name__ == &quot;__main__&quot;:
# Load image specified by first parameter
im = loadWaltex(sys.argv[1])
im.save(&#39;result.png&#39;)

And when you run it with:

./decodeRGBA.py objects.waltex

You get:

如何使用Pillow读取一个原始的RGBA4444图像?

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(&#39;Carl.waltex&#39;, dtype=np.uint16, offset=offset).reshape((h,w))
#&#160;Split the RGBA4444 out from the uint16
R = (data &gt;&gt; 12) &amp; 0xf
G = (data &gt;&gt;  8) &amp; 0xf
B = (data &gt;&gt;  4) &amp; 0xf
A =  data        &amp; 0xf
#&#160;Stack the 4 individual channels to make an RGBA image
RGBA = np.dstack((R,G,B,A)).astype(np.uint8) &lt;&lt; 4
# Make into PIL Image
im = Image.fromarray(RGBA)
im.save(&#39;result.png&#39;)

如何使用Pillow读取一个原始的RGBA4444图像?


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(&#39;Carl.waltex&#39;, 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(&#39;Carl.waltex&#39;, &#39;rb&#39;).read()[offset:]
# Unpack into bunch of h*w unsigned shorts
uint16s = struct.unpack(&quot;H&quot; * h *w, data)
#&#160;Build a list of RGBA tuples
pixels = []
for RGBA4444 in uint16s:
R = (RGBA4444 &gt;&gt; 8) &amp; 0xf0
G = (RGBA4444 &gt;&gt; 4) &amp; 0xf0
B =  RGBA4444       &amp; 0xf0
A = ( RGBA4444      &amp; 0xf) &lt;&lt; 4
pixels.append((R,G,B,A))
#&#160;Push the list of RGBA tuples into an empty image
RGBA = Image.new(&#39;RGBA&#39;, (w,h))
RGBA.putdata(pixels)
RGBA.save(&#39;result.png&#39;)

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

Numpy: 3.6 ms &#177; 73.2 &#181;s per loop (mean &#177; std. dev. of 7 runs, 100 loops each)
listy: 213 ms &#177; 712 &#181;s per loop (mean &#177; 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.

答案2

得分: 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))

请注意,上述内容包括一些代码和注释,我已经按您的要求提供了代码部分的翻译。如果您有任何其他翻译需求或问题,请随时提出。

英文:

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(&#39;objects.waltex&#39;)
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&quot;WALT&quot;
class WaltexImageFile(ImageFile.ImageFile):
format = &quot;Waltex&quot;
format_description = &quot;Waltex texture image&quot;
def _open(self):
header = self.fp.read(HEADER_LENGTH)
magic, vers, fmt, w, h, _ = struct.unpack(&#39;4sBBHH6s&#39;, header)
# size in pixels (width, height)
self._size = w, h
# mode setting
self.mode = &#39;RGBA&#39;
# Decoder
if fmt == 0:
# RGBA8888
# Just use built-in raw decoder
self.tile = [(&quot;raw&quot;, (0, 0) + self.size, HEADER_LENGTH, (self.mode, 
0, 1))]
elif fmt == 3:
# RGBA4444
# Use raw decoder with custom RGBA;4B unpacker
self.tile = [(&quot;raw&quot;, (0, 0) + self.size, HEADER_LENGTH, (&#39;RGBA;4B&#39;, 
0, 1))]
Image.register_open(WaltexImageFile.format, WaltexImageFile, _accept)
Image.register_extensions(
WaltexImageFile.format,
[
&quot;.waltex&quot;
],
)
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.

huangapple
  • 本文由 发表于 2023年2月18日 07:47:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/75490202.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定