Skip to content

Commit 47ed25c

Browse files
authored
Merge pull request #18 from tyharwood/hotfix
Generic Interpretation Implementation
2 parents 7beb7bb + d948ab7 commit 47ed25c

File tree

10 files changed

+200
-152
lines changed

10 files changed

+200
-152
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ owmap [-h] [--mapname MAPNAME] [--terrainmap TERRAINMAP]
2727

2828
At its most basic, the cli tool can take 3 positional arguments like so:
2929

30-
`owmap [heightmap] [vegmap] [terrainmap]`
30+
`owmap [heightmap] [terrainmap] [vegmap] `
3131

3232
Each of these maps represents a different layer of Old World's tile system. All of these maps are required to build a proper world. The tool can also be run with flags instead of positional arguments, for example:
3333

34-
`owmap --terrainmap docs/donut.png --heightmap docs/donut_height.png --vegmap docs/donut_veg.png --mapname docs/donut.xml`
34+
`owmap --terrainmap docs/terrain_ex.png --heightmap docs/height_ex.png --vegmap docs/veg_ex.png --mapname docs/donut.xml`
3535

3636
or more succinctly:
3737

38-
`owmap -t docs/donut.png -e docs/donut_height.png -v docs/donut_veg.png -o docs/donut.xml`
38+
`owmap -t docs/terrain_ex.png -e docs/height_ex.png -v docs/veg_ex.png -o docs/donut.xml`
3939

4040
The order doesn't matter for flags.

docs/donut.png

-699 Bytes
Binary file not shown.

docs/donut_height.png

-1.03 KB
Binary file not shown.

docs/donut_veg.png

-776 Bytes
Binary file not shown.

docs/height_ex.png

412 Bytes
Loading

docs/terrain_ex.png

400 Bytes
Loading

docs/veg_ex.png

252 Bytes
Loading

owmap/cli.py

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,41 @@ def build_parser():
99

1010
parser.add_argument(
1111
'maps', nargs='*',
12-
help='Map images in order: [height] [vegetation] [terrain]'
12+
help='Map images in order: [height] [terrain] [vegetation]'
1313
)
1414

1515
parser.add_argument(
1616
'--mapname', '-o', default="newmap.xml",
1717
help='Output map name (default: map.xml)'
1818
)
1919

20-
parser.add_argument(
21-
'--terrainmap', '-t',
22-
help='Terrain map',
23-
)
2420
parser.add_argument(
2521
'--heightmap','-e',
2622
help='Height map '
2723
)
24+
25+
parser.add_argument(
26+
'--terrainmap', '-t',
27+
help='Terrain map',
28+
)
29+
2830
parser.add_argument(
2931
'--vegmap', '-v',
3032
help='Vegetation map'
3133
)
3234

35+
parser.add_argument(
36+
'--example', action='store_true',
37+
default=False,
38+
help='Generate an example image-set'
39+
)
40+
41+
parser.add_argument(
42+
'--genpalette', '-p',
43+
action='store_true', default=False,
44+
help='Generate a small image of the current palette used'
45+
)
46+
3347
return parser
3448

3549
# TODO: Finish CLI
@@ -40,26 +54,34 @@ def main():
4054
parser = build_parser()
4155
args = parser.parse_args()
4256

43-
if args.maps:
44-
if len(args.maps) != 3:
45-
parser.error(
46-
"\n\t3 positional arguments required: " \
47-
"[heightmap] [vegmap] [terrainmap]"
48-
)
49-
height, veg, terrain = args.maps
50-
51-
else:
52-
height = args.heightmap
53-
veg = args.vegmap
54-
terrain = args.terrainmap
55-
56-
if not all([height, veg, terrain]):
57-
parser.error(
58-
"\n\tMissing required arguments, 1 or more of: "\
59-
"[heightmap] [vegmap] [terrainmap]"
60-
)
61-
62-
genmap.process_map_images(height, veg, terrain, args.mapname)
57+
if args.example:
58+
genmap.generate_donut_map('./docs/')
59+
60+
if args.genpalette:
61+
#genmap.generate_palette()
62+
...
63+
64+
expected_args = ['height', 'terrain', 'veg']
65+
argdict = {
66+
'height': str(),
67+
'terrain':str(),
68+
'veg': str()
69+
}
70+
71+
for i in range(min(len(args.maps), len(expected_args))):
72+
argdict[expected_args[i]] = args.maps[i]
73+
74+
if args.heightmap: argdict['height'] = args.heightmap
75+
if args.terrainmap: argdict['terrain'] = args.terrainmap
76+
if args.vegmap: argdict['veg'] = args.vegmap
77+
78+
if not all([argdict['height'], argdict['terrain'], argdict['veg']]):
79+
parser.print_usage()
80+
parser.exit(2)
81+
82+
genmap.process_map_images(
83+
argdict['height'], argdict['terrain'], argdict['veg'], args.mapname
84+
)
6385

6486
if __name__ == "__main__":
6587
main()

owmap/genmap.py

Lines changed: 128 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,97 @@
22
from PIL import Image, ImageDraw
33
import xml.etree.ElementTree as ET
44

5-
def generate_donut_map(path='donut.png', size=128):
5+
# TODO: Have palette be imported from yaml files instead of hard-coded
6+
TERRAIN = {
7+
(0, 128, 0): "TERRAIN_LUSH",
8+
(255, 200, 0): "TERRAIN_ARID",
9+
(255, 255, 0): "TERRAIN_SAND",
10+
(0, 255, 0): "TERRAIN_TEMPERATE",
11+
(222, 222, 222): "TERRAIN_TUNDRA",
12+
(0, 222, 111): "TERRAIN_MARSH",
13+
(222, 0, 111): "TERRAIN_URBAN",
14+
(0, 0, 128): "TERRAIN_WATER",
15+
(0,0,0) : "Unknown"
16+
}
17+
HEIGHT = {
18+
(222, 222, 222): "HEIGHT_MOUNTAIN",
19+
(144, 144, 144): "HEIGHT_HILL",
20+
(111, 111, 111): "HEIGHT_FLAT",
21+
(0, 255, 255): "HEIGHT_LAKE",
22+
(0, 111, 255): "HEIGHT_COAST",
23+
(0, 0, 255): "HEIGHT_OCEAN",
24+
(255, 0, 0): "HEIGHT_VOLCANO"
25+
}
26+
VEGET = {
27+
(0, 90, 0): "VEGETATION_TREES",
28+
(200, 128, 0): "VEGETATION_SCRUB",
29+
}
30+
31+
def generate_donut_map(path='.', size=128):
632
"""Generate a donut-shaped map with concentric circles of different terrain types."""
7-
img = Image.new('RGB', (size, size), (255, 255, 255)) # ocean
8-
draw = ImageDraw.Draw(img)
33+
# --------------------------- TERRAIN ---------------------------
34+
terrain = Image.new('RGB', (size, size), (0, 0, 128)) # ocean
35+
draw = ImageDraw.Draw(terrain)
936

1037
center = size // 2
1138
outer_radius = int(size * 0.4)
1239
middle_radius = int(size * 0.3)
1340
inner_radius = int(size * 0.1)
1441

15-
# Draw circles from largest to smallest
1642
draw.ellipse([(center-outer_radius, center-outer_radius),
1743
(center+outer_radius, center+outer_radius)],
18-
fill=(255, 255, 255))
44+
fill=(255, 200, 0))
1945

2046
draw.ellipse([(center-middle_radius, center-middle_radius),
2147
(center+middle_radius, center+middle_radius)],
22-
fill=(0, 255, 0))
48+
fill=(0, 128, 0))
2349

2450
draw.ellipse([(center-inner_radius, center-inner_radius),
2551
(center+inner_radius, center+inner_radius)],
26-
fill=(255, 255, 255))
52+
fill=(0, 0, 128))
2753

28-
img.save(path)
54+
terrain.save(f"{path}/terrain_ex.png")
55+
56+
#----------------------VEG------------------------------------
57+
veg = Image.new('RGB', (size, size), (0, 0, 0)) # ocean
58+
draw = ImageDraw.Draw(veg)
59+
60+
center = size // 2
61+
outer_radius = int(size * 0.25)
62+
inner_radius = int(size * 0.12)
63+
64+
draw.ellipse([(center-outer_radius, center-outer_radius),
65+
(center+outer_radius, center+outer_radius)],
66+
fill=(0, 90, 0))
67+
68+
draw.ellipse([(center-inner_radius, center-inner_radius),
69+
(center+inner_radius, center+inner_radius)],
70+
fill=(0, 0, 0))
71+
72+
veg.save(f"{path}/veg_ex.png")
73+
# -----------------HEIGHT--------------------------------------
74+
height = Image.new('RGB', (size, size), (0, 111, 255)) # ocean
75+
draw = ImageDraw.Draw(height)
76+
77+
center = size // 2
78+
outer_radius = int(size * 0.4)
79+
middle_radius = int(size * 0.3)
80+
inner_radius = int(size * 0.1)
81+
82+
draw.ellipse([(center-outer_radius, center-outer_radius),
83+
(center+outer_radius, center+outer_radius)],
84+
fill=(144, 144, 144))
85+
86+
draw.ellipse([(center-middle_radius, center-middle_radius),
87+
(center+middle_radius, center+middle_radius)],
88+
fill=(111, 111, 111))
89+
90+
draw.ellipse([(center-inner_radius, center-inner_radius),
91+
(center+inner_radius, center+inner_radius)],
92+
fill=(0, 255, 255))
93+
94+
height.save(f"{path}/height_ex.png")
95+
2996

3097
def generate_palette(palette, filename):
3198
"""Generate a PNG image with one pixel for each color in the palette."""
@@ -43,112 +110,89 @@ def generate_palette(palette, filename):
43110
# TODO: Save palette as svg instead of png
44111
img.save(filename)
45112

46-
# TODO: Rewrite interpret_generic or impl. process_layer
47-
# Not amenable to current pipeline, rewrite or impl. process_layer
113+
48114
def interpret_generic(rgb, tilemap, tolerance=15):
49115
"""
50116
More generic function, not sure if this is the best way to do it.
51117
"""
52-
tiletype = tilemap.get(rgb, "Unknown")
53-
54-
if tiletype != "Unknown" and tiletype != "None":
118+
tiletype = tilemap.get(rgb, None)
119+
if tiletype:
55120
return tiletype
56-
121+
122+
best_fit = 255, "Unknown"
57123
for center_rgb, tiletype in tilemap.items():
58124
# Manhattan distance
59125
distance = sum(abs(a - b) for a, b in zip(rgb, center_rgb))
60-
if distance <= tolerance * 3: # tolerance per channel * 3 channels
61-
return tiletype
62-
63-
return "Unknown"
64-
65-
# Define mappings for terrain, height, vegetation, and rivers
66-
def interpret_rgb_as_terrain(rgb):
67-
"""Map RGB values to terrain types."""
68-
terrain_map = {
69-
(0, 128, 0): "TERRAIN_LUSH",
70-
(255, 200, 0): "TERRAIN_ARID",
71-
(255, 255, 0): "TERRAIN_SAND",
72-
(0, 255, 0): "TERRAIN_TEMPERATE",
73-
(222, 222, 222): "TERRAIN_TUNDRA",
74-
(0, 0, 0): "TERRAIN_URBAN",
75-
(0, 0, 128): "TERRAIN_WATER"
76-
}
77-
return terrain_map.get(rgb, "Unknown")
78-
79-
def interpret_rgb_as_height(rgb):
80-
"""Map RGB values to height levels."""
81-
height_map = {
82-
(222, 222, 222): "HEIGHT_MOUNTAIN",
83-
(144, 144, 144): "HEIGHT_HILL",
84-
(111, 111, 111): "HEIGHT_FLAT",
85-
(0, 255, 255): "HEIGHT_LAKE",
86-
(0, 111, 255): "HEIGHT_COAST",
87-
(0, 0, 255): "HEIGHT_OCEAN",
88-
(255, 0, 0): "HEIGHT_VOLCANO"
89-
}
90-
return height_map.get(rgb, "None")
91-
92-
def interpret_rgb_as_vegetation(rgb):
93-
"""Map RGB values to vegetation types."""
94-
vegetation_map = {
95-
(0, 90, 0): "VEGETATION_TREES",
96-
(200, 128, 0): "VEGETATION_SCRUB",
97-
(255,255,255): "None"
98-
}
99-
return vegetation_map.get(rgb, "None")
126+
if distance <= tolerance * 3 or distance < best_fit[0]: # tolerance per channel * 3 channels
127+
best_fit = distance, tiletype
128+
129+
return best_fit[1]
100130

101131
# TODO: Implement process_layer to make it modular
102-
def process_layer():
132+
def process_layer(imagefile, tilemap) -> ET.ElementTree:
103133
"""Generic version of loop code in process_map_images"""
104134
...
105135

106136
# TODO: Add default value when no layer given so all layers are optional
107-
def process_map_images(terrain_file, height_file, vegetation_file, output_file):
137+
def process_map_images(height_file, terrain_file, vegetation_file, output_file):
108138
"""Generate an XML map file from input PNG maps."""
109139
# Open images
110-
terrain_img = Image.open(terrain_file)
111-
height_img = Image.open(height_file)
112-
vegetation_img = Image.open(vegetation_file)
140+
height_img = Image.open(height_file) if height_file else None
141+
terrain_img = Image.open(terrain_file) if terrain_file else None
142+
vegetation_img = Image.open(vegetation_file) if vegetation_file else None
143+
144+
imgs = [height_img, terrain_img, vegetation_img]
145+
146+
# Check if there are any images to render
147+
# TODO: Error message / raise for no images given
148+
if all([not isinstance(img, Image.Image) for img in imgs]): return
113149

114150
# Ensure all images have the same dimensions
115-
w, h = terrain_img.size
116-
if any(img.size != (w, h) for img in [height_img, vegetation_img]):
151+
w,h = imgs[0].size
152+
if any(img.size != (w, h) for img in imgs if isinstance(img, Image.Image)):
117153
raise ValueError("All input images must have the same dimensions.")
118154

119-
# Prepare XML structure
155+
# Prepare XML
120156
root = ET.Element("Root", MapWidth=str(w), MapHeight=str(h), MapEdgesSafe="False")
121157

122158
id = 0
123159
for y in reversed(range(h)):
124160
for x in range(w):
125-
# Get pixel data
126-
terrain_rgb = terrain_img.getpixel((x, y))[:3] # RGB tuple
127-
height_rgb = height_img.getpixel((x, y))[:3]
128-
vegetation_rgb = vegetation_img.getpixel((x, y))[:3]
129-
130-
# Interpret pixel data
131-
terrain = interpret_rgb_as_terrain(terrain_rgb)
132-
height = interpret_rgb_as_height(height_rgb)
133-
vegetation = interpret_rgb_as_vegetation(vegetation_rgb)
134-
135161
# Create tile element
136162
tile = ET.SubElement(root, "Tile", ID=str(id))
137-
ET.SubElement(tile, "Terrain").text = terrain
138-
ET.SubElement(tile, "Height").text = height
139-
if vegetation:
140-
ET.SubElement(tile, "Vegetation").text = vegetation
163+
164+
# Terrain
165+
if terrain_img:
166+
terrain_rgb = terrain_img.getpixel((x, y))[:3] # RGB tuple
167+
terrain = interpret_generic(terrain_rgb, TERRAIN)
168+
ET.SubElement(tile, "Terrain").text = terrain
169+
170+
# Height
171+
if height_img:
172+
height_rgb = height_img.getpixel((x, y))[:3]
173+
height = interpret_generic(height_rgb, HEIGHT)
174+
if height != "Unknown":
175+
ET.SubElement(tile, "Height").text = height
176+
177+
178+
# Vegetation
179+
if vegetation_img:
180+
vegetation_rgb = vegetation_img.getpixel((x, y))[:3]
181+
vegetation = interpret_generic(vegetation_rgb, VEGET)
182+
183+
if vegetation != 'Unknown':
184+
ET.SubElement(tile, "Vegetation").text = vegetation
185+
186+
141187
id+=1
142-
# Write to output file
188+
189+
# Write out to XML doc
143190
tree = ET.ElementTree(root)
144191
tree.write(output_file, encoding="utf-8", xml_declaration=True)
145192

146-
# Example usage
147193
if __name__ == "__main__":
148-
terrain_file = "docs/terrainmap.png"
149-
height_file = "docs/heightmap.png"
150-
vegetation_file = "docs/vegmap.png"
151-
output_file = "docs/map.xml"
152194

153-
process_map_images(terrain_file, height_file, vegetation_file, output_file)
154-
#generate_donut_map('docs/donut.png')
195+
generate_palette(HEIGHT, 'docs/heightpalette.png')
196+
generate_palette(TERRAIN, 'docs/terrainpalette.png')
197+
198+
generate_palette(VEGET, 'docs/vegepalette.png')

0 commit comments

Comments
 (0)