Skip to content

Instantly share code, notes, and snippets.

@hogjonny
Last active December 17, 2020 19:55
Show Gist options
  • Save hogjonny/6879571 to your computer and use it in GitHub Desktop.
Save hogjonny/6879571 to your computer and use it in GitHub Desktop.
Working on code to convert an image to an 8-bit heightmap along with a colorRamp.\n 1. Use a !!! duplicate !!! of your image image to create index palette, save the palette (.act file)\n 2. Run through this script\n 3. Now use the sorted palette to index palette-ize your original image\n 4. Now switch the palette to 'grayscale' ... this you save…
#!/usr/bin/env python
'''
hogGenGradient.py
~~~
A script built on PIL to convert an image to 8-bit heightmap and a
luminance sorted gradient map.
Requirements: PIL, numpy
Tested mainly in Python 2.7 (also limited tests in 2.6.4)
I love the idea of gradient map
shaders ... but I have always wanted a better way of automatting
the authoring of the heightmap and associated ramp.
First, I was using PS to index an image and I wanted to figure out
how to convert the palette (.act) to a ramp. That is where I dug in
and figured out how to unpack the .act file format.
Basically, the ramp is a modern gpu equivelant of the palette, as well
as image index you can access the value via UV, in this case any value
along the 'length' of the U axis (or x, or left to right, however you
want to think about it.)
So I figured, that to get a heightmap that matched the ramp all you really
need to do, is in PS swap the palette for 'grayscale', then save the image
out in whatever format you need (I actually suggest after the palette swap
converting it back to a 8-bit grayscale or 24-bit rgb map and saving it.)
The main problem with this approach, is that a PS palette is generated
based on quantization (so I am guessing it's stored something like most
used to least used colors... it's a mess). If what you want to do (like
me) is to use the base heightmap and swap gradiant ramps for things like
color grading, or multiple materials, it makes authoring ramps or altering
them a real fucking pain - and/or doing so munges up the resulting image.
So I figured, what you really need in many cases, is a sorted pallete/ramp
from Dark --> Light that best emulates a Black --> White gradient, for one
this should make authoring and swapping new ramps much easier. I ended up
trying several sorting algorithms, ending up at a sort based on 'perceptual
luminance. It is important to point out, that after you have a sorted
ramp/palette you now need a matching heightmap ... so you need to re-quantize,
or index palettize the original source image to the sorted palette (then you
can swap it's palette to grayscale).
So in short, I went about figuring out how to do all of this in this script
without needing to do any steps in PS. The palette/ramp that I end up with
is not identical to PS but it's perceptually comparable such that I am sure
that does not matter much.
Note: I looked through PyQT and it looks like via QImage you could probably
so something similar.
'''
__author__ = 'hogjonny'
import os
import struct
import Image, ImagePalette
import numpy as np
from math import sqrt
# --function---------------------------------------------------------------
def ActToColorRamp(actFilePath, outImgFilePath):
width = 256
height = 4
outTgaImg = Image.new( 'RGB', (width, height), "black")
#outTgaImg.show() # will open the temp image
imgData = np.array(outTgaImg)
with open(actFilePath, "rb") as fb:
fileContents = []
fileSize = os.stat(actFilePath).st_size
colorTableRGB = []
byteCount = 0
colorCount = 0
Red = None
Green = None
Blue = None
# try to read a byte
byte = fb.read(1)
# if the read was True, move along
while byte:
# Do stuff with byte.
value = struct.unpack('B', byte)[0]
fileContents.append(value)
print 'byte_{0}: {1}'.format(byteCount, value)
#set channels
if Red is None:
Red = value
elif Green is None:
Green = value
elif Blue is None:
Blue = value
#pack channels into rgb
if Red != None and Green != None and Blue != None:
colorTuple = (Red, Green, Blue)
print 'Color_{0} is : {1}'.format(colorCount, colorTuple)
colorTableRGB.append(colorTuple)
Red = None
Green = None
Blue = None
y = colorCount
for x in xrange(4):
imgData[x,y] = colorTuple
colorCount+=1
#read a new byte, continue loop
byte = fb.read(1)
byteCount+=1
print 'file :{0}'.format(fileContents)
print 'length: {0}'.format(len(fileContents))
outTgaImg = Image.fromarray(imgData)
outTgaImg.save(outImgFilePath)
#outTgaImg.show() # will load/open the image data ... in photoshop
return colorTableRGB
# --class------------------------------------------------------------------
class RGBL:
def __init__(self, color, lum):
self.color = color
self.lum = lum
def __repr__(self):
return repr((self.color, self.lum))
# --function---------------------------------------------------------------
def lumSortColorTable(colorTable):
print 'Color Table input before sorting is: {0}'.format(colorTable)
colorLumList = []
sortedLumList = []
sortedColorTable = []
count = 0
for color in colorTable:
#unpack the color tuple
Red = color[0]
Green = color[1]
Blue = color[2]
Lum = sqrt( 0.241*Red**2 + 0.691*Green**2 + 0.068*Blue**2 )
print 'Color_{0}, Lum is: {1}'.format(count, Lum)
count+=1
#make a colored child object
rgbl = RGBL(color, Lum)
#pack it into the colorLumList for sorting
colorLumList.append(rgbl)
sortedLumList = sorted(colorLumList, key=lambda rgbl: rgbl.lum)
for colorItem in sortedLumList:
sortedColorTable.append(colorItem.color)
print 'Sorted Color Table is: {0}'.format(sortedColorTable)
return sortedColorTable
# --function---------------------------------------------------------------
def colorTableToImage(colorTableRGB, imgFilePath, height=4):
width=len(colorTableRGB)
print 'Converting this table to image : {0}'.format(colorTableRGB)
outTgaImg = Image.new( 'RGB', (width, height), "black")
imgData = np.array(outTgaImg)
for index in xrange(len(colorTableRGB)):
y = index
color = colorTableRGB[index]
for x in xrange(height):
imgData[x,y] = color
outTgaImg = Image.fromarray(imgData)
outTgaImg.save(imgFilePath)
#outTgaImg.show() # will load/open the image data ... in photoshop
# --function---------------------------------------------------------------
def colorTableToAct(colorTable, actFilePath):
f = open(actFilePath,"wb")
output = bytearray()
for color in colorTable:
for channel in color:
output.extend(struct.pack("B", channel))
f.write(output)
f.close()
# --function---------------------------------------------------------------
def pilImgPalette(imgFilePath, outImgFilePath=None, pType=Image.ADAPTIVE,
colors=256, clrRGB=3,
rampWidth=256, rampHeight=4):
'''
Builds a color palette from an image.
input: image.ext
output: colorTableRGB
colorTableRGB palette, always returns len 256, in color tuples (R,G,B)
colors = number of indicies that hold color.
'''
# -- sub function -----------------------------------------------------
def chunk(seq, size):
return [seq[i:i+size] for i in range(0, len(seq), size)]
#temp image for ramp generation
outTgaImg = Image.new( 'RGB', (rampWidth, rampHeight), "black")
imgData = np.array(outTgaImg)
colorTableRGB = []
img = Image.open(imgFilePath)
pImg = img.convert('P', palette=pType, colors=colors)
#print dir(pImg.__class__)
palette = pImg.getpalette()
colorTableRGB = chunk(palette, clrRGB)
print 'colorTableRGB contents: {0}'.format(colorTableRGB)
print 'colorTableRGB length: {0}'.format(len(colorTableRGB))
if outImgFilePath != None:
colorTableToImage(colorTableRGB, outImgFilePath)
return colorTableRGB
# --function---------------------------------------------------------------
def imageWithRampToHeight(imgFilePath,
colorTableRGB,
outImgFilePath,
tempOutFilePath=None,
colors=256, levels = 256):
#we have to pack the colorTableRGB back into a flat list i.e. the palette format
palette = []
for color in colorTableRGB:
for value in color:
palette.append(value)
#temp image for index palettization
imgSrc = Image.open(imgFilePath)
imgP = imgSrc.copy().convert('P', palette=Image.ADAPTIVE, colors=colors)
imgP.putpalette(palette)
#tempOutImg = Image.new('P', img.size)
imgNew = imgSrc.copy()
imgQ = imgNew.quantize(palette=imgP)
if tempOutFilePath != None:
imgQ.save(tempOutFilePath)
#imgQ.show()
#generate grayscale palette
grayscale = []
stepsize = 256 // levels
for i in range(256):
v = i // stepsize * stepsize
grayscale.extend((v, v, v))
#final 8-bit heightmap (grayscale)
imgOutHeight = imgQ.copy()
imgOutHeight.putpalette(grayscale)
imgOutHeight.save(outImgFilePath)
#imgOutHeight.show()
###########################################################################
# --main code block--------------------------------------------------------
if __name__ == "__main__":
#the original image
origImgFilePath = 'cement.tga'
#photosop index palette, input is a .act file
#image was indexed with 'perceptual' setting then saved
actFilePath = 'cement_test_01.act'
#build the PS.act color table
inColorTableRGB = ActToColorRamp(actFilePath, outImgFilePath = 'cement_test_01_actRamp.png')
print 'Color Table input is: {0}'.format(inColorTableRGB)
#build PS luminance sorted color table
outColorTableRGB = lumSortColorTable(inColorTableRGB)
#build and output the PS sorted color ramp
colorTableToImage(outColorTableRGB, imgFilePath = 'cement_test_01_sortedRamp.png')
#write the sorted colortable out as a new binary file
colorTableToAct(outColorTableRGB, actFilePath = 'cement_test_01_sorted.act')
#use an image to generate palette using PIL, and output to a ramp
pilImgPalette = pilImgPalette(origImgFilePath,
pType=Image.ADAPTIVE,
outImgFilePath = 'cement_test_01_pilRamp.png',
colors=256)
#build PIL luminance sorted color table
pilColorTableRGB = lumSortColorTable(pilImgPalette)
#output the PIL sorted color ramp
colorTableToImage(pilColorTableRGB, imgFilePath = 'cement_test_01_pilSortedRamp.png')
#write the colortable out as a binary file
colorTableToAct(pilColorTableRGB, actFilePath = 'cement_test_01_pilSorted.act')
##notice that the luminance sorted ramps from PS and PIL are perceptually identical
#now use the pilColorTableRGB as the palette to index the original image
#we will save that out just to have a look
#but then, we will replace the palette with a grayscale ramp and save it out again
imageWithRampToHeight(origImgFilePath, pilColorTableRGB,
outImgFilePath='cement_test_01_height.png',
tempOutFilePath='cement_test_01_TEMPindexed.bmp')
@Cubeir
Copy link

Cubeir commented Dec 17, 2020

How I'm gonna use this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment