diff options
| author | John "Lameguy" Wilbert Villamor <lameguy64@gmail.com> | 2022-03-25 09:22:20 +0800 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-03-25 09:22:20 +0800 |
| commit | 975e614b3c840e2f717adac1d1cb9cee4e5e561b (patch) | |
| tree | 6584ce5b0dbe27a466c95c81fac61b0d90f627bd /examples/mdec/mdecimage/encode_image.py | |
| parent | 05d44488bd5587786f4bd0286fc0f555c79aa46a (diff) | |
| parent | 45168ae43e29aa5930ee5a206475ae836078915f (diff) | |
| download | psn00bsdk-975e614b3c840e2f717adac1d1cb9cee4e5e561b.tar.gz | |
Merge pull request #46 from spicyjpeg/psxmdec
Critical ldscript fixes, initial MDEC support and CI updates
Diffstat (limited to 'examples/mdec/mdecimage/encode_image.py')
| -rw-r--r-- | examples/mdec/mdecimage/encode_image.py | 218 |
1 files changed, 218 insertions, 0 deletions
diff --git a/examples/mdec/mdecimage/encode_image.py b/examples/mdec/mdecimage/encode_image.py new file mode 100644 index 0000000..3a5bcea --- /dev/null +++ b/examples/mdec/mdecimage/encode_image.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# Simple MDEC image encoder (requires PIL/Pillow and NumPy to be installed) +# (C) 2022 spicyjpeg - MPL licensed + +import math +from warnings import warn +from argparse import ArgumentParser, FileType + +import numpy +from PIL import Image + +LUMA_SCALE = 8 +CHROMA_SCALE = 16 + +## Tables + +ZIGZAG_TABLE = numpy.array(( + 0, 1, 5, 6, 14, 15, 27, 28, + 2, 4, 7, 13, 16, 26, 29, 42, + 3, 8, 12, 17, 25, 30, 41, 43, + 9, 11, 18, 24, 31, 40, 44, 53, + 10, 19, 23, 32, 39, 45, 52, 54, + 20, 22, 33, 38, 46, 51, 55, 60, + 21, 34, 37, 47, 50, 56, 59, 61, + 35, 36, 48, 49, 57, 58, 62, 63 +), numpy.uint8).argsort() + +# The default luma and chroma quantization table is based on the MPEG-1 +# quantization table, with the only difference being the first value (2 instead +# of 8). +QUANT_TABLE = numpy.array(( + 2, 16, 19, 22, 26, 27, 29, 34, + 16, 16, 22, 24, 27, 29, 34, 37, + 19, 22, 26, 27, 29, 34, 34, 38, + 22, 22, 26, 27, 29, 34, 37, 40, + 22, 26, 27, 29, 32, 35, 40, 48, + 26, 27, 29, 32, 35, 40, 48, 58, + 26, 27, 29, 34, 38, 46, 56, 69, + 27, 29, 35, 38, 46, 56, 69, 83 +), numpy.uint8).reshape(( 8, 8 )) + +S = [ math.cos((i or 4) / 16 * math.pi) / 2 for i in range(8) ] + +DCT_MATRIX = numpy.array(( + S[0], S[0], S[0], S[0], S[0], S[0], S[0], S[0], + S[1], S[3], S[5], S[7], -S[7], -S[5], -S[3], -S[1], + S[2], S[6], -S[6], -S[2], -S[2], -S[6], S[6], S[2], + S[3], -S[7], -S[1], -S[5], S[5], S[1], S[7], -S[3], + S[4], -S[4], -S[4], S[4], S[4], -S[4], -S[4], S[4], + S[5], -S[1], S[7], S[3], -S[3], -S[7], S[1], -S[5], + S[6], -S[2], S[2], -S[6], -S[6], S[2], -S[2], S[6], + S[7], -S[5], S[3], -S[1], S[1], -S[3], S[5], -S[7] +), numpy.float32).reshape(( 8, 8 )) + +## Helpers + +def to_int10(value): + clamped = min(max(int(value), -0x200), 0x1ff) + + return clamped + (0 if clamped >= 0 else 0x400) + +def rgb_to_ycbcr_planar(image): + scaled = image.astype(numpy.float32) / 255.0 + r, g, b = scaled.transpose(( 2, 0, 1 )) + + # https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion + y = 16 + r * 65.481 + g * 128.553 + b * 24.966 + cb = 128 - r * 37.797 - g * 74.203 + b * 112.000 + cr = 128 + r * 112.000 - g * 93.786 - b * 18.214 + + return y, cb, cr + +## Block encoder + +def encode_block(buffer, block, scale): + # Perform discrete cosine transform on the block, divide the coefficients by + # the quantization table and reorder them in zigzag order. + _block = block.astype(numpy.float32) - 128.0 + coeffs = (DCT_MATRIX @ _block @ DCT_MATRIX.T) / QUANT_TABLE + coeffs = coeffs.reshape(( 64, ))[ZIGZAG_TABLE] + + buffer[0] = (scale << 10) | to_int10(round(coeffs[0])) + offset = 1 + + # Divide the AC coefficients by the given quantization scale and encode them + # as run-length pairs by counting how many zeroes there are between each + # non-zero value. + ac_values = coeffs[1:] * 8.0 / scale + encoded = [] + run_length = 0 + + for ac in ac_values.round().astype(numpy.int32): + if ac: + buffer[offset] = (run_length << 10) | to_int10(ac) + offset += 1 + + run_length = 0 + else: + run_length += 1 + + # Flush any remaining zeroes. + if run_length: + buffer[offset] = (run_length - 1) << 10 + offset += 1 + + # Add 1 or 2 end-of-block codes depending on whether the number of 16-bit + # values output so far is odd or even. Some emulators will break if blocks + # are not 32-bit aligned. + buffer[offset] = 0xfe00 + offset += 1 + if offset % 2: + buffer[offset] = 0xfe00 + offset += 1 + + return offset + +def encode_macroblock(buffer, block, y_scale, c_scale): + #y, cb, cr = rgb_to_ycbcr_planar(block) + y, cb, cr = block.transpose(( 2, 0, 1 )) + offset = 0 + + # Split the macroblock into 6 monochrome 8x8 blocks (Cr, Cb at half + # resolution + Y1-4). The MDEC uses 4:2:0 chroma subsampling. + # TODO: use bilinear sampling instead of nearest-neighbor for chroma + offset += encode_block(buffer[offset:], cr[0:16:2, 0:16:2], c_scale) + offset += encode_block(buffer[offset:], cb[0:16:2, 0:16:2], c_scale) + offset += encode_block(buffer[offset:], y[0: 8, 0: 8], y_scale) + offset += encode_block(buffer[offset:], y[0: 8, 8:16], y_scale) + offset += encode_block(buffer[offset:], y[8:16, 0: 8], y_scale) + offset += encode_block(buffer[offset:], y[8:16, 8:16], y_scale) + + return offset + +## Main + +def get_args(): + parser = ArgumentParser( + description = "Generates uncompressed MDEC bitstream data from an image." + ) + parser.add_argument( + "input_file", + type = FileType("rb"), + help = "input image file" + ) + parser.add_argument( + "-o", "--output", + type = FileType("wb"), + default = "image.bin", + help = "where to output converted image data (image.bin by default)", + metavar = "file" + ) + parser.add_argument( + "-m", "--monochrome", + action = "store_true", + help = "encode image as monochrome (8x8 blocks) instead of color (16x16 macroblocks)" + ) + parser.add_argument( + "-y", "--luma", + type = int, + default = LUMA_SCALE, + help = f"quantization scale for luma/monochrome blocks (0-63, default {LUMA_SCALE})", + metavar = "scale" + ) + parser.add_argument( + "-c", "--chroma", + type = int, + default = CHROMA_SCALE, + help = f"quantization scale for chroma blocks (0-63, default {CHROMA_SCALE})", + metavar = "scale" + ) + + return parser.parse_args() + +def main(): + args = get_args() + if args.luma < 0 or args.luma > 63: + raise ValueError("luma quantization scale must be in 0-63 range") + if args.chroma < 0 or args.chroma > 63: + raise ValueError("chroma quantization scale must be in 0-63 range") + + image = Image.open(args.input_file, "r") + data = numpy.array(image.convert("YCbCr"), numpy.uint8) + size = 8 if args.monochrome else 16 + + if image.width % size: + warn(RuntimeWarning(f"image width is not a multiple of {size}, trimming")) + if image.height % size: + warn(RuntimeWarning(f"image height is not a multiple of {size}, trimming")) + + # Preallocate 1 MB for the converted image data (faster than expanding an + # array dynamically -- this script is too slow already). + buffer = numpy.empty(0x80000, numpy.uint16) + offset = 0 + + # Split the image into 8x8 or 16x16 blocks and encode them in column-major + # order. + for x in range(0, image.width, size): + for y in range(0, image.height, size): + block = data[y:(y + size), x:(x + size)] + + if args.monochrome: + offset += encode_block(buffer[offset:], block[:, :, 0], args.luma) + else: + offset += encode_macroblock(buffer[offset:], block, args.luma, args.chroma) + + # Pad the generated data to the size of a DMA chunk (32x 32-bit words or + # 128 bytes). + length = (offset + 63) & 0xffffffc0 + buffer[offset:length] = 0xfe00 + + if length > (0xffff * 2): + warn(RuntimeWarning("image is too large to be decoded with a single DecDCTin() call")) + + with args.output as _file: + buffer[0:length].tofile(_file) + +if __name__ == "__main__": + main() |
