Created
March 24, 2021 20:48
-
-
Save lasersox/0e2b3c173e1b1740971de0d975eeed57 to your computer and use it in GitHub Desktop.
This file contains 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 | |
# -*- 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 " 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