mirror of
https://github.com/revarbat/BOSL2.git
synced 2025-08-17 22:11:29 +02:00
output formatting improvements
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Utility to convert GeoTIFF data to OpenSCAD, JSON, or PNG grayscale formats.
|
# Utility to convert GeoTIFF data to OpenSCAD, JSON, or PNG grayscale formats.
|
||||||
# Written with a lot of back-and-forth collaboration with ChatGPT
|
# Written with some back-and-forth collaboration with ChatGPT
|
||||||
# 14 May 2025
|
# 16 May 2025
|
||||||
|
|
||||||
# Sources of Planetary/Moon GeoTIFF Data (information below may be out of date)
|
# Sources of Planetary/Moon GeoTIFF Data (information below may be out of date)
|
||||||
#
|
#
|
||||||
@@ -23,12 +23,17 @@
|
|||||||
# Files may be large (100–500 MB)! Some are .IMG or .JP2 and must be converted to .tif using GDAL.
|
# Files may be large (100–500 MB)! Some are .IMG or .JP2 and must be converted to .tif using GDAL.
|
||||||
# Some planetary datasets use planetocentric or planetographic projections — still usable for 2D mapping.
|
# Some planetary datasets use planetocentric or planetographic projections — still usable for 2D mapping.
|
||||||
|
|
||||||
|
# ----------------------------
|
||||||
|
# Required modules
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
# builtin modules that should always be available
|
# builtin modules that should always be available
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
# Require necessary other modules
|
||||||
def require_module(name, alias=None, install_hint=None):
|
def require_module(name, alias=None, install_hint=None):
|
||||||
try:
|
try:
|
||||||
module = __import__(name)
|
module = __import__(name)
|
||||||
@@ -44,7 +49,6 @@ def require_module(name, alias=None, install_hint=None):
|
|||||||
print(f"Try: pip install {name}")
|
print(f"Try: pip install {name}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Require necessary other modules
|
|
||||||
require_module('rasterio', install_hint='pip install rasterio')
|
require_module('rasterio', install_hint='pip install rasterio')
|
||||||
require_module('numpy', alias='np', install_hint='pip install numpy')
|
require_module('numpy', alias='np', install_hint='pip install numpy')
|
||||||
require_module('PIL.Image', alias='Image', install_hint='pip install pillow')
|
require_module('PIL.Image', alias='Image', install_hint='pip install pillow')
|
||||||
@@ -54,6 +58,7 @@ from rasterio.enums import Resampling
|
|||||||
# ----------------------------
|
# ----------------------------
|
||||||
# Command-line argument parsing
|
# Command-line argument parsing
|
||||||
# ----------------------------
|
# ----------------------------
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Convert a GeoTIFF elevation file to an OpenSCAD 2D array using nonlinear elevation scaling.",
|
description="Convert a GeoTIFF elevation file to an OpenSCAD 2D array using nonlinear elevation scaling.",
|
||||||
epilog="""Examples:
|
epilog="""Examples:
|
||||||
@@ -81,9 +86,8 @@ if output_ext not in [".scad", ".json", ".png"]:
|
|||||||
output_type = output_ext[1:] # Removes the dot, e.g., 'json', 'png', 'scad'
|
output_type = output_ext[1:] # Removes the dot, e.g., 'json', 'png', 'scad'
|
||||||
output_filename = args.output
|
output_filename = args.output
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# Parse resize dimensions
|
# Parse resize dimensions
|
||||||
# ----------------------------
|
|
||||||
def parse_resize(resize_str, aspect):
|
def parse_resize(resize_str, aspect):
|
||||||
if "x" in resize_str:
|
if "x" in resize_str:
|
||||||
w, h = map(int, resize_str.lower().split("x"))
|
w, h = map(int, resize_str.lower().split("x"))
|
||||||
@@ -102,21 +106,18 @@ with rasterio.open(args.input_file) as src:
|
|||||||
input_width = src.width
|
input_width = src.width
|
||||||
input_height = src.height
|
input_height = src.height
|
||||||
output_width, output_height = parse_resize(args.resize, input_width/input_height)
|
output_width, output_height = parse_resize(args.resize, input_width/input_height)
|
||||||
print(f"Reading data from {args.input_file} and resampling to {output_width}×{output_height}")
|
print(f"Reading data from {args.input_file} and resampling")
|
||||||
data = src.read(1, out_shape=(1, output_height, output_width), resampling=Resampling.bilinear)
|
data = src.read(1, out_shape=(1, output_height, output_width), resampling=Resampling.bilinear)
|
||||||
print("Processing data")
|
|
||||||
# Replace nodata values
|
# Replace nodata values
|
||||||
nodata = src.nodata
|
nodata = src.nodata
|
||||||
if nodata is not None:
|
if nodata is not None:
|
||||||
data[data == nodata] = 0
|
data[data == nodata] = 0
|
||||||
data = np.nan_to_num(data, nan=0)
|
data = np.nan_to_num(data, nan=0)
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# Basic elevation stats
|
# Basic elevation stats
|
||||||
# ----------------------------
|
|
||||||
raw_min = np.min(data)
|
raw_min = np.min(data)
|
||||||
raw_max = np.max(data)
|
raw_max = np.max(data)
|
||||||
print(f"Elevations after resampling: min={raw_min}, max={raw_max}")
|
|
||||||
|
|
||||||
min_land_value = args.min_land_value # e.g. 0.04
|
min_land_value = args.min_land_value # e.g. 0.04
|
||||||
land_mask = data > 0 # positive elevations
|
land_mask = data > 0 # positive elevations
|
||||||
@@ -157,45 +158,55 @@ if np.any(sea_mask):
|
|||||||
# Map sea to [ -min_land_value … more negative ]
|
# Map sea to [ -min_land_value … more negative ]
|
||||||
scaled[sea_mask] = -((sea_data - min_sea) * scale_factor + min_land_value)
|
scaled[sea_mask] = -((sea_data - min_sea) * scale_factor + min_land_value)
|
||||||
|
|
||||||
# -----------------------------------------------------------------
|
# ----------------------------
|
||||||
|
# Output
|
||||||
|
# ----------------------------
|
||||||
|
|
||||||
# Compact formatter for json (no unnecessary whitespace)
|
# Compact formatter for OpenSCAD (no unnecessary whitespace, no leading zero before decimal point)
|
||||||
def format_json_array(data_array):
|
|
||||||
return json.dumps(data_array, separators=(',', ':'))
|
|
||||||
|
|
||||||
# Compact formatter for OpenSCAD (no unnecessary whitespace)
|
|
||||||
def format_val(val):
|
def format_val(val):
|
||||||
# Omit leading 0 and trailing zeros
|
# Omit leading 0 and trailing zeros
|
||||||
out = f"{val:.2f}".lstrip("0").rstrip("0").rstrip(".") if val >= 0 else f"-{abs(val):.2f}".lstrip("0").rstrip("0").rstrip(".")
|
out = f"{val:.2f}".lstrip("0").rstrip("0").rstrip(".") if val >= 0 else f"-{abs(val):.2f}".lstrip("0").rstrip("0").rstrip(".")
|
||||||
if (len(out) == 0): return "0"
|
if (len(out) == 0): return "0"
|
||||||
else: return out
|
else: return out
|
||||||
|
|
||||||
print("Writing output file")
|
# Compact formatter for json (no unnecessary whitespace, but has leading zeros for json standards compliance)
|
||||||
|
def format_json_array(data_array):
|
||||||
|
return json.dumps(data_array, separators=(',', ':'))
|
||||||
|
|
||||||
|
print(f"Original resolution: {src.width}×{src.height}")
|
||||||
|
print(f"Output resolution: {output_width}×{output_height}")
|
||||||
|
print(f"Resampled elevation range: {raw_min} to {raw_max}")
|
||||||
|
scel_min = np.min(scaled)
|
||||||
|
scel_max = np.max(scaled)
|
||||||
|
if output_type=="png":
|
||||||
|
# Normalize to 0–255 for 8-bit grayscale
|
||||||
|
scaled = (scaled - scaled.min()) / (scaled.max() - scaled.min())
|
||||||
|
scel_min = np.min(scaled*255).astype(np.uint8)
|
||||||
|
scel_max = np.max(scaled*255).astype(np.uint8)
|
||||||
|
print(f"Scaled elevation range: {format_val(scel_min)} to {format_val(scel_max)}")
|
||||||
|
print(f"Writing output file {output_filename}")
|
||||||
|
|
||||||
if output_type=="json":
|
if output_type=="json":
|
||||||
formatted_array = [
|
formatted_array = [
|
||||||
[format_val(val) for val in row] for row in scaled.tolist()
|
[round(val, 2) for val in row] for row in scaled.tolist()
|
||||||
]
|
]
|
||||||
with open(output_filename, "w") as f:
|
with open(output_filename, "w") as f:
|
||||||
json.dump({args.varname: formatted_array}, f, separators=(",", ":"))
|
json.dump({args.varname: formatted_array}, f, separators=(",", ":"))
|
||||||
elif output_type=="png":
|
elif output_type=="png":
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
# Normalize to 0–255 for 8-bit grayscale
|
img_array = (scaled * 255).astype(np.uint8)
|
||||||
scaled_normalized = (scaled - scaled.min()) / (scaled.max() - scaled.min())
|
|
||||||
img_array = (scaled_normalized * 255).astype(np.uint8)
|
|
||||||
img = Image.fromarray(img_array, mode='L')
|
img = Image.fromarray(img_array, mode='L')
|
||||||
img.save(output_filename)
|
img.save(output_filename)
|
||||||
else: # output .scad
|
else: # output .scad
|
||||||
with open(output_filename, "w") as f:
|
with open(output_filename, "w") as f:
|
||||||
f.write(f"// Auto-generated terrain data\n")
|
f.write(f"// Auto-generated terrain data\n")
|
||||||
f.write(f"// Source file: {args.input_file}\n")
|
f.write(f"// Source file: {args.input_file}\n")
|
||||||
f.write(f"// Original resolution: {src.width}x{src.height}\n")
|
f.write(f"// Original resolution: {src.width}×{src.height}\n")
|
||||||
f.write(f"// Output resolution: {output_width}x{output_height}\n")
|
f.write(f"// Output resolution: {output_width}×{output_height}\n")
|
||||||
f.write(f"// Raw elevation range: {raw_min:.2f} to {raw_max:.2f} meters\n")
|
f.write(f"// Resampled elevation range: {raw_min} to {raw_max} meters\n")
|
||||||
f.write(f"// Scaled value range: {np.min(scaled):.4f} to {np.max(scaled):.4f}\n")
|
f.write(f"// Scaled elevation range: {scel_min} to {scel_max}\n")
|
||||||
f.write(f"{args.varname} = [\n")
|
f.write(f"{args.varname} = [\n")
|
||||||
for row in scaled:
|
for row in scaled:
|
||||||
line = "[" + ",".join(format_val(val) for val in row) + "],\n"
|
line = "[" + ",".join(format_val(val) for val in row) + "],\n"
|
||||||
f.write(line)
|
f.write(line)
|
||||||
f.write("];\n")
|
f.write("];\n")
|
||||||
|
|
||||||
print(f"✅ Done: Output saved to {output_filename}")
|
|
||||||
|
@@ -317,7 +317,6 @@ Alpha channel is ignored. After processing the image as desired, you may save it
|
|||||||
let rotation = 0;
|
let rotation = 0;
|
||||||
let flipH = false;
|
let flipH = false;
|
||||||
let flipV = false;
|
let flipV = false;
|
||||||
let fileSuffix = "";
|
|
||||||
let origDim = { width:0, height:0 };
|
let origDim = { width:0, height:0 };
|
||||||
let uncropDim = { width:0, height:0 };
|
let uncropDim = { width:0, height:0 };
|
||||||
let cropDim = { width:0, height:0 };
|
let cropDim = { width:0, height:0 };
|
||||||
@@ -510,7 +509,6 @@ Alpha channel is ignored. After processing the image as desired, you may save it
|
|||||||
grayscaleCtx.restore();
|
grayscaleCtx.restore();
|
||||||
|
|
||||||
grayscaleSizeText.textContent = `Output size: ${finalWidth}×${finalHeight}`;
|
grayscaleSizeText.textContent = `Output size: ${finalWidth}×${finalHeight}`;
|
||||||
fileSuffix = finalWidth.toString()+"x"+finalHeight.toString();
|
|
||||||
updateKbytes();
|
updateKbytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,10 +713,11 @@ Alpha channel is ignored. After processing the image as desired, you may save it
|
|||||||
const arrayContent = grayscaleMatrix.map(row => {
|
const arrayContent = grayscaleMatrix.map(row => {
|
||||||
return " [" + row.map(val => useUnit ? parseFloat((val/255.0).toFixed(3)) : val).join(",") + "]";
|
return " [" + row.map(val => useUnit ? parseFloat((val/255.0).toFixed(3)) : val).join(",") + "]";
|
||||||
}).join(",\n");
|
}).join(",\n");
|
||||||
const openscadArray = (arrayName.value.length>0 ? arrayName.value : 'image_array')+" = [\n" + arrayContent + "\n];";
|
const introcomment = " = [ // " + cropDim.width + "×" + cropDim.height + "\n";
|
||||||
|
const dimSuffix = "_"+cropDim.width + "x" + cropDim.height
|
||||||
|
const openscadArray = (arrayName.value.length>0 ? arrayName.value : 'image_array') + introcomment + arrayContent + "\n];";
|
||||||
const blob = new Blob([openscadArray], { type: "text/plain" });
|
const blob = new Blob([openscadArray], { type: "text/plain" });
|
||||||
let suffix = fileSuffix.length>0 ? (arrayName.value.length>0 ? fileSuffix : "image"+fileSuffix) : "image";
|
let filename = (arrayName.value.length>0 ? arrayName.value : "image_array") + dimSuffix + '.scad';
|
||||||
let filename = arrayName.value.length>0 ? arrayName.value+'_'+suffix+'.scad' : suffix+'.scad';
|
|
||||||
if (window.showSaveFilePicker) {
|
if (window.showSaveFilePicker) {
|
||||||
saveWithFilePicker(blob, filename);
|
saveWithFilePicker(blob, filename);
|
||||||
} else {
|
} else {
|
||||||
|
Reference in New Issue
Block a user