afe1ce39cb
沿用彩色纹理的视图归属、接缝留白和自动页面尺寸。 已验证双视图与单视图 Blender 导出、相关测试及文档门禁。
175 lines
7.1 KiB
Python
175 lines
7.1 KiB
Python
"""Convert camera-projected patch cells to black capsule strokes.
|
|
|
|
Run with Blender's source geometry and the existing texture job. This first
|
|
camera-space prototype preserves the colour pattern's grid and visibility
|
|
projection. It does not enforce a physical minimum stroke width.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
|
from camera_grid_stroke_geometry import capsule_in_cell, projected_stroke_uv
|
|
from camera_grid_stroke_optimizer import optimize_angles
|
|
from patch_geometry import clip_rect
|
|
from patch_projection import build_patch_geometry
|
|
from random_patch_dd02c import (
|
|
_clipped_polygons,
|
|
_clipped_shapes,
|
|
_prepare_mesh_object,
|
|
_require_object,
|
|
_require_uv_layer,
|
|
fill_background,
|
|
fill_polygon,
|
|
save_png_image,
|
|
write_pdf_vector,
|
|
write_svg_vector,
|
|
)
|
|
|
|
MODE = "camera_grid_capsules"
|
|
DEFAULT_PARAMS = {
|
|
"seed": 7,
|
|
"width_ratio": 0.42,
|
|
"length_ratio": 0.84,
|
|
"orientation_mode": "random",
|
|
"proposals": 100000,
|
|
}
|
|
|
|
|
|
def _args():
|
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--job", type=Path, required=True)
|
|
parser.add_argument("--output-dir", type=Path, required=True)
|
|
parser.add_argument("--params", type=Path, default=None)
|
|
parser.add_argument("--artifact-prefix", default="camera_grid_stroke")
|
|
parser.add_argument("--seed", type=int, default=None)
|
|
parser.add_argument("--width-ratio", type=float, default=None)
|
|
parser.add_argument("--length-ratio", type=float, default=None)
|
|
parser.add_argument("--orientation-mode", choices=("random", "optimized"),
|
|
default=None, help="Use seeded random or experimental optimized angles")
|
|
parser.add_argument("--proposals", type=int, default=None,
|
|
help="Angle changes attempted in optimized mode")
|
|
return parser.parse_args(argv)
|
|
|
|
|
|
def _pixel_polygon(uv_polygon, width, height):
|
|
return [(u * (width - 1), v * (height - 1))
|
|
for u, v in clip_rect(uv_polygon, 0.0, 1.0, 0.0, 1.0)]
|
|
|
|
|
|
def render_stroke_polygons(uv_name, mesh, strokes, width, height, vector,
|
|
base):
|
|
"""Write the shared raster and vector artwork from UV polygons."""
|
|
image_name = f"CameraGridStroke_{uv_name}"
|
|
if image_name in bpy.data.images:
|
|
bpy.data.images.remove(bpy.data.images[image_name])
|
|
image = bpy.data.images.new(image_name, width=width, height=height, alpha=True)
|
|
pixels = list(image.pixels)
|
|
fill_background(pixels, width, height, (0, 0, 0, 1))
|
|
|
|
mesh_polygons = [_pixel_polygon(poly, width, height) for poly in mesh]
|
|
mesh_polygons = [poly for poly in mesh_polygons if len(poly) >= 3]
|
|
for poly in mesh_polygons:
|
|
fill_polygon(None, pixels, width, height, poly, (1, 1, 1, 1))
|
|
|
|
shapes = []
|
|
for polygon in strokes:
|
|
pixel_poly = _pixel_polygon(polygon, width, height)
|
|
if len(pixel_poly) < 3:
|
|
continue
|
|
shapes.append((pixel_poly, (0, 0, 0, 1)))
|
|
fill_polygon(None, pixels, width, height, pixel_poly, (0, 0, 0, 1))
|
|
|
|
image.pixels[:] = pixels
|
|
image.update()
|
|
save_png_image(image, base.with_suffix(".png"))
|
|
page_mesh = _clipped_polygons(mesh_polygons, width, height)
|
|
page_shapes = _clipped_shapes(shapes, width, height)
|
|
vector_args = (width, height, vector["width_mm"], vector["height_mm"])
|
|
write_svg_vector(base.with_suffix(".svg"), *vector_args,
|
|
page_mesh, page_shapes, [], (0, 0, 0, 1), uv_name,
|
|
artwork_label="stroke")
|
|
write_pdf_vector(base.with_suffix(".pdf"), *vector_args,
|
|
page_mesh, page_shapes, [], (0, 0, 0, 1),
|
|
artwork_label="stroke")
|
|
print(f"{uv_name}: {len(shapes)} projected stroke fragments")
|
|
|
|
|
|
def _render(uv_name, geometry, settings, output_dir, width_ratio, length_ratio,
|
|
angle_map=None, artifact_prefix="camera_grid_stroke"):
|
|
nx, ny = settings["nx"], settings["ny"]
|
|
seed = settings.get("seed") or 0
|
|
outline_cache = {}
|
|
strokes = []
|
|
for i, j, fragment, loop_uvs in geometry["camera_fragments"]:
|
|
key = (i, j)
|
|
if key not in outline_cache:
|
|
outline_cache[key] = capsule_in_cell(
|
|
i, j, nx, ny, seed, width_ratio, length_ratio,
|
|
angle=None if angle_map is None else float(angle_map[j, i]))
|
|
strokes.append(projected_stroke_uv(fragment, loop_uvs, outline_cache[key]))
|
|
label = artifact_prefix
|
|
if angle_map is not None and artifact_prefix == "camera_grid_stroke":
|
|
label += "_optimized"
|
|
base = output_dir / f"{label}_{uv_name}"
|
|
render_stroke_polygons(uv_name, geometry["mesh"], strokes,
|
|
settings["width"], settings["height"], settings["vector"], base)
|
|
|
|
|
|
def main():
|
|
args = _args()
|
|
params = dict(DEFAULT_PARAMS)
|
|
if args.params is not None:
|
|
loaded = json.loads(args.params.read_text(encoding="utf-8"))
|
|
if not isinstance(loaded, dict) or set(loaded) - set(DEFAULT_PARAMS):
|
|
raise ValueError("invalid stroke params")
|
|
params.update(loaded)
|
|
for key in DEFAULT_PARAMS:
|
|
value = getattr(args, key)
|
|
if value is not None:
|
|
params[key] = value
|
|
if not 0 < params["width_ratio"] <= params["length_ratio"] < 1:
|
|
raise ValueError("require 0 < width-ratio <= length-ratio < 1")
|
|
if params["orientation_mode"] not in ("random", "optimized") or params["proposals"] < 1:
|
|
raise ValueError("invalid orientation mode or proposal count")
|
|
job = json.loads(args.job.read_text(encoding="utf-8"))
|
|
if job.get("schema") != "orisys-sensor-design/texture-job-v3":
|
|
raise ValueError("camera-grid strokes require a single-view texture-job-v3")
|
|
settings = dict(job["settings"])
|
|
settings["seed"] = params["seed"]
|
|
obj = _prepare_mesh_object(job["objects"]["mesh"])
|
|
cam = _require_object(job["objects"]["camera"], "CAMERA", "Camera")
|
|
names = [entry["uv_map"] for entry in job["outputs"]]
|
|
for name in names:
|
|
_require_uv_layer(obj, name)
|
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
|
args.output_dir = args.output_dir.resolve()
|
|
angle_map = None
|
|
if params["orientation_mode"] == "optimized":
|
|
angle_map, report = optimize_angles(
|
|
settings["nx"], settings["ny"], settings.get("seed") or 0,
|
|
params["width_ratio"], params["length_ratio"], params["proposals"],
|
|
)
|
|
report_path = args.output_dir / f"{args.artifact_prefix}_optimization.json"
|
|
report_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
|
print(f"Camera-grid optimization: {report}")
|
|
geometry = build_patch_geometry(
|
|
obj, cam, bpy.context.scene, bpy.context.evaluated_depsgraph_get(),
|
|
names, settings["nx"], settings["ny"], settings["edge_sample_grid"],
|
|
capture_camera_fragments=True,
|
|
)
|
|
for name in names:
|
|
_render(name, geometry[name], settings, args.output_dir,
|
|
params["width_ratio"], params["length_ratio"], angle_map,
|
|
args.artifact_prefix)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|