Skip to content

Instantly share code, notes, and snippets.

@EricBurnett
Created February 10, 2010 20:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save EricBurnett/300780 to your computer and use it in GitHub Desktop.
Save EricBurnett/300780 to your computer and use it in GitHub Desktop.
Convert an image to all-RGB by colourizing from 3d hilbert line
import Image # Python Imaging Library (PIL)
import random
import math
import os
import sys
import getopt
# Main work function. Takes the path to a (square) image and the number of
# bits per colour channel desired. That value dictates the size of the output
# image, so it must be divisible by 2. For < 8, lowest order bits dropped.
def allrgbify(path, bitsPerChannel):
if bitsPerChannel & 1 != 0:
print "bitsPerChannel must be divisible by 2"
return
if (bitsPerChannel > 8):
print "Do you really want a picture " + \
str(int(math.sqrt(2**(3*bitsPerChannel)))) + " pixels on a side?"
return
size = 2**(3*bitsPerChannel/2)
shift = 8-bitsPerChannel
im = Image.open(path)
im = im.resize((size, size))
pix = im.load()
# Flat is an array of tuples, (index, x, y)
flat = [(hindex(bitsPerChannel, map(lambda a: a>>shift, pix[x,y])), x, y)
for y in xrange(size) for x in xrange(size)]
# Shuffle so pixels with the same calculated index treated "fairly".
random.shuffle(flat)
# Sort by calculated index function.
def cmp(x,y):
return x[0]-y[0]
flat.sort(cmp)
# Map new colour scheme back to the respective pixels in the image.
for i in xrange(2**(3*(8-shift))):
pt = flat[i]
pix[pt[1], pt[2]] = tuple(map(lambda a: a<<shift,
hindexInverse(bitsPerChannel, i)))
name, ext = os.path.splitext(path)
im.save(name + "_allrgbify.png", "PNG")
# Generate allrgb versions of selected images with specified bit depth.
def genAll():
files = ["C:/Users/Eric Burnett/Desktop/Python/zoom5.png",
"C:/Users/Eric Burnett/Desktop/Python/zoom6.png"]
for f in files:
allrgbify(f, 6)
# Semi-related function - output the hilbert colours in a line.
def hilbertLine():
im = Image.new("RGB", (512, 1))
im.putdata([tuple(map(lambda a: a<<5, hindexInverse(3, i)))
for i in range(512)])
im.save("C:/Users/Eric Burnett/Desktop/Python/hilbertLine.png")
# Verify a file to ensure it actually uses every colour only once. Only works
# for images generated with bit depth 8.
def verify(path):
im = Image.open(path)
pix = im.load()
size = im.size
flat = [(pix[x,y][0]<<16)|(pix[x,y][1]<<8)|pix[x,y][2]
for y in range(size[0]) for x in range(size[1])]
flat.sort()
for i in xrange(2**24-1):
if flat[i] != i:
print "incorrect value! i="+str(i)
return
# Hilbert logic from https://www.cs.dal.ca/sites/default/files/CS-2006-07.pdf
# Algorithm a direct implementation of the logic presented there.
# returns the index into the rgb cube indexed by the 3d hilbert curve
def hindex(m, p):
dim = 3
h = 0
e = 0
d = 0
for i in range(m)[::-1]:
l = bit(p[2], i) << 2 | bit(p[1], i) << 1 | bit(p[0], i)
l = cycleLeft(l, 1, dim)
l = T(e, d, dim, l)
w = gcInverse(dim, l)
e = e ^ (cycleLeft(E(w), d+1, dim))
d = (d + D(w, dim) + 1) % dim
h = (h << dim) | w
return h
def hindexInverse(m, h):
dim = 3
e = 0
d = 0
p = [0, 0, 0]
for i in range(m)[::-1]:
w = bit(h, 3*i+2) << 2 | bit(h, 3*i+1) << 1 | bit(h, 3*i)
l = gc(w)
l = TInverse(e, d, dim, l)
for j in range(dim):
p[j] |= bit(l, j) << i
e = e ^ (cycleLeft(E(w), d+1, dim))
d = (d + D(w, dim) + 1) % dim
return p
# trailing set bits
def tsb(i):
# Split on last 4 bits
if ((i & 7) == 7):
# All 4 set
return 4 + tsb(i >> 4)
tsb4 = (0, 1, 0, 2, 0, 1, 0, 3)
return tsb4[i & 7]
# the greycode of the integer i
def gc(i):
return i ^ (i >> 1)
def gcInverse(m, g):
i = g
j = 1
while j < m:
i = i ^ (g >> j)
j = j + 1
return i
def D(i, n):
if i == 0:
return 0
if i & 1:
return tsb(i) % n
return tsb(i-1) % n
def E(i):
if i == 0:
return 0
return gc((i-1) & (~1))
def cycle(a, b, n):
return (a >> b) | ((a << (n-b)) & (2**n - 1))
def cycleLeft(a, b, n):
return cycle(a, n-b, n)
def T(e, d, n, b):
return cycle(b^e,d+1,n)
def TInverse(e, d, n, b):
return T(cycle(e, d+1, n), n-d-1, n, b)
def bit(n, i):
return (n >> i) & 1
def bitSet(n, i, val):
return n | val << i
# Ensure hilbert mapping is a bijection as expected.
def testHilbertInvertible():
for i in xrange(2**24-1):
if hindex(8, hindexInverse(8, i)) != i:
print "failure on ", i
return 0
def main():
possibleArgs = ["image=", "bpc="]
usage = "Usage: python allrgbify.py --image=\"image path\" --bpc=desired_bits_per_channel"
try:
opts, args = getopt.getopt(sys.argv[1:], "", possibleArgs)
except getopt.error, msg:
print msg
print usage
sys.exit(2)
path = ""
bpc = 0
for o, a in opts:
if o == "--image":
path = a
else:
bpc = int(a)
if path == "":
print usage
sys.exit(2)
allrgbify(path, bpc)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment