Skip to content

Instantly share code, notes, and snippets.

@lasersox
Created March 24, 2021 20:48
Show Gist options
  • Save lasersox/0e2b3c173e1b1740971de0d975eeed57 to your computer and use it in GitHub Desktop.
Save lasersox/0e2b3c173e1b1740971de0d975eeed57 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
## changelog
# Original version by Alex Ross
# patch to validate html, fix hex parsing, and check PIL version by micheal -- 10/16/2007
"""gen_imgmap.py
Usage: gen_imgmap.py [-hi] image_file csv_file out_file
Options:
-i, --integer-colors Interpret color values as integers (hex is default).
-h, --help Show this information.
Arguments:
image_file – png with one color per clickable area. The areas do not
have to be contiguous, and may have holes.
csv_file – csv mapping colors to tag attributes. The first row must be
labels. The first column must be either a hexadecimal color, or
an integer color (specify -i for integer colors). The remaining
columns will be attached to the area tags as attribute values
(theattribute key will be the column label).
out_file – file name to which the generated image map will be written.
Requirements:
ImageMagick ≥ 6.3.0
Python ≥ 2.4
numpy ≥ 1.0
PIL ≥ 1.1.6
"""
import sys
import time
import os
import getopt
import csv
import Image
from numpy import *
pil_version = Image.VERSION.split('.')
pil_version =[int(e) for e in pil_version]
if not (len(pil_version) == 3 and pil_version[0] >= 1 and pil_version[1] >= 1 and pil_version[2] >= 6):
sys.stderr.write("your PIL version is too old %s minimum 1.1.6. needed\n" % Image.VERSION)
sys.exit(1)
sys.stdout = sys.stderr
def save(a, name):
Image.fromarray(bit2rgb(a)).save(name)
def bit2rgb(b):
""" Convert a bitmap to an RGB img. """
b = b[:,:,newaxis]*255
return concatenate((b, b, b), 2)
def pixels_of(img):
for y, x in combinations_of(range(img.shape[0]), range(img.shape[1])):
yield y,x
def combinations_of(A,B):
for a in A:
for b in B:
yield a,b
def dist(yx1, yx2):
""" Euclidean distance from `yx1` to `yx2`."""
y1, x1 = yx1
y2, x2 = yx2
return sqrt(float((x2-x1)**2 + (y2-y1)**2))
def fill(img, yx, val):
""" Fill `img` from `yx` with `val`. """
img = img.copy()
edge = [yx]
while edge:
newedge = []
for (y,x) in edge:
for (s, t) in ((y, x+1), (y, x-1), (y+1, x), (y-1, x)):
try:
p = img[s, t]
except IndexError:
pass
else:
if p != val:
img[s, t] = val
newedge.append((s, t))
edge = newedge
return img
def count_neighbors(yx, val, img):
""" Return the number of pixels adjacent to (Y,X) with value `val`. """
c = 0
for t,s in neighbors_of(yx, corners=False):
if (t < 0 or t >= img.shape[0]) or (s < 0 or s >= img.shape[1]):
c+=1
continue
if img[t,s] == val:
c+=1
return c
def neighbors_of(yx, corners=True):
""" Yield the coordinates of pixels that are adjacent yx.
If `corners` is False, don't yield the pixels that are adjacent
to the corners of `yx`.
"""
y,x = yx
if corners:
pixels = [(y-1, x-1), (y-1, x), (y-1, x+1),
( y, x-1), ( y, x+1),
(y+1, x-1), (y+1, x), (y+1, x+1)]
else:
pixels = [(y-1, x), ( y, x+1), (y+1, x), ( y, x-1),]
for yx in pixels:
yield yx
def shapes_from(a):
""" Yield the separate contiguous shapes of a. """
a = a.copy()
while True:
Y, X = a.nonzero()
YX = zip(Y,X)
y,x = min(YX)
b = fill(a, (y,x), 0)
s = logical_xor(b, a)
yield s
a = logical_xor(s,a)
if not a.any():
break
def poly_from(shp):
""" Return the bounding polygon of a contiguous, solid shp.
`shp` should be a binary numpy array. All parts of the shape should be
connected to all other parts of the shape (contiguous). There should not
be a hole in any part of the shape (solid).
"""
if not shp.any():
return []
Y, X = shp.nonzero()
YX = zip(Y,X)
y,x = min(YX)
wfirst = (y, x)
bfirst = (y-1, x)
hist = [(wfirst, bfirst)]
wcurs, bcurs = wfirst, bfirst
# Traverse the shape in a clockwise manner, recording all
# pixels as we go.
while True:
wn = [yx for yx in list(neighbors_of(wcurs))+[wcurs] if yx in YX]
bn = [yx for yx in list(neighbors_of(bcurs)) + [bcurs] if not yx in YX]
distances = [dist(w, b) for w, b in combinations_of(wn, bn)]
temp = sorted(zip(distances, list(combinations_of(wn, bn))))
next_curs = [wb for d, wb in temp if d < 2.0 and wb not in hist]
if all([w in [wh for wh, bh in hist if dist(b, bh) < 2] for w,b in next_curs]) or not next_curs:
break
for wb in next_curs:
wcurs, bcurs = wb
hist.append(wb)
break
poly = [w for w,b in hist]
return poly
def area_of(yx_1, yx_2, yx_3):
""" Euclidean area of the triangle `yx_1`, `yx_2`, `yx_3`. """
y1, x1 = yx_1
y2, x2 = yx_2
y3, x3 = yx_3
return 1/2. * abs(-x2*y1 + x3*y1 + x1*y2 - x3*y2 - x1*y3 + x2*y3)
def simplified(poly, min_area = 0.25):
""" Return a simplified version of `poly` (`poly` is a list or array of points.).
Uses a “triangle-based” simplification algorithm. For consecutive
points along the edge of `poly`"""
# simplify polygon
i = 0
while True:
if i+2 >= len(poly):
break
j,k,m = i, i+1, i+2
if area_of(poly[j], poly[k], poly[m]) < min_area:
del poly[k]
i = 0
continue
else:
i+=1
return poly
def flip_lonely_pixels(box, min_neighbors=2):
""" Fill in any lonely pixels (a pixel is lonely
if it has less than 2 identical neighbors).
"""
print " Flipping lonely pixels…"
box = box.copy()
while True:
boxl = box.copy()
for Y, X in pixels_of(box):
if count_neighbors((Y,X), box[Y,X], box) < min_neighbors:
box[Y,X] = not box[Y,X]
if (boxl == box).all():
break
return box
def fill_holes(shp):
""" Return `shp` with all of its holes filled in."""
e = shp.copy()
for yx in [(0,0), (-1,0), (0,-1), (-1,-1)]:
d = fill(shp, yx, True)
e = logical_or(e, d)
if logical_not(e).any():
# the shape has a hole
z_order = -1
else:
z_order = 1
filled = logical_or(shp, logical_not(e))
save(filled, 'filled.png')
return filled, z_order
def areatags_from(mask_path=None, kwds={}):
""" Return a list of html area tags for the mask at `mask_path`.
Any kwds is a dictionary of attributes which are added to
ALL of the returned tags. I.e. if you pass {href="http://google.com"},
all of the area tag will have “href="http://google.com"”.
"""
img = asarray(Image.open(mask_path))
img = img[:,:,0].astype(bool)
# get the smallest box containing the shape in a.
if not img.any():
return []
Y, X = img.nonzero()
miny, maxy = min(Y), max(Y)+1
minx, maxx = min(X), max(X)+1
pad = 3
box = zeros((maxy-miny + 2*pad, maxx - minx + 2*pad), bool)
box[pad:-pad, pad:-pad] = img[miny:maxy, minx:maxx]
box = flip_lonely_pixels(box)
if not box.any():
return []
# find polys
print " Finding shapes…",
shapes = []
for i, shp in enumerate(shapes_from(box)):
print i+1,
shp, z_order = fill_holes(shp)
shapes.append((shp, z_order))
print
print " Computing bounding polygons from shapes…"
polys = []
for i, (shp, z_order) in enumerate(shapes):
print " %i: z_order = %i" % (i, z_order)
polys.append((poly_from(shp), z_order))
# simplify polys
print " Simplifying polys…"
# two passes of simplification.
polys = [(simplified(poly, min_area=0.8), z_order) for poly, z_order in polys]
polys = [(simplified(poly[len(poly)/2:] + poly[:len(poly)/2], min_area=0.8), z_order) for poly, z_order in polys]
polys = [([(miny + y - pad + 2, minx + x - pad + 1) for y,x in poly], z_order) for poly, z_order in polys]
# format html
tags = []
print " Formatting area tag…"
for poly, z_order in polys:
tag = []
tag.append('<area shape="poly" ')
for key in kwds.keys():
tag.append('%s="%s" ' % (key, kwds[key]))
tag.append('coords="')
tag.append(", ".join("%i,%i" % (x,y) for y,x in poly))
tag.append('"></area>\n')
tags.append(("".join(tag), z_order))
return tags
def rgbint(color):
""" Converts an integer color code into an rgb tuple. """
color = int(color) - 2**25
return (color % 2**8, color % 2**16 / 2**8, color / 2**16)
def rgbhex(color):
""" Convert a hexadecimal color code into a rgb tuple. """
_2hex = lambda n: int(n,16)
if len(color) in [4,7]:
assert color[0] == "#"
color = color[1:]
if len(color) == 6:
s = [color[i:i+2] for i in range(0,5,2)]
else:
s = [color[i] for i in range(0,3)]
return tuple(map(_2hex, s))
def rgbtup(rgb):
r,g,b = rgb
return 2**25 + b * 2**16 + g * 2**8 + r
def process_csv(csv_file):
""" Generate a dictionary keyed by color from a csv. """
import csv as _csv
f = open(csv_file)
csv = _csv.reader(f)
# the first line should be the labels
attrs = {}
labels = csv.next()
labels = [lbl.lower() for lbl in labels]
assert "color" == labels[0]
for row in csv:
if row:
color = row[0]
attrs[color] = []
for x, b in enumerate(labels[1:]):
attrs[color].append((b, row[x+1].strip(" \"")))
return attrs
def generate_masks(base_img, attrs, integer_colors=False):
masks = []
if not os.path.exists("./masks"):
os.mkdir("masks")
for color in attrs:
if integer_colors:
rgb = rgbint(color)
else:
rgb = rgbhex(color)
label, value = attrs[color][0]
new_mask = "masks/%s.png" % value
if not os.path.exists(new_mask):
ex = 'convert %s -transparent "rgb%r" '\
'-fx "a" +matte -negate %s' % (base_img, rgb, new_mask)
print ex
os.system(ex)
masks.append([color, new_mask])
return masks
def generate_area_tags(masks, attrs):
area_tags = []
for color, m in masks:
print "Processing area tag(s) for %s…" % m
tags = areatags_from(m, dict(attrs[color]))
if not tags:
continue
for tag, z_order in tags:
if z_order > 0:
area_tags.insert(0, tag)
else:
area_tags.append(tag)
return area_tags
def timeit(func):
def wrapper(*args, **kwds):
begin_time = time.time()
result = func(*args, **kwds)
end_time = time.time()
print "Total time: %f minutes." % ((end_time - begin_time) / 60)
return result
return wrapper
@timeit
def main_(base_img, attrs_csv, output_file="out.html", integer_colors=False):
attrs = process_csv(attrs_csv)
masks = generate_masks(base_img, attrs, integer_colors)
area_tags = generate_area_tags(masks, attrs)
try:
html = file(output_file, "w")
html.write('<html><body>\n<map name="genmap">\n')
for tag in area_tags:
html.write(tag)
html.write("</map>\n")
html.write('<img src="%s" usemap="#genmap">\n' % base_img)
html.write("</body></html>\n")
finally:
html.close()
class Usage(Exception):
def __init__(self, msg):
self.msg = msg
def main(argv=None):
if argv is None:
argv = sys.argv
try:
try:
opts, args = getopt.getopt(argv[1:], "hi", ["help", "integer-colors"])
except getopt.error, msg:
raise Usage(msg)
# more code, unchanged
integer_colors = False
for opt in opts:
if opt[0] in ["-h", "--help"]:
print __doc__
return 2
elif opt[0] in ["-i", "--integer-colors"]:
integer_colors = True
if len(args) not in [2,3] :
raise Usage("Invalid arguments.")
return 2
else:
base_img, attrs_csv, out_file = args
main_(base_img, attrs_csv, out_file, integer_colors)
except Usage, err:
print >>sys.stderr, err.msg
print >>sys.stderr, "for help use --help"
return 2
if __name__ == "__main__":
sys.exit(main())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment