test(M00): add landmark-region visual diff
This commit is contained in:
@@ -3,6 +3,7 @@ extends SceneTree
|
||||
## Compares original-client JPG/PNG references with OpenWC checkpoint PNGs.
|
||||
## Usage: godot --headless --path . --script res://src/tools/compare_render_checkpoints.gd --
|
||||
## --reference <directory> --candidate <directory> [--output <report.json>]
|
||||
## [--region x,y,width,height]
|
||||
## [--pixel-threshold 0.04] [--mean-threshold 0.015] [--changed-ratio-threshold 0.01]
|
||||
## Use --self-test for a synthetic regression check that writes only under user://.
|
||||
|
||||
@@ -56,13 +57,18 @@ func _parse_arguments(raw_arguments: PackedStringArray) -> Dictionary:
|
||||
parsed.self_test = true
|
||||
index += 1
|
||||
continue
|
||||
if argument not in ["--reference", "--candidate", "--output", "--only", "--pixel-threshold", "--mean-threshold", "--changed-ratio-threshold"]:
|
||||
if argument not in ["--reference", "--candidate", "--output", "--only", "--region", "--pixel-threshold", "--mean-threshold", "--changed-ratio-threshold"]:
|
||||
return {"error": "unknown argument: %s" % argument}
|
||||
if index + 1 >= raw_arguments.size():
|
||||
return {"error": "missing value for %s" % argument}
|
||||
var key := argument.trim_prefix("--").replace("-", "_")
|
||||
var value := raw_arguments[index + 1]
|
||||
if key.ends_with("threshold"):
|
||||
if key == "region":
|
||||
var region_parse_result := _parse_comparison_region(value)
|
||||
if region_parse_result.has("error"):
|
||||
return region_parse_result
|
||||
parsed[key] = region_parse_result.region
|
||||
elif key.ends_with("threshold"):
|
||||
if not value.is_valid_float():
|
||||
return {"error": "%s requires a number" % argument}
|
||||
parsed[key] = float(value)
|
||||
@@ -72,6 +78,24 @@ func _parse_arguments(raw_arguments: PackedStringArray) -> Dictionary:
|
||||
return parsed
|
||||
|
||||
|
||||
func _parse_comparison_region(value: String) -> Dictionary:
|
||||
var components := value.split(",", false)
|
||||
if components.size() != 4:
|
||||
return {"error": "--region requires x,y,width,height"}
|
||||
for component in components:
|
||||
if not component.strip_edges().is_valid_int():
|
||||
return {"error": "--region components must be integers"}
|
||||
var region := Rect2i(
|
||||
int(components[0]),
|
||||
int(components[1]),
|
||||
int(components[2]),
|
||||
int(components[3])
|
||||
)
|
||||
if region.position.x < 0 or region.position.y < 0 or region.size.x <= 0 or region.size.y <= 0:
|
||||
return {"error": "--region requires non-negative x/y and positive width/height"}
|
||||
return {"region": region}
|
||||
|
||||
|
||||
func _compare_directories(reference_directory: String, candidate_directory: String, options: Dictionary) -> Dictionary:
|
||||
reference_directory = ProjectSettings.globalize_path(reference_directory)
|
||||
candidate_directory = ProjectSettings.globalize_path(candidate_directory)
|
||||
@@ -108,6 +132,7 @@ func _compare_directories(reference_directory: String, candidate_directory: Stri
|
||||
"pixel_threshold": options.pixel_threshold,
|
||||
"mean_threshold": options.mean_threshold,
|
||||
"changed_ratio_threshold": options.changed_ratio_threshold,
|
||||
"comparison_region": _region_array(options.get("region", Rect2i())),
|
||||
"compared_count": results.size() - missing_count,
|
||||
"failed_count": failed_count,
|
||||
"missing_count": missing_count,
|
||||
@@ -127,12 +152,22 @@ func _compare_images(reference_path: String, candidate_path: String, options: Di
|
||||
"reference_size": [reference_image.get_width(), reference_image.get_height()],
|
||||
"candidate_size": [candidate_image.get_width(), candidate_image.get_height()],
|
||||
}
|
||||
var comparison_region: Rect2i = options.get("region", Rect2i(Vector2i.ZERO, reference_image.get_size()))
|
||||
if comparison_region == Rect2i():
|
||||
comparison_region = Rect2i(Vector2i.ZERO, reference_image.get_size())
|
||||
var image_region := Rect2i(Vector2i.ZERO, reference_image.get_size())
|
||||
if not image_region.encloses(comparison_region):
|
||||
return {
|
||||
"status": "region_out_of_bounds",
|
||||
"region": _region_array(comparison_region),
|
||||
"image_size": [reference_image.get_width(), reference_image.get_height()],
|
||||
}
|
||||
|
||||
var error_sum := 0.0
|
||||
var changed_pixel_count := 0
|
||||
var pixel_count := reference_image.get_width() * reference_image.get_height()
|
||||
for y in reference_image.get_height():
|
||||
for x in reference_image.get_width():
|
||||
var pixel_count := comparison_region.size.x * comparison_region.size.y
|
||||
for y in range(comparison_region.position.y, comparison_region.end.y):
|
||||
for x in range(comparison_region.position.x, comparison_region.end.x):
|
||||
var perceptual_error := _perceptual_color_error(reference_image.get_pixel(x, y), candidate_image.get_pixel(x, y))
|
||||
error_sum += perceptual_error
|
||||
if perceptual_error > float(options.pixel_threshold):
|
||||
@@ -144,11 +179,18 @@ func _compare_images(reference_path: String, candidate_path: String, options: Di
|
||||
"status": "passed" if passed else "threshold_exceeded",
|
||||
"width": reference_image.get_width(),
|
||||
"height": reference_image.get_height(),
|
||||
"region": _region_array(comparison_region),
|
||||
"mean_perceptual_error": mean_perceptual_error,
|
||||
"changed_pixel_ratio": changed_pixel_ratio,
|
||||
}
|
||||
|
||||
|
||||
func _region_array(region: Rect2i) -> Array[int]:
|
||||
if region == Rect2i():
|
||||
return []
|
||||
return [region.position.x, region.position.y, region.size.x, region.size.y]
|
||||
|
||||
|
||||
func _perceptual_color_error(reference_color: Color, candidate_color: Color) -> float:
|
||||
var reference_linear := reference_color.srgb_to_linear()
|
||||
var candidate_linear := candidate_color.srgb_to_linear()
|
||||
@@ -229,6 +271,22 @@ func _run_self_test() -> int:
|
||||
if not identical_report.passed or changed_report.passed:
|
||||
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: expected identical pass and changed failure")
|
||||
return 1
|
||||
var region_options := options.duplicate()
|
||||
region_options["region"] = Rect2i(1, 1, 1, 1)
|
||||
var excluded_change_report := _compare_directories(reference_directory, changed_directory, region_options)
|
||||
if not excluded_change_report.passed or excluded_change_report.comparison_region != [1, 1, 1, 1]:
|
||||
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: region must exclude pixels outside the rectangle")
|
||||
return 1
|
||||
var out_of_bounds_options := options.duplicate()
|
||||
out_of_bounds_options["region"] = Rect2i(1, 1, 2, 2)
|
||||
var out_of_bounds_result := _compare_images(
|
||||
reference_directory.path_join("synthetic.jpg"),
|
||||
changed_directory.path_join("synthetic__cold_process.png"),
|
||||
out_of_bounds_options
|
||||
)
|
||||
if out_of_bounds_result.status != "region_out_of_bounds":
|
||||
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: out-of-bounds region must fail")
|
||||
return 1
|
||||
if reference_image.save_jpg(reference_directory.path_join("ignored.jpg"), 1.0) != OK:
|
||||
return 1
|
||||
var filtered_options := options.duplicate()
|
||||
|
||||
Reference in New Issue
Block a user