summaryrefslogtreecommitdiff
path: root/magick.py
diff options
context:
space:
mode:
authorJohannes 'josch' Schauer <josch@debian.org>2020-04-05 20:28:44 +0200
committerJohannes 'josch' Schauer <josch@debian.org>2020-04-05 20:28:44 +0200
commite7866e5e3c125b734332dd93843ef683332c0a43 (patch)
treefedab8befa17c89fae1ed4cd68131108eed546c4 /magick.py
parent140ed3b81e844b06f82bb5819fe335b514b2aed4 (diff)
New upstream version 0.3.4
Diffstat (limited to 'magick.py')
-rw-r--r--magick.py306
1 files changed, 306 insertions, 0 deletions
diff --git a/magick.py b/magick.py
new file mode 100644
index 0000000..b3dcc18
--- /dev/null
+++ b/magick.py
@@ -0,0 +1,306 @@
+#!/usr/bin/env python3
+
+import sys
+import numpy
+import scipy.signal
+import zlib
+import struct
+
+
+def find_closest_palette_color(color, palette):
+ if color.ndim == 0:
+ idx = (numpy.abs(palette - color)).argmin()
+ else:
+ # naive distance function by computing the euclidean distance in RGB space
+ idx = ((palette - color) ** 2).sum(axis=-1).argmin()
+ return palette[idx]
+
+
+def floyd_steinberg(img, palette):
+ for y in range(img.shape[0]):
+ for x in range(img.shape[1]):
+ oldpixel = img[y, x]
+ newpixel = find_closest_palette_color(oldpixel, palette)
+ quant_error = oldpixel - newpixel
+ img[y, x] = newpixel
+ if x + 1 < img.shape[1]:
+ img[y, x + 1] += quant_error * 7 / 16
+ if y + 1 < img.shape[0]:
+ img[y + 1, x - 1] += quant_error * 3 / 16
+ img[y + 1, x] += quant_error * 5 / 16
+ if x + 1 < img.shape[1] and y + 1 < img.shape[0]:
+ img[y + 1, x + 1] += quant_error * 1 / 16
+ return img
+
+
+def convolve_rgba(img, kernel):
+ return numpy.stack(
+ (
+ scipy.signal.convolve2d(img[:, :, 0], kernel, "same"),
+ scipy.signal.convolve2d(img[:, :, 1], kernel, "same"),
+ scipy.signal.convolve2d(img[:, :, 2], kernel, "same"),
+ scipy.signal.convolve2d(img[:, :, 3], kernel, "same"),
+ ),
+ axis=-1,
+ )
+
+
+def rgb2gray(img):
+ result = numpy.zeros((60, 60), dtype=numpy.dtype("int64"))
+ for y in range(img.shape[0]):
+ for x in range(img.shape[1]):
+ clin = sum(img[y, x] * [0.2126, 0.7152, 0.0722]) / 0xFFFF
+ if clin <= 0.0031308:
+ csrgb = 12.92 * clin
+ else:
+ csrgb = 1.055 * clin ** (1 / 2.4) - 0.055
+ result[y, x] = csrgb * 0xFFFF
+ return result
+
+
+def palettize(img, pal):
+ result = numpy.zeros((img.shape[0], img.shape[1]), dtype=numpy.dtype("int64"))
+ for y in range(img.shape[0]):
+ for x in range(img.shape[1]):
+ for i, col in enumerate(pal):
+ if numpy.array_equal(img[y, x], col):
+ result[y, x] = i
+ break
+ else:
+ raise Exception()
+ return result
+
+
+def write_png(data, path, bitdepth, colortype, palette=None):
+ with open(path, "wb") as f:
+ f.write(b"\x89PNG\r\n\x1A\n")
+ # PNG image type Colour type Allowed bit depths
+ # Greyscale 0 1, 2, 4, 8, 16
+ # Truecolour 2 8, 16
+ # Indexed-colour 3 1, 2, 4, 8
+ # Greyscale with alpha 4 8, 16
+ # Truecolour with alpha 6 8, 16
+ block = b"IHDR" + struct.pack(
+ ">IIBBBBB",
+ data.shape[1], # width
+ data.shape[0], # height
+ bitdepth, # bitdepth
+ colortype, # colortype
+ 0, # compression
+ 0, # filtertype
+ 0, # interlaced
+ )
+ f.write(
+ struct.pack(">I", len(block) - 4)
+ + block
+ + struct.pack(">I", zlib.crc32(block))
+ )
+ if palette is not None:
+ block = b"PLTE"
+ for col in palette:
+ block += struct.pack(">BBB", col[0], col[1], col[2])
+ f.write(
+ struct.pack(">I", len(block) - 4)
+ + block
+ + struct.pack(">I", zlib.crc32(block))
+ )
+ raw = b""
+ for y in range(data.shape[0]):
+ raw += b"\0"
+ if bitdepth == 16:
+ raw += data[y].astype(">u2").tobytes()
+ elif bitdepth == 8:
+ raw += data[y].astype(">u1").tobytes()
+ elif bitdepth in [4, 2, 1]:
+ valsperbyte = 8 // bitdepth
+ for x in range(0, data.shape[1], valsperbyte):
+ val = 0
+ for j in range(valsperbyte):
+ if x + j >= data.shape[1]:
+ break
+ val |= (data[y, x + j].astype(">u2") & (2 ** bitdepth - 1)) << (
+ (valsperbyte - j - 1) * bitdepth
+ )
+ raw += struct.pack(">B", val)
+ else:
+ raise Exception()
+ compressed = zlib.compress(raw)
+ block = b"IDAT" + compressed
+ f.write(
+ struct.pack(">I", len(compressed))
+ + block
+ + struct.pack(">I", zlib.crc32(block))
+ )
+ block = b"IEND"
+ f.write(struct.pack(">I", 0) + block + struct.pack(">I", zlib.crc32(block)))
+
+
+def main():
+ outdir = sys.argv[1]
+
+ # create a 256 color palette by first writing 16 shades of gray
+ # and then writing an array of RGB colors with 6, 8 and 5 levels
+ # for red, green and blue, respectively
+ pal8 = numpy.zeros((256, 3), dtype=numpy.dtype("int64"))
+ i = 0
+ for gray in range(15, 255, 15):
+ pal8[i] = [gray, gray, gray]
+ i += 1
+ for red in 0, 0x33, 0x66, 0x99, 0xCC, 0xFF:
+ for green in 0, 0x24, 0x49, 0x6D, 0x92, 0xB6, 0xDB, 0xFF:
+ for blue in 0, 0x40, 0x80, 0xBF, 0xFF:
+ pal8[i] = [red, green, blue]
+ i += 1
+ assert i == 256
+
+ # windows 16 color palette
+ pal4 = numpy.array(
+ [
+ [0x00, 0x00, 0x00],
+ [0x80, 0x00, 0x00],
+ [0x00, 0x80, 0x00],
+ [0x80, 0x80, 0x00],
+ [0x00, 0x00, 0x80],
+ [0x80, 0x00, 0x80],
+ [0x00, 0x80, 0x80],
+ [0xC0, 0xC0, 0xC0],
+ [0x80, 0x80, 0x80],
+ [0xFF, 0x00, 0x00],
+ [0x00, 0xFF, 0x00],
+ [0xFF, 0x00, 0x00],
+ [0x00, 0xFF, 0x00],
+ [0xFF, 0x00, 0xFF],
+ [0x00, 0xFF, 0x00],
+ [0xFF, 0xFF, 0xFF],
+ ],
+ dtype=numpy.dtype("int64"),
+ )
+
+ # choose values slightly off red, lime and blue because otherwise
+ # imagemagick will classify the image as Depth: 8/1-bit
+ pal2 = numpy.array(
+ [[0, 0, 0], [0xFE, 0, 0], [0, 0xFE, 0], [0, 0, 0xFE]],
+ dtype=numpy.dtype("int64"),
+ )
+
+ # don't choose black and white or otherwise imagemagick will classify the
+ # image as bilevel with 8/1-bit depth instead of palette with 8-bit color
+ # don't choose gray colors or otherwise imagemagick will classify the
+ # image as grayscale
+ pal1 = numpy.array(
+ [[0x01, 0x02, 0x03], [0xFE, 0xFD, 0xFC]], dtype=numpy.dtype("int64")
+ )
+
+ # gaussian kernel with sigma=3
+ kernel = numpy.array(
+ [
+ [0.011362, 0.014962, 0.017649, 0.018648, 0.017649, 0.014962, 0.011362],
+ [0.014962, 0.019703, 0.02324, 0.024556, 0.02324, 0.019703, 0.014962],
+ [0.017649, 0.02324, 0.027413, 0.028964, 0.027413, 0.02324, 0.017649],
+ [0.018648, 0.024556, 0.028964, 0.030603, 0.028964, 0.024556, 0.018648],
+ [0.017649, 0.02324, 0.027413, 0.028964, 0.027413, 0.02324, 0.017649],
+ [0.014962, 0.019703, 0.02324, 0.024556, 0.02324, 0.019703, 0.014962],
+ [0.011362, 0.014962, 0.017649, 0.018648, 0.017649, 0.014962, 0.011362],
+ ],
+ numpy.float,
+ )
+
+ # constructs a 2D array of a circle with a width of 36
+ circle = list()
+ offsets_36 = [14, 11, 9, 7, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0]
+ for offs in offsets_36 + offsets_36[::-1]:
+ circle.append([0] * offs + [1] * (len(offsets_36) - offs) * 2 + [0] * offs)
+
+ alpha = numpy.zeros((60, 60, 4), dtype=numpy.dtype("int64"))
+
+ # draw three circles
+ for (xpos, ypos, color) in [
+ (12, 3, [0xFFFF, 0, 0, 0xFFFF]),
+ (21, 21, [0, 0xFFFF, 0, 0xFFFF]),
+ (3, 21, [0, 0, 0xFFFF, 0xFFFF]),
+ ]:
+ for x, row in enumerate(circle):
+ for y, pos in enumerate(row):
+ if pos:
+ alpha[y + ypos, x + xpos] += color
+ alpha = numpy.clip(alpha, 0, 0xFFFF)
+ alpha = convolve_rgba(alpha, kernel)
+
+ write_png(alpha, outdir + "/alpha.png", 16, 6)
+
+ normal16 = alpha[:, :, 0:3]
+ write_png(normal16, outdir + "/normal16.png", 16, 2)
+
+ write_png(normal16 / 0xFFFF * 0xFF, outdir + "/normal.png", 8, 2)
+
+ write_png(0xFF - normal16 / 0xFFFF * 0xFF, outdir + "/inverse.png", 8, 2)
+
+ gray16 = rgb2gray(normal16)
+
+ write_png(gray16, outdir + "/gray16.png", 16, 0)
+
+ write_png(gray16 / 0xFFFF * 0xFF, outdir + "/gray8.png", 8, 0)
+
+ write_png(
+ floyd_steinberg(gray16, numpy.arange(16) / 0xF * 0xFFFF) / 0xFFFF * 0xF,
+ outdir + "/gray4.png",
+ 4,
+ 0,
+ )
+
+ write_png(
+ floyd_steinberg(gray16, numpy.arange(4) / 0x3 * 0xFFFF) / 0xFFFF * 0x3,
+ outdir + "/gray2.png",
+ 2,
+ 0,
+ )
+
+ write_png(
+ floyd_steinberg(gray16, numpy.arange(2) / 0x1 * 0xFFFF) / 0xFFFF * 0x1,
+ outdir + "/gray1.png",
+ 1,
+ 0,
+ )
+
+ write_png(
+ palettize(
+ floyd_steinberg(normal16, pal8 * 0xFFFF / 0xFF) / 0xFFFF * 0xFF, pal8
+ ),
+ outdir + "/palette8.png",
+ 8,
+ 3,
+ pal8,
+ )
+
+ write_png(
+ palettize(
+ floyd_steinberg(normal16, pal4 * 0xFFFF / 0xFF) / 0xFFFF * 0xFF, pal4
+ ),
+ outdir + "/palette4.png",
+ 4,
+ 3,
+ pal4,
+ )
+
+ write_png(
+ palettize(
+ floyd_steinberg(normal16, pal2 * 0xFFFF / 0xFF) / 0xFFFF * 0xFF, pal2
+ ),
+ outdir + "/palette2.png",
+ 2,
+ 3,
+ pal2,
+ )
+
+ write_png(
+ palettize(
+ floyd_steinberg(normal16, pal1 * 0xFFFF / 0xFF) / 0xFFFF * 0xFF, pal1
+ ),
+ outdir + "/palette1.png",
+ 1,
+ 3,
+ pal1,
+ )
+
+
+main()