Last active
December 17, 2020 19:55
-
-
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…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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') | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How I'm gonna use this?