From 66e680feff80e0ee298cbfa2c69fc5a85c9095d5 Mon Sep 17 00:00:00 2001 From: tcl Date: Wed, 18 Nov 2015 14:57:26 +0100 Subject: [PATCH 1/3] Added output for a Hypsographic curve. A small bit of polish. --- .../generated_blessed_images.py | 3 +- tests/draw_test.py | 11 +++- tests/drawing_functions_test.py | 7 +-- worldengine/cli/main.py | 45 +++++++++++----- worldengine/draw.py | 52 ++++++++++++++++++- 5 files changed, 98 insertions(+), 20 deletions(-) diff --git a/tests/blessed_images/generated_blessed_images.py b/tests/blessed_images/generated_blessed_images.py index cb35ce03..c5cd1993 100644 --- a/tests/blessed_images/generated_blessed_images.py +++ b/tests/blessed_images/generated_blessed_images.py @@ -27,8 +27,9 @@ def main(blessed_images_dir, tests_data_dir): draw_temperature_levels_on_file(w, "%s/temperature_28070.png" % blessed_images_dir) draw_biome_on_file(w, "%s/biome_28070.png" % blessed_images_dir) draw_scatter_plot_on_file(w, "%s/scatter_28070.png" % blessed_images_dir) + draw_hypsographic_plot_on_file(w, "%s/hypsographic_28070.png" % blessed_images_dir) draw_satellite_on_file(w, "%s/satellite_28070.png" % blessed_images_dir) - draw_ancientmap_on_file(w, "%s/ancientmap_28070_factor3.png" % blessed_images_dir, resize_factor=3) + draw_ancientmap_on_file(w, "%s/ancientmap_28070_factor2.png" % blessed_images_dir, resize_factor=2) img = PNGWriter.rgba_from_dimensions(w.width * 2, w.height * 2, "%s/rivers_28070_factor2.png" % blessed_images_dir) draw_rivers_on_image(w, img, factor=2) diff --git a/tests/draw_test.py b/tests/draw_test.py index 610029be..30470fdb 100644 --- a/tests/draw_test.py +++ b/tests/draw_test.py @@ -3,7 +3,8 @@ import numpy from worldengine.draw import _biome_colors, draw_simple_elevation, elevation_color, \ draw_elevation, draw_riversmap, draw_grayscale_heightmap, draw_ocean, draw_precipitation, \ - draw_world, draw_temperature_levels, draw_biome, draw_scatter_plot, draw_satellite + draw_world, draw_temperature_levels, draw_biome, draw_scatter_plot, draw_hypsographic_plot, \ + draw_satellite from worldengine.biome import Biome from worldengine.world import World from worldengine.image_io import PNGWriter, PNGReader @@ -154,6 +155,14 @@ def test_draw_scatter_plot(self): draw_scatter_plot(w, 512, target) self._assert_img_equal("scatter_28070", target) + def test_draw_hypsographic_plot(self): + x_bins = 800 + y_bins = 600 + w = World.open_protobuf("%s/seed_28070.world" % self.tests_data_dir) + target = PNGWriter.grayscale_from_dimensions(x_bins, y_bins, channel_bitdepth=8) + draw_hypsographic_plot(w, target, x_bins, y_bins) + self._assert_img_equal("hypsographic_28070", target) + def test_draw_satellite(self): w = World.open_protobuf("%s/seed_28070.world" % self.tests_data_dir) target = PNGWriter.rgba_from_dimensions(w.width, w.height) diff --git a/tests/drawing_functions_test.py b/tests/drawing_functions_test.py index 67496e23..31f0e5c5 100644 --- a/tests/drawing_functions_test.py +++ b/tests/drawing_functions_test.py @@ -13,9 +13,10 @@ def setUp(self): self.w = World.open_protobuf("%s/seed_28070.world" % self.tests_data_dir) def test_draw_ancient_map(self): - target = PNGWriter.rgba_from_dimensions(self.w.width * 3, self.w.height * 3) - draw_ancientmap(self.w, target, resize_factor=3) - self._assert_img_equal("ancientmap_28070_factor3", target) + factor = int(2) + target = PNGWriter.rgba_from_dimensions(self.w.width * factor, self.w.height * factor) + draw_ancientmap(self.w, target, resize_factor=factor) + self._assert_img_equal("ancientmap_28070_factor%i" % factor, target) def test_gradient(self): self._assert_are_colors_equal((10, 20, 40), diff --git a/worldengine/cli/main.py b/worldengine/cli/main.py index f07bd339..bfb80a6c 100644 --- a/worldengine/cli/main.py +++ b/worldengine/cli/main.py @@ -7,7 +7,7 @@ from worldengine.draw import draw_ancientmap_on_file, draw_biome_on_file, draw_ocean_on_file, \ draw_precipitation_on_file, draw_grayscale_heightmap_on_file, draw_simple_elevation_on_file, \ draw_temperature_levels_on_file, draw_riversmap_on_file, draw_scatter_plot_on_file, \ - draw_satellite_on_file, draw_icecaps_on_file + draw_hypsographic_plot_on_file, draw_satellite_on_file, draw_icecaps_on_file from worldengine.plates import world_gen, generate_plates_simulation from worldengine.imex import export from worldengine.step import Step @@ -84,18 +84,27 @@ def generate_rivers_map(world, filename): draw_riversmap_on_file(world, filename) print("+ rivers map generated in '%s'" % filename) -def draw_scatter_plot(world, filename): - draw_scatter_plot_on_file(world, filename) - print("+ scatter plot generated in '%s'" % filename) -def draw_satellite_map(world, filename): +def generate_satellite_map(world, filename): draw_satellite_on_file(world, filename) print("+ satellite map generated in '%s'" % filename) -def draw_icecaps_map(world, filename): + +def generate_icecaps_map(world, filename): draw_icecaps_on_file(world, filename) print("+ icecap map generated in '%s'" % filename) + +def generate_scatter_plot(world, filename): + draw_scatter_plot_on_file(world, filename) + print("+ scatter plot generated in '%s'" % filename) + + +def generate_hypsographic_plot(world, filename): + draw_hypsographic_plot_on_file(world, filename) + print("+ hypsographic plot generated in '%s'" % filename) + + def generate_plates(seed, world_name, output_dir, width, height, num_plates=10): """ @@ -317,12 +326,14 @@ def main(): g_generate.add_argument('--not-fade-borders', dest='fade_borders', action="store_false", help="Not fade borders", default=True) - g_generate.add_argument('--scatter', dest='scatter_plot', - action="store_true", help="generate scatter plot") g_generate.add_argument('--sat', dest='satelite_map', action="store_true", help="generate satellite map") g_generate.add_argument('--ice', dest='icecaps_map', action="store_true", help="generate ice caps map") + g_generate.add_argument('--scatter', dest='scatter_plot', + action="store_true", help="generate scatter plot") + g_generate.add_argument('--hypsographic', dest='hypsographic_plot', + action="store_true", help="generate hypsographic curve") # ----------------------------------------------------- g_ancient_map = parser.add_argument_group( @@ -468,7 +479,9 @@ def main(): for x in range(0,len(humids)): humids[x] = 1 - float(humids[x]) if args.scatter_plot and not generation_operation: - usage(error="Scatter plot can be produced only during world generation") + usage(error="Scatter plot can be produced only during world generation.") + if args.hypsographic_plot and not generation_operation: + usage(error="Hypsograpic curve can be produced only during world generation.") print('Worldengine - a world generator (v. %s)' % VERSION) print('-----------------------') @@ -486,6 +499,7 @@ def main(): print(' icecaps heightmap : %s' % args.icecaps_map) print(' rivers map : %s' % args.rivers_map) print(' scatter plot : %s' % args.scatter_plot) + print(' hypsographic plot : %s' % args.hypsographic_plot) print(' satellite map : %s' % args.satelite_map) print(' fade borders : %s' % args.fade_borders) if args.temps: @@ -542,15 +556,18 @@ def main(): if args.rivers_map: generate_rivers_map(world, '%s/%s_rivers.png' % (args.output_dir, world_name)) - if args.scatter_plot: - draw_scatter_plot(world, - '%s/%s_scatter.png' % (args.output_dir, world_name)) if args.satelite_map: - draw_satellite_map(world, + generate_satellite_map(world, '%s/%s_satellite.png' % (args.output_dir, world_name)) if args.icecaps_map: - draw_icecaps_map(world, + generate_icecaps_map(world, '%s/%s_icecaps.png' % (args.output_dir, world_name)) + if args.scatter_plot: + generate_scatter_plot(world, + '%s/%s_scatter.png' % (args.output_dir, world_name)) + if args.hypsographic_plot: + generate_hypsographic_plot(world, + '%s/%s_hypsographic.png' % (args.output_dir, world_name)) elif operation == 'plates': print('') # empty line diff --git a/worldengine/draw.py b/worldengine/draw.py index 663c0c1a..7493c4a2 100644 --- a/worldengine/draw.py +++ b/worldengine/draw.py @@ -732,7 +732,49 @@ def draw_scatter_plot(world, size, target): ny = (size - 1) * ((p - min_humidity) / humidity_delta) target.set_pixel(int(nx), (size - 1) - int(ny), (r, 128, b, 255)) - + + +def draw_hypsographic_plot(world, target, x_bins, y_bins): + # Will draw a Hypsograhpic plot that contains information about the distribution of elevations in the world. + # y-axis shows the elevation, x-axis shows how often a certain elevation occurs. + # For further details see: + # https://en.wikipedia.org/wiki/Elevation#Hypsography + # http://www.ngdc.noaa.gov/mgg/global/etopo1_surface_histogram.html + + assert int(x_bins) > 0 and int(y_bins) > 0, "Hypsographic curve needs more than 0 bins." + + # set colors + color_none = target.get_max_colors() # white + color_full = int(color_none / 2) # gray + color_line = 0 # black + + # Prepare a list of available elevations. Scale them to the y-axis and put them into a sorted list, + # smallest point to highest. + e = world.elevation['data'] + e_min = e.min() + e_max = e.max() + e = (e - e_min) / (e_max - e_min) # normalize to [0, 1] + e *= int(y_bins) - 0.01 # instead of "- 1", small trick to make the last bin contain more than one point + e = numpy.sort(e, axis=None) # flatten the array and order values by height + e = e.astype(dtype=numpy.uint16) # do not round (the graph wouldn't change, just shift) + + # Fill the plot. + # Start by calculating how many points of the heightmap have to be represented by a single bin (n). + # Then step through the bins and fill in points from highest to lowest. Every bin is set to the average height of + # the n points it represents. + # Instead of drawing to [y, x] draw to [y_bins - y - 1, x_bins - x - 1] to flip the output in x- and y-direction. + n = e.size / float(x_bins) + for x in range(x_bins): + avg = sum(e[int(x * n):int((x + 1) * n)]) + avg = int(avg / float(n)) # average height of n height-points + for y in range(y_bins): + target[y_bins - y - 1, x_bins - x - 1] = color_none if y > avg else color_full + + # Draw sea-level (a black line at sea-level). + sea = int(numpy.interp(world.sea_level(), [e_min, e_max], [0, y_bins])) # map sea-level to appropriate y-position + for x in range(x_bins): + target[y_bins - sea - 1, x] = color_line + # ------------- # Draw on files @@ -811,6 +853,14 @@ def draw_scatter_plot_on_file(world, filename): img.complete() +def draw_hypsographic_plot_on_file(world, filename): + x_bins = 800 + y_bins = 600 + img = PNGWriter.grayscale_from_dimensions(x_bins, y_bins, filename, channel_bitdepth=8) + draw_hypsographic_plot(world, img, x_bins, y_bins) + img.complete() + + def draw_satellite_on_file(world, filename): img = PNGWriter.rgba_from_dimensions(world.width, world.height, filename) draw_satellite(world, img) From 76e26224dcd415ba0d86495c7e3f7b099c82f525 Mon Sep 17 00:00:00 2001 From: tcl Date: Thu, 19 Nov 2015 00:33:08 +0100 Subject: [PATCH 2/3] Refinements to the hypsographic plot. --- worldengine/draw.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/worldengine/draw.py b/worldengine/draw.py index 7493c4a2..ad0cb54d 100644 --- a/worldengine/draw.py +++ b/worldengine/draw.py @@ -747,6 +747,7 @@ def draw_hypsographic_plot(world, target, x_bins, y_bins): color_none = target.get_max_colors() # white color_full = int(color_none / 2) # gray color_line = 0 # black + color_percent_sep = int(color_none * 0.8) # light-gray # Prepare a list of available elevations. Scale them to the y-axis and put them into a sorted list, # smallest point to highest. @@ -770,10 +771,25 @@ def draw_hypsographic_plot(world, target, x_bins, y_bins): for y in range(y_bins): target[y_bins - y - 1, x_bins - x - 1] = color_none if y > avg else color_full - # Draw sea-level (a black line at sea-level). - sea = int(numpy.interp(world.sea_level(), [e_min, e_max], [0, y_bins])) # map sea-level to appropriate y-position - for x in range(x_bins): - target[y_bins - sea - 1, x] = color_line + # Draw a vertical line every ten percent. + step = x_bins / 10.0 + for i in range(1, 10): # no lines at 0 and (x_bins - 1) + x = int(step * i) + for y in range(y_bins): + target[y, x_bins - x - 1] = color_percent_sep + + # Map sea-level etc. to appropriate y-positions in the graph. + lines = [] + for threshold in world.elevation['thresholds']: # see generation.py->initialize_ocean_and_thresholds() for details + if threshold[1] is None: + continue # TODO: remove if the thresholds are ever guaranteed to be numbers + lines.append(int(numpy.interp(threshold[1], [e_min, e_max], [0, y_bins]))) + + # Draw lines for sea-level, hill-level etc. The lines will get lighter with increasing height. + for i in range(len(lines)): + color = numpy.interp(i, [0, len(lines)], [color_line, color_none]) + for x in range(x_bins): + target[y_bins - lines[i] - 1, x] = color # ------------- From 027466636bc5b535eac28bb45814aa508f178523 Mon Sep 17 00:00:00 2001 From: tcl Date: Thu, 19 Nov 2015 16:46:09 +0100 Subject: [PATCH 3/3] Completely reworked to make use of matplotlib. --- requirements.txt | 1 + setup.py | 3 +- .../generated_blessed_images.py | 1 + tests/draw_test.py | 11 ++-- tox.ini | 1 + worldengine/cli/main.py | 3 +- worldengine/draw.py | 66 ------------------- worldengine/draw_plots.py | 52 +++++++++++++++ 8 files changed, 63 insertions(+), 75 deletions(-) create mode 100644 worldengine/draw_plots.py diff --git a/requirements.txt b/requirements.txt index 47f81745..32d564e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ pypng==0.0.18 PyPlatec==1.4.0 protobuf==3.0.0a3 six==1.10.0 +matplotlib \ No newline at end of file diff --git a/setup.py b/setup.py index 22866c32..da0cc8e7 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,8 @@ 'console_scripts': ['worldengine=worldengine.cli.main:main'], }, 'install_requires': ['PyPlatec==1.4.0', 'pypng>=0.0.18', 'numpy>=1.9.2, <= 1.10.0.post2', - 'argparse==1.2.1', 'noise==1.2.2', 'protobuf==3.0.0a3'], + 'argparse==1.2.1', 'noise==1.2.2', 'protobuf==3.0.0a3', + 'matplotlib'], 'license': 'MIT License' } diff --git a/tests/blessed_images/generated_blessed_images.py b/tests/blessed_images/generated_blessed_images.py index c5cd1993..d3083329 100644 --- a/tests/blessed_images/generated_blessed_images.py +++ b/tests/blessed_images/generated_blessed_images.py @@ -10,6 +10,7 @@ import os from worldengine.world import * from worldengine.draw import * +from worldengine.draw_plots import * from worldengine.image_io import PNGWriter diff --git a/tests/draw_test.py b/tests/draw_test.py index 30470fdb..d45f27b9 100644 --- a/tests/draw_test.py +++ b/tests/draw_test.py @@ -3,8 +3,8 @@ import numpy from worldengine.draw import _biome_colors, draw_simple_elevation, elevation_color, \ draw_elevation, draw_riversmap, draw_grayscale_heightmap, draw_ocean, draw_precipitation, \ - draw_world, draw_temperature_levels, draw_biome, draw_scatter_plot, draw_hypsographic_plot, \ - draw_satellite + draw_world, draw_temperature_levels, draw_biome, draw_scatter_plot, draw_satellite +from worldengine.draw_plots import draw_hypsographic_plot from worldengine.biome import Biome from worldengine.world import World from worldengine.image_io import PNGWriter, PNGReader @@ -156,12 +156,9 @@ def test_draw_scatter_plot(self): self._assert_img_equal("scatter_28070", target) def test_draw_hypsographic_plot(self): - x_bins = 800 - y_bins = 600 w = World.open_protobuf("%s/seed_28070.world" % self.tests_data_dir) - target = PNGWriter.grayscale_from_dimensions(x_bins, y_bins, channel_bitdepth=8) - draw_hypsographic_plot(w, target, x_bins, y_bins) - self._assert_img_equal("hypsographic_28070", target) + plot = draw_hypsographic_plot(w) + # self._assert_img_equal("hypsographic_28070", target) # TODO: find way to compare these def test_draw_satellite(self): w = World.open_protobuf("%s/seed_28070.world" % self.tests_data_dir) diff --git a/tox.ini b/tox.ini index b91be59e..99de69f8 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ deps = six pypng h5py + matplotlib [testenv] deps = diff --git a/worldengine/cli/main.py b/worldengine/cli/main.py index bfb80a6c..8ddee820 100644 --- a/worldengine/cli/main.py +++ b/worldengine/cli/main.py @@ -7,7 +7,8 @@ from worldengine.draw import draw_ancientmap_on_file, draw_biome_on_file, draw_ocean_on_file, \ draw_precipitation_on_file, draw_grayscale_heightmap_on_file, draw_simple_elevation_on_file, \ draw_temperature_levels_on_file, draw_riversmap_on_file, draw_scatter_plot_on_file, \ - draw_hypsographic_plot_on_file, draw_satellite_on_file, draw_icecaps_on_file + draw_satellite_on_file, draw_icecaps_on_file +from worldengine.draw_plots import draw_hypsographic_plot_on_file from worldengine.plates import world_gen, generate_plates_simulation from worldengine.imex import export from worldengine.step import Step diff --git a/worldengine/draw.py b/worldengine/draw.py index ad0cb54d..5e3a7fe4 100644 --- a/worldengine/draw.py +++ b/worldengine/draw.py @@ -734,64 +734,6 @@ def draw_scatter_plot(world, size, target): target.set_pixel(int(nx), (size - 1) - int(ny), (r, 128, b, 255)) -def draw_hypsographic_plot(world, target, x_bins, y_bins): - # Will draw a Hypsograhpic plot that contains information about the distribution of elevations in the world. - # y-axis shows the elevation, x-axis shows how often a certain elevation occurs. - # For further details see: - # https://en.wikipedia.org/wiki/Elevation#Hypsography - # http://www.ngdc.noaa.gov/mgg/global/etopo1_surface_histogram.html - - assert int(x_bins) > 0 and int(y_bins) > 0, "Hypsographic curve needs more than 0 bins." - - # set colors - color_none = target.get_max_colors() # white - color_full = int(color_none / 2) # gray - color_line = 0 # black - color_percent_sep = int(color_none * 0.8) # light-gray - - # Prepare a list of available elevations. Scale them to the y-axis and put them into a sorted list, - # smallest point to highest. - e = world.elevation['data'] - e_min = e.min() - e_max = e.max() - e = (e - e_min) / (e_max - e_min) # normalize to [0, 1] - e *= int(y_bins) - 0.01 # instead of "- 1", small trick to make the last bin contain more than one point - e = numpy.sort(e, axis=None) # flatten the array and order values by height - e = e.astype(dtype=numpy.uint16) # do not round (the graph wouldn't change, just shift) - - # Fill the plot. - # Start by calculating how many points of the heightmap have to be represented by a single bin (n). - # Then step through the bins and fill in points from highest to lowest. Every bin is set to the average height of - # the n points it represents. - # Instead of drawing to [y, x] draw to [y_bins - y - 1, x_bins - x - 1] to flip the output in x- and y-direction. - n = e.size / float(x_bins) - for x in range(x_bins): - avg = sum(e[int(x * n):int((x + 1) * n)]) - avg = int(avg / float(n)) # average height of n height-points - for y in range(y_bins): - target[y_bins - y - 1, x_bins - x - 1] = color_none if y > avg else color_full - - # Draw a vertical line every ten percent. - step = x_bins / 10.0 - for i in range(1, 10): # no lines at 0 and (x_bins - 1) - x = int(step * i) - for y in range(y_bins): - target[y, x_bins - x - 1] = color_percent_sep - - # Map sea-level etc. to appropriate y-positions in the graph. - lines = [] - for threshold in world.elevation['thresholds']: # see generation.py->initialize_ocean_and_thresholds() for details - if threshold[1] is None: - continue # TODO: remove if the thresholds are ever guaranteed to be numbers - lines.append(int(numpy.interp(threshold[1], [e_min, e_max], [0, y_bins]))) - - # Draw lines for sea-level, hill-level etc. The lines will get lighter with increasing height. - for i in range(len(lines)): - color = numpy.interp(i, [0, len(lines)], [color_line, color_none]) - for x in range(x_bins): - target[y_bins - lines[i] - 1, x] = color - - # ------------- # Draw on files # ------------- @@ -869,14 +811,6 @@ def draw_scatter_plot_on_file(world, filename): img.complete() -def draw_hypsographic_plot_on_file(world, filename): - x_bins = 800 - y_bins = 600 - img = PNGWriter.grayscale_from_dimensions(x_bins, y_bins, filename, channel_bitdepth=8) - draw_hypsographic_plot(world, img, x_bins, y_bins) - img.complete() - - def draw_satellite_on_file(world, filename): img = PNGWriter.rgba_from_dimensions(world.width, world.height, filename) draw_satellite(world, img) diff --git a/worldengine/draw_plots.py b/worldengine/draw_plots.py new file mode 100644 index 00000000..dfc250f4 --- /dev/null +++ b/worldengine/draw_plots.py @@ -0,0 +1,52 @@ +import numpy +import matplotlib.pyplot as plot + + +def draw_hypsographic_plot(world): + # Will draw a Hypsograhpic plot that contains information about the distribution of elevations in the world. + # y-axis shows the elevation, x-axis shows how often a certain elevation occurs. + # For further details see: + # https://en.wikipedia.org/wiki/Elevation#Hypsography + # http://www.ngdc.noaa.gov/mgg/global/etopo1_surface_histogram.html + + # Set up variables. + sea_level = world.sea_level() + fig = plot.figure() + p = fig.add_subplot(111) + + # Prepare a list of available elevations by putting them into a sorted list, smallest point to highest. + # 0 will refer to sea-level. + y = world.elevation['data'] - sea_level + y = numpy.sort(y, axis=None) # flatten the array and order values by height + + # Corresponding x-values. Inverted so the highest values are left instead of right. x refers to %, hence [0, 100]. + x = numpy.arange(100, 0, -100.0 / y.size) + + # Plot the data. + p.plot(x, y) + + # Cosmetics. + p.set_ylim(y.min(), y.max()) + p.fill_between(x, y.min(), y, color='0.8') + p.set_xlabel("Cumulative Area (% of World's Surface)") + p.set_ylabel("Elevation (no units)") + p.set_title("Hypsographic Curve - %s" % world.name) + + # Draw lines for sea-level, hill-level etc. The lines will get lighter with increasing height. + th = world.elevation['thresholds'] # see generation.py->initialize_ocean_and_thresholds() for details + for i in range(len(th)): + if th[i][1] is None: + continue + color = numpy.interp(i, [0, len(th)], [0, 1.0]) # grayscale values, black to white; white will never be used + p.axhline(th[i][1] - sea_level, color=str(color), linewidth=0.5) + p.text(99, th[i][1] - sea_level, th[i][0], fontsize=8, horizontalalignment='right', verticalalignment='bottom' if th[i][1] - sea_level > 0 else 'top') + + p.set_xticks(range(0, 101, 10)) + + return fig + + +def draw_hypsographic_plot_on_file(world, filename): + fig = draw_hypsographic_plot(world) + fig.savefig(filename) + plot.close(fig)