diff --git a/.opencode/skills/alpha-mask-creator/SKILL.md b/.opencode/skills/alpha-mask-creator/SKILL.md new file mode 100644 index 0000000..e8db0e5 --- /dev/null +++ b/.opencode/skills/alpha-mask-creator/SKILL.md @@ -0,0 +1,65 @@ +--- +name: alpha-mask-creator +description: Takes an image and creates a high quality alpha mask of the requested entity. Typically invoked when the user asks to "create a specific alpha mask for" +--- +# Alpha Mask Creator + +This skill creates alpha masks from images based on user requests. Use this when the user asks to "create a specific alpha mask for..." along with providing an image. + +## Workflow + +### 1. Draw Polygon Outline +Use the `polygon-drawer` skill to draw a polygon outline around the identified object. Save the output to `./tmp/` with a sensible filename: +- Format: `..png` +- Example: If the user wants a door mask from `room_010.png`, name it `room_010.door.png` + +### 2. Extract Mask +Run the `extract_mask` script to generate the alpha mask: +```bash +source ./tools/venv/bin/activate +python tools/extract_mask.py "the door with the red outline" ./tmp/..png ./tmp/..mask.png +``` + +### 3. Quality Check +Examine the generated mask using the `image-inspector` agent: +- Check if the mask is precise and accurate +- Verify the object is properly isolated +- Ensure no artifacts or incorrect areas + +**If the mask quality is poor:** +- Retry the polygon-drawer step with different parameters or seed +- Iterate until the mask is satisfactory + +### 5. Report Results +Tell the user: +- Location of the polygon outline image: `./tmp/..png` +- Location of the mask: `./tmp/..mask.png` +- Confirm the mask quality is acceptable + +## Naming Conventions + +| User Request | Original File | Output Files | +|--------------|---------------|--------------| +| Door | `room_010.png` | `room_010.door.png`, `room_010.door.mask.png` | +| Tree | `forest_bg.png` | `forest_bg.tree.png`, `forest_bg.tree.mask.png` | +| Character | `scene_005.png` | `scene_005.character.png`, `scene_005.character.mask.png` | + +## Tools Used + +- `image-inspector` - Analyze images to locate objects +- `polygon-drawer` - Draw polygon outlines +- `bash` - Run extract_mask script +- `image-expert` - Verify mask quality + +## Example Interaction + +**User:** "Create a specific alpha mask for the door in this image" [attaches `cottage_exterior.png`] + +**You:** +1. Analyze the image to locate the door +2. Draw polygon around the door → `./tmp/cottage_exterior.door.png` +3. Extract mask → `./tmp/cottage_exterior.door.mask.png` +4. Verify mask quality +5. Report: "Mask created successfully! Files saved to: + - Polygon outline: `./tmp/cottage_exterior.door.png` + - Alpha mask: `./tmp/cottage_exterior.door.mask.png`" diff --git a/.opencode/skills/kq4-polygon-drawer/SKILL.md b/.opencode/skills/kq4-polygon-drawer/SKILL.md new file mode 100644 index 0000000..ba9da9b --- /dev/null +++ b/.opencode/skills/kq4-polygon-drawer/SKILL.md @@ -0,0 +1,67 @@ +# Polygon Drawer Skill + +This skill uses @tools/draw_polygon.py to draw polygons on any image. It's useful for highlighting areas, marking objects, creating masks, or annotating images with colored shapes. + +## Usage Examples + +### Draw a red box around an object +``` +Draw a red box around the door in image.png +# Executes: python tools/draw_polygon.py tmp/annotated_image.png "0.3,0.2 0.7,0.2 0.7,0.8 0.3,0.8" --color red --save tmp/annotated_image.png +# Opens: /home/noti/dev/ai-game-2/tmp/annotated_image.png +``` + +### Highlight an area in green +``` +Highlight the pool in green using a polygon +# Executes: python tools/draw_polygon.py tmp/pool_highlight.png "0.2,0.5 0.8,0.5 0.8,0.9 0.2,0.9" --color green --fill --save tmp/pool_highlight.png +# Opens: /home/noti/dev/ai-game-2/tmp/pool_highlight.png +``` + +### Draw a blue circle (approximated with polygon) +``` +Draw a blue circle around the tree +# Executes: python tools/draw_polygon.py tmp/tree_circle.png "0.4,0.3 0.6,0.3 0.6,0.5 0.4,0.5" --color blue --save tmp/tree_circle.png +# Opens: /home/noti/dev/ai-game-2/tmp/tree_circle.png +``` + +### Fill a region with semi-transparent overlay +``` +Fill the sky area with semi-transparent yellow +# Executes: python tools/draw_polygon.py tmp/sky_overlay.png "0,0 1,0 1,0.6 0,0.6" --color yellow --fill --save tmp/sky_overlay.png +# Opens: /home/noti/dev/ai-game-2/tmp/sky_overlay.png +``` + +### Mark a specific point with a small polygon +``` +Mark the character location with a red polygon +# Executes: python tools/draw_polygon.py tmp/character_mark.png "0.5,0.4 0.52,0.38 0.54,0.4 0.52,0.42" --color red --save tmp/character_mark.png +# Opens: /home/noti/dev/ai-game-2/tmp/character_mark.png +``` + +## How It Works + +1. **Analyze** the image to understand what needs highlighting +2. **Extract** polygon coordinates based on visual patterns or user input +3. **Execute** the draw_polygon.py script with percentage coordinates (0.0-1.0) +4. **Save** the result to ./tmp/ and display the file path +5. **Open** the output for you to view + +## Coordinate System + +Coordinates use **percentage mode** (0.0-1.0) by default: +- `0.0,0.0` = top-left corner +- `1.0,1.0` = bottom-right corner +- `0.5,0.5` = center of image + +## Common Use Cases + +- Highlighting objects or regions of interest +- Creating masks for image processing +- Marking locations for documentation +- Annotating images with colored boundaries +- Visualizing spatial relationships + +## Output + +All annotated images are saved to `./tmp/` and the full path is displayed so you can open them in your image viewer. diff --git a/.opencode/skills/polygon-drawer/SKILL.md b/.opencode/skills/polygon-drawer/SKILL.md new file mode 100644 index 0000000..4c6a18b --- /dev/null +++ b/.opencode/skills/polygon-drawer/SKILL.md @@ -0,0 +1,79 @@ +--- +name: polygon-drawer +description: Takes an image and adds a polygon to it at the user's request. Typically the user will ask to add a polygon around a particular object +--- +# Polygon Drawer Skill + +This skill uses @tools/draw_polygon.py to help you draw polygons on any image. It's useful for highlighting areas, drawing boxes, marking regions, or annotating images with colored shapes. + +## Usage Examples + +### Draw a red box around an object +``` +Draw a red box around the door in +``` + +### Highlight a specific area in green +``` +Highlight the pool area in with a green polygon +``` + +### Draw a blue rectangle with percentage coordinates +``` +Draw a blue polygon on using coordinates: 0.5,0.3 0.7,0.3 0.7,0.6 0.5,0.6 +``` + +### Mark a triangular region with fill +``` +Draw a semi-transparent yellow triangle on : 0.5,0.1 0.9,0.9 0.1,0.9 --fill +``` + +### Draw a custom-colored polygon with thick lines +``` +Draw a cyan polygon on : 0.2,0.2 0.8,0.2 0.8,0.8 0.2,0.8 --color cyan --thickness 5 +``` + +### Use pixel coordinates (absolute mode) +``` +Draw a red box using pixel coordinates: 100,50 400,50 400,300 100,300 --absolute +``` + +Make sure to quote the coordinates when calling the script: "100,50 400,50 400,300 100,300" + +## How It Works + +1. **Analyze** the image to identify what needs highlighting +2. **Calculate** polygon coordinates (default: percentage mode 0.0-1.0) +3. **Execute** `@tools/draw_polygon.py` with the calculated points +4. **Output** the resulting image to `./tmp/` directory +5. **Display** the save path so you can open it + +## Coordinate Systems + +- **Percentage mode** (default): 0.0-1.0 coordinates where (0,0) is top-left and (1,1) is bottom-right +- **Absolute mode**: Use `--absolute` flag for actual pixel coordinates + +## Output Location +By default, the image is just shown to the user without writing anything. + +If the user asks to save it, use the --save flag. + +All generated images are saved to: +``` +./tmp/ +``` + +The skill will output the full path to the saved image, e.g., `./tmp/output_1234567890.png` + +## Color Options + +- Named colors: red, green, blue, yellow, cyan, magenta, white, black, orange, purple, brown, pink, gray, grey, lime, navy, teal, maroon +- Hex codes: #FF0000, #00FF00FF, etc. + +## Flags + +- `--fill`: Fill polygon with semi-transparent color +- `--absolute`: Use pixel coordinates instead of percentages +- `--color `: Specify polygon color (default: red) +- `--thickness `: Line thickness (default: 4) +- `--save `: Override default save location (still goes to ./tmp/) diff --git a/AGENTS.md b/AGENTS.md index baeebcd..bb5cf57 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,10 @@ This is a **Godot 4.6** game project (King's Quest IV remake). The project uses: - NavigationServer2D for pathfinding - Custom script builder pattern for game scripts +## Tools + * Helper scripts (python) exist in ./tools + * a virtual env exists there, too. When you need to run one, you should source ./tools/venv/bin/activate + ## Build & Development Commands @@ -290,3 +294,4 @@ scene.start_main_script(scene.ScriptBuilder.init(scene.ScriptBuilder.walk_path(e func _on_exit_interacted() -> void: $target_scene.default_script(self) ``` + diff --git a/asset-work/kq4_062_bedroom/caption_1_3877392774_with_box.png b/asset-work/kq4_062_bedroom/caption_1_3877392774_with_box.png new file mode 100644 index 0000000..4ccd031 --- /dev/null +++ b/asset-work/kq4_062_bedroom/caption_1_3877392774_with_box.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f393ac90fd838a8d1f3ed49765fdbdfa3af9e6c206cc26f062b7fb49d33195d +size 3994503 diff --git a/asset-work/kq4_062_bedroom/caption_1_3877392774_with_box.png.import b/asset-work/kq4_062_bedroom/caption_1_3877392774_with_box.png.import new file mode 100644 index 0000000..1c45ea1 --- /dev/null +++ b/asset-work/kq4_062_bedroom/caption_1_3877392774_with_box.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://w8p5drokk4sv" +path="res://.godot/imported/caption_1_3877392774_with_box.png-49e8cf7c8aa8b4112e682ca98e672cf8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://asset-work/kq4_062_bedroom/caption_1_3877392774_with_box.png" +dest_files=["res://.godot/imported/caption_1_3877392774_with_box.png-49e8cf7c8aa8b4112e682ca98e672cf8.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 diff --git a/scenes/kq4_003_fountain_pool/.caption_1_2884713022_generated.png-autosave.kra b/scenes/kq4_003_fountain_pool/.caption_1_2884713022_generated.png-autosave.kra deleted file mode 100644 index 8b1bcba..0000000 Binary files a/scenes/kq4_003_fountain_pool/.caption_1_2884713022_generated.png-autosave.kra and /dev/null differ diff --git a/tmp/caption_1_1979017646_generated.house.mask.png b/tmp/caption_1_1979017646_generated.house.mask.png new file mode 100644 index 0000000..63eda6e --- /dev/null +++ b/tmp/caption_1_1979017646_generated.house.mask.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdafd59aec31ddb20bbccb0eb321b61286edd7812030e4d0e6e35ca94793a27d +size 6746767 diff --git a/tmp/caption_1_1979017646_generated.house.mask.png.import b/tmp/caption_1_1979017646_generated.house.mask.png.import new file mode 100644 index 0000000..dada2f9 --- /dev/null +++ b/tmp/caption_1_1979017646_generated.house.mask.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cqq5llgcudt15" +path="res://.godot/imported/caption_1_1979017646_generated.house.mask.png-405a06af69107de7cf56d2c8cd43a11a.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tmp/caption_1_1979017646_generated.house.mask.png" +dest_files=["res://.godot/imported/caption_1_1979017646_generated.house.mask.png-405a06af69107de7cf56d2c8cd43a11a.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 diff --git a/tmp/caption_1_1979017646_generated.house.png b/tmp/caption_1_1979017646_generated.house.png new file mode 100644 index 0000000..8964d0a --- /dev/null +++ b/tmp/caption_1_1979017646_generated.house.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31ba761dd794da24e02566cad98dc212d42deac084618af5e0e7f8d0ca113cda +size 4919561 diff --git a/tmp/caption_1_1979017646_generated.house.png.import b/tmp/caption_1_1979017646_generated.house.png.import new file mode 100644 index 0000000..ce8488b --- /dev/null +++ b/tmp/caption_1_1979017646_generated.house.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bjjc74ww1ynu2" +path="res://.godot/imported/caption_1_1979017646_generated.house.png-ee332416aaeec8be4ca29e4afab44dae.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tmp/caption_1_1979017646_generated.house.png" +dest_files=["res://.godot/imported/caption_1_1979017646_generated.house.png-ee332416aaeec8be4ca29e4afab44dae.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 diff --git a/tmp/caption_1_454377357_generated.door.mask.png b/tmp/caption_1_454377357_generated.door.mask.png new file mode 100644 index 0000000..4f664a1 --- /dev/null +++ b/tmp/caption_1_454377357_generated.door.mask.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:821db828c8bf860e64ec628267faa89fcb240c165f9521b62395a613cbd07348 +size 7437435 diff --git a/tmp/caption_1_454377357_generated.door.mask.png.import b/tmp/caption_1_454377357_generated.door.mask.png.import new file mode 100644 index 0000000..d1226d5 --- /dev/null +++ b/tmp/caption_1_454377357_generated.door.mask.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bh0a1gngcfhlt" +path="res://.godot/imported/caption_1_454377357_generated.door.mask.png-aec0dc7b07204882097c739e77da3d17.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tmp/caption_1_454377357_generated.door.mask.png" +dest_files=["res://.godot/imported/caption_1_454377357_generated.door.mask.png-aec0dc7b07204882097c739e77da3d17.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 diff --git a/tmp/caption_1_454377357_generated.door.png b/tmp/caption_1_454377357_generated.door.png new file mode 100644 index 0000000..45151d5 --- /dev/null +++ b/tmp/caption_1_454377357_generated.door.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a5c48e4ae0451529214a3f7a6a563a24924856264e6349ee35846a0782006b2 +size 5532384 diff --git a/tmp/caption_1_454377357_generated.door.png.import b/tmp/caption_1_454377357_generated.door.png.import new file mode 100644 index 0000000..43e9e26 --- /dev/null +++ b/tmp/caption_1_454377357_generated.door.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cgx781rh8x160" +path="res://.godot/imported/caption_1_454377357_generated.door.png-bc97e1ab49f2ba5ecd73d4604203e223.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tmp/caption_1_454377357_generated.door.png" +dest_files=["res://.godot/imported/caption_1_454377357_generated.door.png-bc97e1ab49f2ba5ecd73d4604203e223.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 diff --git a/tmp/caption_1_454377357_generated.door.v2.mask.png b/tmp/caption_1_454377357_generated.door.v2.mask.png new file mode 100644 index 0000000..6cd4fcc --- /dev/null +++ b/tmp/caption_1_454377357_generated.door.v2.mask.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddb6375c34cc7dd4e229374136a823d006f11134e4b2ca123fd0a93332585eb6 +size 7436024 diff --git a/tmp/caption_1_454377357_generated.door.v2.mask.png.import b/tmp/caption_1_454377357_generated.door.v2.mask.png.import new file mode 100644 index 0000000..ce6ede2 --- /dev/null +++ b/tmp/caption_1_454377357_generated.door.v2.mask.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b82q5606fv2ta" +path="res://.godot/imported/caption_1_454377357_generated.door.v2.mask.png-3db723937261dbc5639bd97d6f39ddb8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tmp/caption_1_454377357_generated.door.v2.mask.png" +dest_files=["res://.godot/imported/caption_1_454377357_generated.door.v2.mask.png-3db723937261dbc5639bd97d6f39ddb8.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 diff --git a/tmp/caption_1_454377357_generated.door.v2.png b/tmp/caption_1_454377357_generated.door.v2.png new file mode 100644 index 0000000..2458ee2 --- /dev/null +++ b/tmp/caption_1_454377357_generated.door.v2.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7d381aa38794a9c28275f57e8468e2803e0342e61ca0f5723da2e959d883373 +size 5528889 diff --git a/tmp/caption_1_454377357_generated.door.v2.png.import b/tmp/caption_1_454377357_generated.door.v2.png.import new file mode 100644 index 0000000..b8a1e8b --- /dev/null +++ b/tmp/caption_1_454377357_generated.door.v2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://doo1jdbpi336u" +path="res://.godot/imported/caption_1_454377357_generated.door.v2.png-2fdfe23cc08c0c7abc1681d07849c9fc.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tmp/caption_1_454377357_generated.door.v2.png" +dest_files=["res://.godot/imported/caption_1_454377357_generated.door.v2.png-2fdfe23cc08c0c7abc1681d07849c9fc.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 diff --git a/tmp/stone_box.png b/tmp/stone_box.png new file mode 100644 index 0000000..542ba1c --- /dev/null +++ b/tmp/stone_box.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d5eab55ffac9b45ea740f679f3a891aaca8f0eae3b71df9e62901a191c7eb03 +size 5533266 diff --git a/tmp/stone_box.png.import b/tmp/stone_box.png.import new file mode 100644 index 0000000..71682e1 --- /dev/null +++ b/tmp/stone_box.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://tc8kp6g4d6v3" +path="res://.godot/imported/stone_box.png-23873de51cd956b464cbf5c40e614970.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tmp/stone_box.png" +dest_files=["res://.godot/imported/stone_box.png-23873de51cd956b464cbf5c40e614970.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 diff --git a/tmp/stone_mask.png b/tmp/stone_mask.png new file mode 100644 index 0000000..6189196 --- /dev/null +++ b/tmp/stone_mask.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e477949dd169a965e95b2d5d333e975bb977d64b52712e23a0cbacff3288136 +size 7442131 diff --git a/tmp/stone_mask.png.import b/tmp/stone_mask.png.import new file mode 100644 index 0000000..47dcf38 --- /dev/null +++ b/tmp/stone_mask.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d1h745821w85b" +path="res://.godot/imported/stone_mask.png-2f0bfa74533a45031cbfc6d9f501b5d2.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tmp/stone_mask.png" +dest_files=["res://.godot/imported/stone_mask.png-2f0bfa74533a45031cbfc6d9f501b5d2.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 diff --git a/tmp/stone_outline.png b/tmp/stone_outline.png new file mode 100644 index 0000000..327c19b --- /dev/null +++ b/tmp/stone_outline.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76b7ab7a59200aaff5c98b195666a3d54f68353c8f2343fa080b6acd2bea6cea +size 5537082 diff --git a/tmp/stone_outline.png.import b/tmp/stone_outline.png.import new file mode 100644 index 0000000..3ffcd8a --- /dev/null +++ b/tmp/stone_outline.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdo0hg0am6cus" +path="res://.godot/imported/stone_outline.png-33fd18a74617b08b2436a4a9d98a33b6.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tmp/stone_outline.png" +dest_files=["res://.godot/imported/stone_outline.png-33fd18a74617b08b2436a4a9d98a33b6.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 diff --git a/tools/draw_polygon.py b/tools/draw_polygon.py new file mode 100644 index 0000000..e9b1929 --- /dev/null +++ b/tools/draw_polygon.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Draw a polygon on an image using Pillow.""" + +import argparse +import sys +from pathlib import Path + +try: + from PIL import Image, ImageDraw, ImageFilter +except ImportError: + print("Error: Pillow is required. Install with: pip install Pillow") + sys.exit(1) + + +COLOR_PALETTE = { + "red": "#FF0000", + "green": "#00FF00", + "blue": "#0000FF", + "yellow": "#FFFF00", + "cyan": "#00FFFF", + "magenta": "#FF00FF", + "white": "#FFFFFF", + "black": "#000000", + "orange": "#FFA500", + "purple": "#800080", + "brown": "#A52A2A", + "pink": "#FFC0CB", + "gray": "#808080", + "grey": "#808080", + "lime": "#00FF00", + "navy": "#000080", + "teal": "#008080", + "maroon": "#800000", +} + + +def parse_color(color_str: str) -> str: + """Parse color string to hex.""" + color_lower = color_str.lower() + if color_lower in COLOR_PALETTE: + return COLOR_PALETTE[color_lower] + if color_str.startswith("#") and len(color_str) in [7, 9]: + return color_str + raise ValueError( + f"Invalid color '{color_str}'. Use a name (e.g., red, blue) or hex code (e.g., #FF0000)." + ) + + +def parse_points(points_str: str) -> list[tuple[float, float]]: + """Parse points string into list of (x, y) tuples.""" + points = [] + for point in points_str.split(): + if "," not in point: + raise ValueError(f"Invalid point format: '{point}'. Use 'x,y' format.") + try: + x, y = point.split(",") + points.append((float(x.strip()), float(y.strip()))) + except ValueError: + raise ValueError(f"Invalid point format: '{point}'. Use 'x,y' format.") + return points + + +def convert_to_pixels( + points: list[tuple[float, float]], width: int, height: int, absolute: bool +) -> list[tuple[int, int]]: + """Convert points to pixel coordinates.""" + if absolute: + return [(int(x), int(y)) for x, y in points] + else: + return [ + (int(x * width), int(y * height)) + for x, y in points + if 0 <= x <= 1 and 0 <= y <= 1 + ] + + +def draw_polygon_on_image( + image_path: Path, + points: list[tuple[float, float]], + color: str, + thickness: int, + fill: bool, + absolute: bool, + save_path: Path | None, +) -> None: + """Draw polygon on image.""" + image = Image.open(image_path).convert("RGBA") + width, height = image.size + + pixel_points = convert_to_pixels(points, width, height, absolute) + + if len(pixel_points) < 3: + raise ValueError("Need at least 3 points to draw a polygon.") + + draw = ImageDraw.Draw(image) + + hex_color = parse_color(color) + draw.polygon(pixel_points, fill=hex_color if fill else None, outline=hex_color, width=thickness) + + if fill: + alpha = 128 + fill_color = hex_color[:7] + f"{alpha:02X}" + draw.polygon(pixel_points, fill=fill_color, outline=hex_color, width=thickness) + + if save_path: + image.save(save_path) + print(f"Saved: {save_path}") + else: + image.show() + + +def main(): + parser = argparse.ArgumentParser( + description="Draw a polygon on an image using Pillow.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Draw a triangle using percentages (default) + python draw_polygon.py scene.png "0.5,0.1 0.9,0.9 0.1,0.9" + + # Draw with pixel coordinates (absolute mode) + python draw_polygon.py scene.png "100,50 400,50 250,300" --absolute + + # Save to file with blue color + python draw_polygon.py scene.png "0.2,0.2 0.8,0.2 0.8,0.8 0.2,0.8" --color blue --save output.png + + # Fill polygon semi-transparently + python draw_polygon.py scene.png "0.3,0.3 0.7,0.3 0.7,0.7 0.3,0.7" --fill --color red + + # Draw with custom hex color and thickness + python draw_polygon.py scene.png "0.1,0.1 0.9,0.1 0.9,0.9 0.1,0.9" --color #00FF00 --thickness 5 + +Coordinate Formats: + Percentage (default): 0.0 to 1.0, where 0.0,0.0 is top-left and 1.0,1.0 is bottom-right + Absolute (with --absolute): Actual pixel coordinates from 0 to image width/height + +Color Formats: + Named colors: red, green, blue, yellow, cyan, magenta, white, black, orange, purple, brown, pink, gray, grey, lime, navy, teal, maroon + Hex codes: #FF0000, #00FF00FF, etc. + """, + ) + + parser.add_argument( + "image", + type=Path, + help="Path to input image file", + ) + parser.add_argument( + "points", + type=str, + help="Space-separated list of x,y coordinates (e.g., '0.1,0.1 0.9,0.1 0.5,0.9'). Use percentages (0.0-1.0) by default, or --absolute for pixel coordinates.", + ) + parser.add_argument( + "--color", + type=str, + default="red", + help="Polygon color (default: red). Use named colors (red, blue, green, etc.) or hex codes (#FF0000).", + ) + parser.add_argument( + "--absolute", + action="store_true", + help="Use pixel coordinates instead of percentages", + ) + parser.add_argument( + "--save", + type=Path, + default=None, + help="Save output to file path instead of displaying", + ) + parser.add_argument( + "--thickness", + type=int, + default=2, + help="Line thickness in pixels (default: 2)", + ) + parser.add_argument( + "--fill", + action="store_true", + help="Fill polygon with semi-transparent color", + ) + + args = parser.parse_args() + + if not args.image.exists(): + print(f"Error: Image not found: {args.image}", file=sys.stderr) + sys.exit(1) + + try: + points = parse_points(args.points) + if len(points) < 3: + print(f"Error: Need at least 3 points. Got {len(points)}.", file=sys.stderr) + sys.exit(1) + + draw_polygon_on_image( + image_path=args.image, + points=points, + color=args.color, + thickness=args.thickness, + fill=args.fill, + absolute=args.absolute, + save_path=args.save, + ) + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tools/extract_mask.py b/tools/extract_mask.py index b8e43f8..4ccbb9e 100755 --- a/tools/extract_mask.py +++ b/tools/extract_mask.py @@ -125,7 +125,7 @@ def extract_mask( with open(workflow_path, "r") as f: workflow = json.load(f) - prompt_text = f"Create a black and white alpha mask of {subject}" + prompt_text = f"Create a black and white alpha mask of {subject}, leaving everything else black" print(f"Encoding input image...") base64_image = encode_image_base64(input_image) diff --git a/tools/make_ora.py b/tools/make_ora.py new file mode 100644 index 0000000..33c5aed --- /dev/null +++ b/tools/make_ora.py @@ -0,0 +1,154 @@ +import argparse +import os +import zipfile +import tempfile +import shutil +import numpy as np +from PIL import Image +import xml.etree.ElementTree as ET + +NUM_LAYERS = 4 +MASKS_PER_LAYER = 3 +NOISE_SCALES = [4, 16, 64] + + +def noise_mask(w, h, scale): + sw = max(1, w // scale) + sh = max(1, h // scale) + + noise = np.random.rand(sh, sw) + img = Image.fromarray((noise * 255).astype(np.uint8), "L") + img = img.resize((w, h), Image.BILINEAR) + + return img + + +def build_stack_xml(w, h, groups): + image = ET.Element( + "image", + {"version": "0.0.3", "w": str(w), "h": str(h)} + ) + + root_stack = ET.SubElement(image, "stack") + + for group in groups: + + group_el = ET.SubElement( + root_stack, + "stack", + {"name": group["name"]} + ) + + for layer in group["layers"]: + ET.SubElement( + group_el, + "layer", + { + "name": layer["name"], + "src": layer["src"], + "opacity": "1.0" + } + ) + + return ET.tostring(image, encoding="utf-8", xml_declaration=True) + + +def build_ora(input_png, output_ora): + + base = Image.open(input_png).convert("RGBA") + w, h = base.size + + tmp = tempfile.mkdtemp() + + try: + + data_dir = os.path.join(tmp, "data") + thumb_dir = os.path.join(tmp, "Thumbnails") + + os.makedirs(data_dir) + os.makedirs(thumb_dir) + + groups = [] + + for i in range(NUM_LAYERS): + + group_layers = [] + + base_path = f"data/layer_{i}.png" + base.save(os.path.join(tmp, base_path)) + + group_layers.append({ + "name": "base", + "src": base_path + }) + + for j in range(MASKS_PER_LAYER): + + mask = noise_mask(w, h, NOISE_SCALES[j]) + + mask_path = f"data/layer_{i}_mask_{j}.png" + mask.save(os.path.join(tmp, mask_path)) + + group_layers.append({ + "name": f"mask {j}", + "src": mask_path + }) + + groups.append({ + "name": f"group {i}", + "layers": group_layers + }) + + stack_xml = build_stack_xml(w, h, groups) + + with open(os.path.join(tmp, "stack.xml"), "wb") as f: + f.write(stack_xml) + + with open(os.path.join(tmp, "mimetype"), "w") as f: + f.write("image/openraster") + + base.save(os.path.join(tmp, "mergedimage.png")) + + thumb = base.copy() + thumb.thumbnail((256, 256)) + thumb.save(os.path.join(thumb_dir, "thumbnail.png")) + + with zipfile.ZipFile(output_ora, "w") as z: + + z.write( + os.path.join(tmp, "mimetype"), + "mimetype", + compress_type=zipfile.ZIP_STORED + ) + + for root, _, files in os.walk(tmp): + for file in files: + + if file == "mimetype": + continue + + full = os.path.join(root, file) + rel = os.path.relpath(full, tmp) + + z.write(full, rel) + + finally: + shutil.rmtree(tmp) + + +def main(): + + parser = argparse.ArgumentParser( + description="Generate ORA with grouped layers and mask layers" + ) + + parser.add_argument("input_png") + parser.add_argument("output_ora") + + args = parser.parse_args() + + build_ora(args.input_png, args.output_ora) + + +if __name__ == "__main__": + main() diff --git a/tools/requirements.txt b/tools/requirements.txt index 09edf7d..fd87dcc 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,3 +1,4 @@ opencv-python numpy shapely +Pillow>=10.0.0