supports resources based on masks.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.godot/**
|
||||
tools/venv/**
|
||||
.import
|
||||
addons
|
||||
build/
|
||||
|
||||
4
PolygonPointsResource.gd
Normal file
4
PolygonPointsResource.gd
Normal file
@@ -0,0 +1,4 @@
|
||||
extends Resource
|
||||
class_name PolygonPointsResource
|
||||
|
||||
@export var points: PackedVector2Array
|
||||
1
PolygonPointsResource.gd.uid
Normal file
1
PolygonPointsResource.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dtemboas3bi8y
|
||||
17
ResourcePolygon2D.gd
Normal file
17
ResourcePolygon2D.gd
Normal file
@@ -0,0 +1,17 @@
|
||||
@tool
|
||||
extends Polygon2D
|
||||
class_name ResourcePolygon2D
|
||||
|
||||
@export var points_resource: PolygonPointsResource:
|
||||
set(value):
|
||||
points_resource = value
|
||||
_update_polygon()
|
||||
|
||||
|
||||
func _update_polygon() -> void:
|
||||
if points_resource and points_resource.points.size() > 0:
|
||||
polygon = points_resource.points
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_update_polygon()
|
||||
1
ResourcePolygon2D.gd.uid
Normal file
1
ResourcePolygon2D.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dcg8kp6a4rarx
|
||||
@@ -2,5 +2,3 @@ extends Node2D
|
||||
class_name ScalePoint
|
||||
|
||||
@export var target_scale: float
|
||||
|
||||
|
||||
|
||||
13
SetPiece_.gd
13
SetPiece_.gd
@@ -10,6 +10,7 @@ extends Polygon2D
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
_update_polygon_from_resource()
|
||||
self.color.a = 0.25
|
||||
if Engine.is_editor_hint():
|
||||
self.color.a = 0.25
|
||||
@@ -17,7 +18,12 @@ func _ready():
|
||||
pass
|
||||
else:
|
||||
hide()
|
||||
pass # Replace with function body.
|
||||
pass # Replace with function body
|
||||
|
||||
|
||||
func _update_polygon_from_resource() -> void:
|
||||
if points_resource and points_resource.points.size() > 0:
|
||||
polygon = points_resource.points
|
||||
|
||||
signal interacted
|
||||
signal walked
|
||||
@@ -28,6 +34,10 @@ signal entered(lab)
|
||||
signal exited
|
||||
|
||||
@export var label: String
|
||||
@export var points_resource: PolygonPointsResource:
|
||||
set(value):
|
||||
points_resource = value
|
||||
_update_polygon_from_resource()
|
||||
|
||||
var is_in = false
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
@@ -38,6 +48,7 @@ func _process(delta):
|
||||
if Geometry2D.is_point_in_polygon(to_local(get_global_mouse_position()), self.polygon):
|
||||
if is_in == false:
|
||||
is_in = true
|
||||
print("ENTERED0", label)
|
||||
emit_signal("entered", label)
|
||||
else:
|
||||
if is_in == true:
|
||||
|
||||
1
label.gd
1
label.gd
@@ -69,6 +69,7 @@ func _process(delta):
|
||||
|
||||
|
||||
func _on_setpiece_entered(lab):
|
||||
print("LABEL ENTERED ", lab)
|
||||
$label.show()
|
||||
$label.text = lab
|
||||
|
||||
|
||||
40
mask.png.import
Normal file
40
mask.png.import
Normal file
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cp5ufx8q34bfi"
|
||||
path="res://.godot/imported/mask.png-b945516e6475612c1c4c3b4f8dd0bdc6.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://mask.png"
|
||||
dest_files=["res://.godot/imported/mask.png-b945516e6475612c1c4c3b4f8dd0bdc6.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
7
new_resource.tres
Normal file
7
new_resource.tres
Normal file
@@ -0,0 +1,7 @@
|
||||
[gd_resource type="Resource" script_class="PolygonPointsResource" format=3 uid="uid://c13c8lnru1rsc"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dtemboas3bi8y" path="res://PolygonPointsResource.gd" id="1_g5ko2"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_g5ko2")
|
||||
metadata/_custom_type_script = "uid://dtemboas3bi8y"
|
||||
@@ -28,7 +28,6 @@ CameraTransition="*res://camera_transition.tscn"
|
||||
|
||||
window/size/viewport_width=1920
|
||||
window/size/viewport_height=1080
|
||||
window/size/mode=4
|
||||
window/size/initial_position_type=0
|
||||
window/stretch/mode="viewport"
|
||||
mouse_cursor/custom_image="res://boot_icon.png"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[ext_resource type="Texture2D" uid="uid://nb7ybebcy6ok" path="res://scenes/kq4_003_fountain_pool/caption_1_2884713022_generated.png" id="2_ev2w4"]
|
||||
[ext_resource type="Script" uid="uid://xmphq3i0wbg3" path="res://ScalePoint_.gd" id="3_1g2ot"]
|
||||
[ext_resource type="PackedScene" uid="uid://c4vc1wx7k6cw" path="res://TransitionPiece.tscn" id="4_6r684"]
|
||||
[ext_resource type="Resource" uid="uid://biiu4g5skgjun" path="res://scenes/kq4_003_fountain_pool/pool_shap2.tres" id="4_nuxlg"]
|
||||
[ext_resource type="Texture2D" uid="uid://bmvcn5pl2qble" path="res://scenes/kq4_003_fountain_pool/fg.png" id="5_cu368"]
|
||||
[ext_resource type="Script" uid="uid://bounwnqg34t5k" path="res://SetPiece_.gd" id="6_n67q9"]
|
||||
|
||||
@@ -92,31 +93,24 @@ position = Vector2(207, 489)
|
||||
[node name="exit" parent="kq4_027_forest_path" index="1"]
|
||||
position = Vector2(396, 438)
|
||||
|
||||
[node name="pool" type="Polygon2D" parent="."]
|
||||
position = Vector2(577, 565)
|
||||
polygon = PackedVector2Array(-297, 55, 23, 45, 623, 45, 873, 55, 1023, 115, 1103, 155, 1043, 215, 923, 295, 423, 305, -77, 300, -197, 275, -277, 215, -317, 135, -297, 55)
|
||||
color = Color(0, 0.407843, 0.776471, 0.25)
|
||||
script = ExtResource("6_n67q9")
|
||||
label = "Pool"
|
||||
|
||||
[node name="columns" type="Polygon2D" parent="."]
|
||||
[node name="columns" type="Polygon2D" parent="." unique_id=896145355]
|
||||
position = Vector2(-43, 689)
|
||||
polygon = PackedVector2Array(398, -10, 402, -118, -4, -128, 0, -19)
|
||||
color = Color(0.7, 0.7, 0.7, 0.25)
|
||||
polygon = PackedVector2Array(398, -10, 402, -118, -4, -128, 0, -19)
|
||||
script = ExtResource("6_n67q9")
|
||||
label = "Columns"
|
||||
|
||||
[node name="stairs" type="Polygon2D" parent="."]
|
||||
[node name="stairs" type="Polygon2D" parent="." unique_id=737804296]
|
||||
position = Vector2(1526, 653)
|
||||
polygon = PackedVector2Array(129, 92, 534, 73, 498, -75, 5, -56)
|
||||
color = Color(0.5, 0.5, 0.5, 0.25)
|
||||
polygon = PackedVector2Array(129, 92, 534, 73, 498, -75, 5, -56)
|
||||
script = ExtResource("6_n67q9")
|
||||
label = "Stairs"
|
||||
|
||||
[node name="ground" type="Polygon2D" parent="."]
|
||||
[node name="ground" type="Polygon2D" parent="." unique_id=1868664008]
|
||||
position = Vector2(-28.5625, 1097.27)
|
||||
polygon = PackedVector2Array(17.5625, -399.27, 392.211, -408.512, 261.641, -180.051, 550.586, -98.4498, 573.523, -160.27, 1285.42, -160.27, 1385.22, -90.7104, 1544.09, -439.453, 396.914, -535.582, 17.5547, -1051.08)
|
||||
color = Color(0.4, 0.27, 0.13, 0.25)
|
||||
polygon = PackedVector2Array(17.5625, -399.27, 392.211, -408.512, 261.641, -180.051, 550.586, -98.4498, 573.523, -160.27, 1285.42, -160.27, 1385.22, -90.7104, 1544.09, -439.453, 396.914, -535.582, 17.5547, -1051.08)
|
||||
script = ExtResource("6_n67q9")
|
||||
label = "Ground"
|
||||
|
||||
@@ -125,14 +119,22 @@ scale = Vector2(1.163, 1.179)
|
||||
texture = ExtResource("5_cu368")
|
||||
centered = false
|
||||
|
||||
[node name="pool" type="Polygon2D" parent="." unique_id=1959309137 groups=["set-piece"]]
|
||||
scale = Vector2(0.78, 0.777)
|
||||
color = Color(0, 0.407843, 0.776471, 0.25)
|
||||
polygon = PackedVector2Array(521.48, 955.4, 550.26, 943.14, 655.6, 946.46, 710.07, 906.29, 1799.47, 902.07, 1850.61, 939.09, 1906.74, 946.14, 1943.33, 962.53, 1953, 978, 1922.91, 1008.61, 1937.93, 1040.47, 1919.35, 1062.35, 1496.34, 1064, 1491.6, 1128.52, 1474.93, 1138.71, 1024.07, 1140.71, 1007.29, 1122.93, 1010.4, 1063.91, 596.53, 1061.93, 577.14, 1045.74, 578.99, 1014.44, 504.65, 1005.35, 492, 990)
|
||||
script = ExtResource("6_n67q9")
|
||||
label = "Pool"
|
||||
points_resource = ExtResource("4_nuxlg")
|
||||
|
||||
[connection signal="interacted" from="kq4_002_meadow" to="." method="_on_meadow_interacted"]
|
||||
[connection signal="interacted" from="kq4_004_ogres_cottage" to="." method="_on_ogre_house_interacted"]
|
||||
[connection signal="interacted" from="kq4_009_shady_wooded_area" to="." method="_on_shady_wooded_area_interacted"]
|
||||
[connection signal="interacted" from="kq4_027_forest_path" to="." method="_on_forest_path_27_interacted"]
|
||||
[connection signal="looked" from="pool" to="." method="_on_pool_looked"]
|
||||
[connection signal="looked" from="columns" to="." method="_on_columns_looked"]
|
||||
[connection signal="looked" from="stairs" to="." method="_on_stairs_looked"]
|
||||
[connection signal="looked" from="ground" to="." method="_on_ground_looked"]
|
||||
[connection signal="looked" from="pool" to="." method="_on_pool_looked"]
|
||||
|
||||
[editable path="kq4_002_meadow"]
|
||||
[editable path="kq4_004_ogres_cottage"]
|
||||
|
||||
8
scenes/kq4_003_fountain_pool/pool_shap.tres
Normal file
8
scenes/kq4_003_fountain_pool/pool_shap.tres
Normal file
@@ -0,0 +1,8 @@
|
||||
[gd_resource type="PolygonPointsResource" script_class="PolygonPointsResource" format=3 uid="uid://ba5nw7qr18eeq"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dtemboas3bi8y" path="res://PolygonPointsResource.gd" id="1_ppr"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_ppr")
|
||||
points = PackedVector2Array(2456.78, 1299.07, 2360.97, 1342.68, 920, 1454, 308.68, 1279.56, 134.7, 1193.86, 49.31, 1028.52, -14.39, 44.73, 33.27, -11.45, 76.7, -30.49, 955.2, -99.7, 2533.27, -92.39, 2578.15, -55.56, 2595, 0, 2586.34, 1059.38)
|
||||
metadata/_custom_type_script = "uid://dtemboas3bi8y"
|
||||
8
scenes/kq4_003_fountain_pool/pool_shap2.tres
Normal file
8
scenes/kq4_003_fountain_pool/pool_shap2.tres
Normal file
@@ -0,0 +1,8 @@
|
||||
[gd_resource type="Resource" script_class="PolygonPointsResource" format=3 uid="uid://biiu4g5skgjun"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dtemboas3bi8y" path="res://PolygonPointsResource.gd" id="1_ppr"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_ppr")
|
||||
points = PackedVector2Array(521.48, 955.4, 550.26, 943.14, 655.6, 946.46, 710.07, 906.29, 1799.47, 902.07, 1850.61, 939.09, 1906.74, 946.14, 1943.33, 962.53, 1953, 978, 1922.91, 1008.61, 1937.93, 1040.47, 1919.35, 1062.35, 1496.34, 1064, 1491.6, 1128.52, 1474.93, 1138.71, 1024.07, 1140.71, 1007.29, 1122.93, 1010.4, 1063.91, 596.53, 1061.93, 577.14, 1045.74, 578.99, 1014.44, 504.65, 1005.35, 492, 990)
|
||||
metadata/_custom_type_script = "uid://dtemboas3bi8y"
|
||||
40
test.png.import
Normal file
40
test.png.import
Normal file
@@ -0,0 +1,40 @@
|
||||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c1s78mcjg2jb5"
|
||||
path="res://.godot/imported/test.png-2b0b935732229e5bd5e655f2644b2498.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://test.png"
|
||||
dest_files=["res://.godot/imported/test.png-2b0b935732229e5bd5e655f2644b2498.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
0
tools/.gdignore
Normal file
0
tools/.gdignore
Normal file
284
tools/mask_to_polygon.py
Executable file
284
tools/mask_to_polygon.py
Executable file
@@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Convert a black/white mask image to a Godot .tres polygon resource.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import secrets
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from shapely.geometry import Polygon
|
||||
from shapely.ops import unary_union
|
||||
|
||||
|
||||
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
|
||||
def to_base36(num: int) -> str:
|
||||
"""Convert integer to lowercase base36 (like Godot)."""
|
||||
if num == 0:
|
||||
return "0"
|
||||
chars = []
|
||||
while num:
|
||||
num, rem = divmod(num, 36)
|
||||
chars.append(ALPHABET[rem])
|
||||
return "".join(reversed(chars))
|
||||
|
||||
|
||||
def generate_godot_uid() -> str:
|
||||
"""Generate a random Godot-compatible UID."""
|
||||
value = secrets.randbits(64)
|
||||
return to_base36(value)
|
||||
|
||||
|
||||
def find_largest_contour(contours: list) -> np.ndarray:
|
||||
"""Find the largest contour by area."""
|
||||
if not contours:
|
||||
return None
|
||||
return max(contours, key=cv2.contourArea)
|
||||
|
||||
|
||||
def contours_to_polygon(contours: list, mode: str) -> np.ndarray:
|
||||
"""
|
||||
Convert contours to a single polygon.
|
||||
|
||||
Args:
|
||||
contours: List of contours from cv2.findContours
|
||||
mode: "convex_hull" or "largest_only"
|
||||
|
||||
Returns:
|
||||
Single contour as numpy array
|
||||
"""
|
||||
if not contours:
|
||||
return None
|
||||
|
||||
if mode == "largest_only":
|
||||
return find_largest_contour(contours)
|
||||
|
||||
if mode == "convex_hull":
|
||||
all_points = np.vstack(contours)
|
||||
return cv2.convexHull(all_points)
|
||||
|
||||
raise ValueError(f"Unknown mode: {mode}")
|
||||
|
||||
|
||||
def simplify_to_target_points(contour: np.ndarray, target_points: int) -> np.ndarray:
|
||||
"""
|
||||
Simplify a contour to approximately target number of points.
|
||||
Uses Douglas-Peucker algorithm with iterative epsilon adjustment.
|
||||
"""
|
||||
if len(contour) <= target_points:
|
||||
return contour
|
||||
|
||||
contour = contour.squeeze()
|
||||
if len(contour.shape) == 1:
|
||||
return contour.reshape(-1, 1, 2)
|
||||
|
||||
perimeter = cv2.arcLength(contour, True)
|
||||
epsilon_low = 0.0
|
||||
epsilon_high = perimeter * 0.1
|
||||
|
||||
best_result = contour
|
||||
|
||||
for _ in range(50):
|
||||
epsilon_mid = (epsilon_low + epsilon_high) / 2
|
||||
simplified = cv2.approxPolyDP(contour, epsilon_mid, True)
|
||||
|
||||
if len(simplified) == target_points:
|
||||
return simplified
|
||||
|
||||
if len(simplified) > target_points:
|
||||
epsilon_low = epsilon_mid
|
||||
else:
|
||||
epsilon_high = epsilon_mid
|
||||
best_result = simplified
|
||||
|
||||
if abs(len(simplified) - target_points) <= 1:
|
||||
best_result = simplified
|
||||
break
|
||||
|
||||
return best_result
|
||||
|
||||
|
||||
def pad_polygon(points: np.ndarray, padding: float) -> np.ndarray:
|
||||
"""
|
||||
Expand polygon outward by padding pixels using Shapely buffer.
|
||||
"""
|
||||
if padding <= 0:
|
||||
return points
|
||||
|
||||
points_2d = points.squeeze()
|
||||
if len(points_2d.shape) == 1:
|
||||
return points
|
||||
|
||||
polygon = Polygon(points_2d)
|
||||
|
||||
if not polygon.is_valid:
|
||||
polygon = polygon.buffer(0)
|
||||
|
||||
buffered = polygon.buffer(padding)
|
||||
|
||||
if buffered.is_empty:
|
||||
return points
|
||||
|
||||
if buffered.geom_type == "MultiPolygon":
|
||||
buffered = max(buffered.geoms, key=lambda p: p.area)
|
||||
|
||||
exterior_coords = list(buffered.exterior.coords)[:-1]
|
||||
return np.array(exterior_coords, dtype=np.float32).reshape(-1, 1, 2)
|
||||
|
||||
|
||||
def ensure_clockwise(points: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Ensure points are in clockwise order (visually).
|
||||
In image coordinates (Y-down), clockwise means positive area.
|
||||
"""
|
||||
points_2d = points.squeeze()
|
||||
if len(points_2d.shape) == 1:
|
||||
return points
|
||||
|
||||
area = 0.0
|
||||
n = len(points_2d)
|
||||
for i in range(n):
|
||||
j = (i + 1) % n
|
||||
area += points_2d[i][0] * points_2d[j][1]
|
||||
area -= points_2d[j][0] * points_2d[i][1]
|
||||
|
||||
if area < 0:
|
||||
return points[::-1]
|
||||
|
||||
return points
|
||||
|
||||
|
||||
def write_preview(
|
||||
original_image: np.ndarray, points: np.ndarray, output_path: Path
|
||||
) -> None:
|
||||
"""
|
||||
Write a preview image showing the polygon drawn on the mask.
|
||||
"""
|
||||
preview = cv2.cvtColor(original_image, cv2.COLOR_GRAY2BGR)
|
||||
pts = points.squeeze().astype(np.int32).reshape(-1, 1, 2)
|
||||
cv2.polylines(preview, [pts], True, (0, 255, 0), 2)
|
||||
for i, pt in enumerate(pts):
|
||||
cv2.circle(preview, tuple(pt[0]), 4, (0, 0, 255), -1)
|
||||
cv2.putText(
|
||||
preview,
|
||||
str(i),
|
||||
(pt[0][0] + 5, pt[0][1] - 5),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.4,
|
||||
(255, 255, 0),
|
||||
1,
|
||||
)
|
||||
cv2.imwrite(str(output_path), preview)
|
||||
|
||||
|
||||
def write_tres(points: np.ndarray, output_path: Path, uid: str) -> None:
|
||||
"""
|
||||
Write polygon points to a Godot .tres resource file.
|
||||
"""
|
||||
points_2d = points.squeeze()
|
||||
if len(points_2d.shape) == 1:
|
||||
raise ValueError("Invalid points array")
|
||||
|
||||
coords = []
|
||||
for pt in points_2d:
|
||||
coords.extend([round(pt[0], 2), round(pt[1], 2)])
|
||||
|
||||
coords_str = ", ".join(str(c) for c in coords)
|
||||
|
||||
script_uid = "uid://dtemboas3bi8y"
|
||||
|
||||
content = f"""[gd_resource type="Resource" script_class="PolygonPointsResource" format=3 uid="uid://{uid}"]
|
||||
|
||||
[ext_resource type="Script" uid="{script_uid}" path="res://PolygonPointsResource.gd" id="1_ppr"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_ppr")
|
||||
points = PackedVector2Array({coords_str})
|
||||
metadata/_custom_type_script = "{script_uid}"
|
||||
"""
|
||||
|
||||
output_path.write_text(content)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Convert a black/white mask to a Godot polygon .tres resource"
|
||||
)
|
||||
parser.add_argument("image", type=Path, help="Input mask image (black/white)")
|
||||
parser.add_argument(
|
||||
"--padding",
|
||||
type=float,
|
||||
default=0,
|
||||
help="Pixels to expand polygon outward (default: 0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-points",
|
||||
type=int,
|
||||
default=10,
|
||||
help="Target number of polygon points (default: 10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Output .tres file path (default: <image_stem>.tres)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mode",
|
||||
choices=["convex_hull", "largest_only"],
|
||||
default="convex_hull",
|
||||
help="How to handle multiple regions (default: convex_hull)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--preview",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Output preview PNG showing polygon on mask",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.image.exists():
|
||||
print(f"Error: Image not found: {args.image}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
image = cv2.imread(str(args.image), cv2.IMREAD_GRAYSCALE)
|
||||
if image is None:
|
||||
print(f"Error: Could not load image: {args.image}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
_, binary = cv2.threshold(image, 127, 255, cv2.THRESH_BINARY)
|
||||
contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
if not contours:
|
||||
print("Error: No contours found in mask", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
polygon = contours_to_polygon(contours, args.mode)
|
||||
padded = pad_polygon(polygon, args.padding)
|
||||
simplified = simplify_to_target_points(padded, args.target_points)
|
||||
clockwise = ensure_clockwise(simplified)
|
||||
|
||||
output_path = args.output
|
||||
if output_path is None:
|
||||
output_path = args.image.with_suffix(".tres")
|
||||
|
||||
uid = generate_godot_uid()
|
||||
write_tres(clockwise, output_path, uid)
|
||||
|
||||
if args.preview:
|
||||
write_preview(image, clockwise, args.preview)
|
||||
print(f"Preview: {args.preview}")
|
||||
|
||||
print(f"Created: {output_path}")
|
||||
print(f"Points: {len(clockwise.squeeze())}")
|
||||
print(f"UID: uid://{uid}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
3
tools/requirements.txt
Normal file
3
tools/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
opencv-python
|
||||
numpy
|
||||
shapely
|
||||
Reference in New Issue
Block a user