Feat: Write mesh overlay as a separate <stem>_mesh image
--draw-mesh no longer modifies the primary render. The clean HDR FITS and tone-mapped image are written first, then the final image-plane mesh is overlaid in place on the already-consumed HDR buffer and saved as a <stem>_mesh sibling. This applies uniformly to single-frame, movie-frame, and imported lens-map renders, with derived paths validated before catalog, spacetime, and render initialization. Dummy PSF backends still write nothing and report ignoring --draw-mesh. The reference-image target now uses the configured image extension so the ENABLE_PNG=0 test path passes, and the CLI tests cover clean-main identity, sibling naming, PATH_MAX overflow, and zero-tolerance HDR comparison. README, README.zh-CN, and usage.md describe the new naming; the mesh reference asset is renamed to match.
This commit is contained in:
1 parent
e4d093a9fe
commit
2a0197229a
7 files changed
+267
-68
No files matched your search
+132
-48
@@ -159,18 +159,24 @@ static int validate_tonemapped_output_path(const char *path) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Length of the path up to (but excluding) its final extension. Only the
|
||||
* last path component is inspected, so dots inside directory names do not
|
||||
* count, and a leading-dot basename such as ".png" has no extension. */
|
||||
static size_t final_stem_length(const char *path) {
|
||||
const char *filename = strrchr(path, '/');
|
||||
const char *extension;
|
||||
|
||||
filename = filename == NULL ? path : filename + 1;
|
||||
extension = strrchr(filename, '.');
|
||||
return extension != NULL && extension != filename ? (size_t)(extension - path)
|
||||
: strlen(path);
|
||||
}
|
||||
|
||||
#ifdef ENABLE_HDR_OUTPUT
|
||||
static int make_hdr_output_path(char path[PATH_MAX], const char *output_path) {
|
||||
static const char hdr_suffix[] = "_HDR.fits";
|
||||
const char *filename = strrchr(output_path, '/');
|
||||
const char *extension;
|
||||
size_t stem_length;
|
||||
const size_t stem_length = final_stem_length(output_path);
|
||||
|
||||
filename = filename == NULL ? output_path : filename + 1;
|
||||
extension = strrchr(filename, '.');
|
||||
stem_length = extension != NULL && extension != filename
|
||||
? (size_t)(extension - output_path)
|
||||
: strlen(output_path);
|
||||
if (stem_length + sizeof hdr_suffix > PATH_MAX)
|
||||
return -1;
|
||||
memcpy(path, output_path, stem_length);
|
||||
@@ -179,6 +185,80 @@ static int make_hdr_output_path(char path[PATH_MAX], const char *output_path) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Derives the mesh-overlay sibling of a tone-mapped output path by inserting
|
||||
* "_mesh" before the final extension: image.png -> image_mesh.png. */
|
||||
static int make_mesh_output_path(char path[PATH_MAX], const char *output_path) {
|
||||
static const char mesh_infix[] = "_mesh";
|
||||
const size_t stem_length = final_stem_length(output_path);
|
||||
const char *suffix = output_path + stem_length;
|
||||
const size_t suffix_length = strlen(suffix);
|
||||
const size_t infix_length = sizeof mesh_infix - 1;
|
||||
|
||||
if (stem_length + infix_length + suffix_length + 1 > PATH_MAX)
|
||||
return -1;
|
||||
memcpy(path, output_path, stem_length);
|
||||
memcpy(path + stem_length, mesh_infix, infix_length);
|
||||
memcpy(path + stem_length + infix_length, suffix, suffix_length + 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *output_path;
|
||||
char mesh_path[PATH_MAX];
|
||||
int draw_mesh;
|
||||
} FrameOutputPaths;
|
||||
|
||||
/* Fills in the plain and mesh-overlay output paths for one frame, returning
|
||||
* -1 if a requested mesh sibling would overflow PATH_MAX. Callers choose the
|
||||
* timing; main() separately pre-validates the single-frame path before
|
||||
* expensive initialization. */
|
||||
static int build_frame_output_paths(const Settings *s, const char *output_path,
|
||||
FrameOutputPaths *paths) {
|
||||
paths->output_path = output_path;
|
||||
paths->draw_mesh = s->draw_mesh;
|
||||
if (paths->draw_mesh && make_mesh_output_path(paths->mesh_path, output_path)) {
|
||||
fprintf(stderr, "Mesh overlay output path is too long: %s\n", output_path);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Canonical output order for every frame: clean HDR, clean tone-mapped image,
|
||||
* then the mesh overlay. The overlay reuses the already-consumed HDR buffer,
|
||||
* so no second full-size framebuffer is allocated and nothing is re-rendered. */
|
||||
static int write_frame_outputs(const Settings *s, const FrameLensMesh *mesh,
|
||||
double *hdr, int width, int height, double fov_deg,
|
||||
const FrameOutputPaths *paths, size_t images,
|
||||
size_t stars, const char *note) {
|
||||
#ifdef ENABLE_HDR_OUTPUT
|
||||
if (s->write_hdr_output &&
|
||||
write_hdr_fits(s->hdr_output_path, hdr, width, height, fov_deg)) {
|
||||
fprintf(stderr, "Failed to write HDR FITS image: %s\n", s->hdr_output_path);
|
||||
return -1;
|
||||
}
|
||||
#else
|
||||
(void)s;
|
||||
(void)fov_deg;
|
||||
#endif
|
||||
const int write_result =
|
||||
write_tonemapped_image(paths->output_path, hdr, width, height);
|
||||
fprintf(stderr, "Rendered %zu images from %zu catalog stars to %s (%s%s)\n",
|
||||
images, stars, paths->output_path,
|
||||
write_result == 0 ? "ok" : "write failed", note);
|
||||
if (write_result)
|
||||
return -1;
|
||||
if (paths->draw_mesh) {
|
||||
frame_draw_mesh(mesh, hdr, width, height, 0.5, 0.5);
|
||||
if (write_tonemapped_image(paths->mesh_path, hdr, width, height)) {
|
||||
fprintf(stderr, "Failed to write mesh overlay image: %s\n",
|
||||
paths->mesh_path);
|
||||
return -1;
|
||||
}
|
||||
fprintf(stderr, "Wrote mesh overlay image: %s\n", paths->mesh_path);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_args(int argc, char **argv, Settings *s,
|
||||
const char **write_path) {
|
||||
#ifdef ENABLE_PNG
|
||||
@@ -415,8 +495,16 @@ static void print_help(const char *program) {
|
||||
" --refine-angle-rel R Relative angular interpolation error limit (default: 0.1)\n"
|
||||
" --refine-jacobian-min J Fold-refinement Jacobian threshold (default: 1e-3)\n"
|
||||
" --refine-min-edge-pixels P Stop refinement below this edge length (default: 0.5)\n"
|
||||
" --refine-min-area-pixels2 A Stop refinement below this triangle area (default: 0.25)\n"
|
||||
" --draw-mesh Draw the final lens mesh overlay (default: disabled)\n"
|
||||
" --refine-min-area-pixels2 A Stop refinement below this triangle area (default: 0.25)\n",
|
||||
stdout);
|
||||
#ifdef ENABLE_PNG
|
||||
fputs(" --draw-mesh Also write the final lens-mesh overlay as <output-stem>_mesh.png\n",
|
||||
stdout);
|
||||
#else
|
||||
fputs(" --draw-mesh Also write the final lens-mesh overlay as <output-stem>_mesh.ppm\n",
|
||||
stdout);
|
||||
#endif
|
||||
fputs(
|
||||
"\nMovie and observer track:\n"
|
||||
" --observer-track PATH Observer worldline/tetrad CSV for movie rendering (default: disabled)\n"
|
||||
" --movie-track-samples One frame per CSV row; ignores start-time/duration/fps (default: disabled)\n"
|
||||
@@ -722,22 +810,15 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog,
|
||||
free(hdr);
|
||||
return -1;
|
||||
}
|
||||
if (s->draw_mesh)
|
||||
frame_draw_mesh(&mesh, hdr, s->width, s->height, 0.5, 0.5);
|
||||
#ifdef ENABLE_HDR_OUTPUT
|
||||
if (s->write_hdr_output &&
|
||||
write_hdr_fits(s->hdr_output_path, hdr, s->width, s->height,
|
||||
s->horizontal_fov_deg)) {
|
||||
fprintf(stderr, "Failed to write HDR FITS image: %s\n", s->hdr_output_path);
|
||||
FrameOutputPaths output_paths;
|
||||
if (build_frame_output_paths(s, output_path, &output_paths)) {
|
||||
frame_lens_mesh_destroy(&mesh);
|
||||
free(hdr);
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
int result = write_tonemapped_image(output_path, hdr, s->width, s->height);
|
||||
fprintf(stderr, "Rendered %zu images from %zu catalog stars to %s (%s)\n",
|
||||
images, catalog->count, output_path,
|
||||
result == 0 ? "ok" : "write failed");
|
||||
const int result = write_frame_outputs(
|
||||
s, &mesh, hdr, s->width, s->height, s->horizontal_fov_deg, &output_paths,
|
||||
images, catalog->count, "");
|
||||
report_psf_splat(s, &psf_stats);
|
||||
frame_lens_mesh_destroy(&mesh);
|
||||
free(hdr);
|
||||
@@ -897,11 +978,13 @@ static int render_movie(const Settings *s, StarCatalog *catalog,
|
||||
}
|
||||
for (size_t i = 0; i < movie.frame_count; ++i) {
|
||||
char output_path[PATH_MAX];
|
||||
FrameOutputPaths output_paths;
|
||||
if (frame_output_path(output_path, s, movie.frames[i].frame_id) ||
|
||||
build_frame_output_paths(s, output_path, &output_paths))
|
||||
goto done;
|
||||
double *hdr = calloc((size_t)s->width * s->height * 3, sizeof *hdr);
|
||||
if (hdr == NULL || frame_output_path(output_path, s, movie.frames[i].frame_id)) {
|
||||
free(hdr);
|
||||
if (hdr == NULL)
|
||||
goto done;
|
||||
}
|
||||
CatalogPrefetchStats prefetch = {0};
|
||||
PsfSplatStats psf_stats = {0};
|
||||
RenderProgress progress = {
|
||||
@@ -925,12 +1008,10 @@ static int render_movie(const Settings *s, StarCatalog *catalog,
|
||||
free(hdr);
|
||||
goto done;
|
||||
}
|
||||
if (s->draw_mesh)
|
||||
frame_draw_mesh(&movie.frames[i].mesh, hdr, s->width, s->height, 0.5, 0.5);
|
||||
const int write_result = write_tonemapped_image(output_path, hdr, s->width, s->height);
|
||||
const int write_result = write_frame_outputs(
|
||||
s, &movie.frames[i].mesh, hdr, s->width, s->height,
|
||||
s->horizontal_fov_deg, &output_paths, images, catalog->count, "");
|
||||
free(hdr);
|
||||
fprintf(stderr, "Rendered %zu images from %zu catalog stars to %s (%s)\n",
|
||||
images, catalog->count, output_path, write_result == 0 ? "ok" : "write failed");
|
||||
report_psf_splat(s, &psf_stats);
|
||||
if (write_result)
|
||||
goto done;
|
||||
@@ -982,6 +1063,9 @@ static int render_lens_map(const Settings *s, StarCatalog *catalog) {
|
||||
for (size_t i = 0; i < map.frame_count; ++i) {
|
||||
const char *output_path = s->output_path;
|
||||
char movie_path[PATH_MAX];
|
||||
#ifndef PSF_BACKEND_DUMMY
|
||||
FrameOutputPaths output_paths;
|
||||
#endif
|
||||
if (map.frame_count > 1) {
|
||||
if (frame_output_path(movie_path, s, (size_t)map.frames[i].frame_id)) {
|
||||
fputs("Could not construct imported-map movie output path.\n", stderr);
|
||||
@@ -989,7 +1073,11 @@ static int render_lens_map(const Settings *s, StarCatalog *catalog) {
|
||||
}
|
||||
output_path = movie_path;
|
||||
}
|
||||
#ifdef PSF_BACKEND_DUMMY
|
||||
#ifndef PSF_BACKEND_DUMMY
|
||||
if (build_frame_output_paths(s, output_path, &output_paths)) {
|
||||
result = -1; break;
|
||||
}
|
||||
#else
|
||||
(void)output_path;
|
||||
#endif
|
||||
#ifdef PSF_BACKEND_DUMMY
|
||||
@@ -1020,28 +1108,17 @@ static int render_lens_map(const Settings *s, StarCatalog *catalog) {
|
||||
result = -1;
|
||||
break;
|
||||
}
|
||||
if (s->draw_mesh)
|
||||
#ifdef PSF_BACKEND_DUMMY
|
||||
fputs("Dummy PSF backend ignores --draw-mesh.\n", stderr);
|
||||
#else
|
||||
frame_draw_mesh(&map.frames[i].mesh, hdr, map.width, map.height, 0.5, 0.5);
|
||||
#endif
|
||||
#ifndef PSF_BACKEND_DUMMY
|
||||
#ifdef ENABLE_HDR_OUTPUT
|
||||
if (s->write_hdr_output &&
|
||||
write_hdr_fits(s->hdr_output_path, hdr, map.width, map.height,
|
||||
map.horizontal_fov_deg)) {
|
||||
fprintf(stderr, "Failed to write HDR FITS image: %s\n", s->hdr_output_path);
|
||||
free(hdr); result = -1; break;
|
||||
}
|
||||
#endif
|
||||
const int write_result = write_tonemapped_image(output_path, hdr, map.width, map.height);
|
||||
const int write_result = write_frame_outputs(
|
||||
s, &map.frames[i].mesh, hdr, map.width, map.height,
|
||||
map.horizontal_fov_deg, &output_paths, images, catalog->count,
|
||||
"; imported lens map");
|
||||
free(hdr);
|
||||
fprintf(stderr, "Rendered %zu images from %zu catalog stars to %s (%s; imported lens map)\n",
|
||||
images, catalog->count, output_path, write_result == 0 ? "ok" : "write failed");
|
||||
report_psf_splat(s, &psf_stats);
|
||||
if (write_result) { result = -1; break; }
|
||||
#else
|
||||
if (s->draw_mesh)
|
||||
fputs("Dummy PSF backend ignores --draw-mesh.\n", stderr);
|
||||
free(hdr);
|
||||
fprintf(stderr,
|
||||
"Dummy PSF classified %zu images from %zu catalog stars; no HDR, PNG, or PPM was written.\n",
|
||||
@@ -1162,6 +1239,13 @@ int main(int argc, char **argv) {
|
||||
return 2;
|
||||
}
|
||||
#endif
|
||||
if (settings.draw_mesh && settings.frames_dir == NULL) {
|
||||
char mesh_path[PATH_MAX];
|
||||
if (make_mesh_output_path(mesh_path, settings.output_path)) {
|
||||
fputs("Mesh overlay output path is too long.\n", stderr);
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if (resolve_camera(&settings)) return 2;
|
||||
SpacetimeSource spacetime = {0};
|
||||
ObserverState observer;
|
||||
|
||||
Reference in new issue
Block a user