Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,342 changes: 119 additions & 1,223 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "propfix",
"version": "0.2.0",
"private": true,
"main": "src/main.cjs",
"main": "src/main.js",
"description": "PropFix – Electron UI (sliders) talking to the local PropFix server",
"scripts": {
"start": "electron ."
Expand Down
216 changes: 125 additions & 91 deletions python/image_processing.py
Original file line number Diff line number Diff line change
@@ -1,104 +1,138 @@
"""Simple Python back‑end for PropFix.

This script reads a JSON command from standard input, applies an image
processing operation and writes the result path or base64 string to
standard output. It is intentionally minimal to demonstrate
communication between Electron/Node and Python using the `python‑shell`
package【362342226299824†L68-L82】.

Supported commands:

* ``enhance`` – automatically adjust contrast using histogram equalisation.
* ``denoise`` – apply a median filter to remove noise.
* ``style`` – placeholder for style transfer. Currently returns the
original image.

The script can be extended to support additional operations by adding
handlers to the ``COMMANDS`` dictionary.
"""
import json
import sys
import sys, os, json
from pathlib import Path
from typing import Callable

import cv2
from typing import Dict, Any, List
from PIL import Image, ImageDraw, ImageFont
import numpy as np
from PIL import Image


def enhance_image(img: np.ndarray) -> np.ndarray:
"""Enhance image contrast using histogram equalisation."""
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
v_eq = cv2.equalizeHist(v)
hsv_eq = cv2.merge((h, s, v_eq))
enhanced = cv2.cvtColor(hsv_eq, cv2.COLOR_HSV2BGR)
return enhanced
# Simple helpers --------------------------------------------------------------
def abspath(p: str) -> str:
return str(Path(p).expanduser().resolve())

def ensure_dir(p: Path):
p.mkdir(parents=True, exist_ok=True)

def save_image(img: Image.Image, out_path: Path) -> str:
ensure_dir(out_path.parent)
img.save(out_path)
return str(out_path.resolve())

def load_image(path_str: str) -> Image.Image:
return Image.open(path_str).convert("RGB")

# "Segmentation" stub (returns plausible parts) -------------------------------
DEFAULT_SEGMENTS = [
"torso", "head", "l_arm", "r_arm", "l_forearm", "r_forearm",
"l_thigh", "r_thigh", "l_calf", "r_calf", "l_hand", "r_hand",
"l_foot", "r_foot"
]

def annotate(img: Image.Image, segments: List[str]) -> Image.Image:
w, h = img.size
draw = ImageDraw.Draw(img, "RGBA")
# draw light grid rectangles where labels go
n = max(4, int(np.sqrt(len(segments))))
cell_w, cell_h = w // n, h // n
idx = 0
for r in range(n):
for c in range(n):
if idx >= len(segments):
break
x0, y0 = c * cell_w + 4, r * cell_h + 4
x1, y1 = (c + 1) * cell_w - 4, (r + 1) * cell_h - 4
draw.rectangle([x0, y0, x1, y1], outline=(79,142,247,180), width=2)
label = segments[idx].replace("_", " ")
draw.text((x0+6, y0+6), label, fill=(230,232,238,220))
idx += 1
return img

# Simple "warp" (global transform) -------------------------------------------
def warp_image(img: Image.Image, controls: Dict[str, Any]) -> Image.Image:
# We approximate part controls with one global scale + translate + rotate
geo = (controls or {}).get("geometry", {})
# Derive average scale/offset
sx_vals, sy_vals, tx_vals, ty_vals, rot_vals = [], [], [], [], []
for part, g in geo.items():
sx_vals.append(float(g.get("sx", 1.0)))
sy_vals.append(float(g.get("sy", 1.0)))
tx_vals.append(float(g.get("tx", 0.0)))
ty_vals.append(float(g.get("ty", 0.0)))
rot_vals.append(float(g.get("rot_deg", 0.0)))
def avg(vs, default):
return (sum(vs) / len(vs)) if vs else default
sx = max(0.6, min(1.4, avg(sx_vals, 1.0)))
sy = max(0.6, min(1.4, avg(sy_vals, 1.0)))
tx = avg(tx_vals, 0.0)
ty = avg(ty_vals, 0.0)
rot = avg(rot_vals, 0.0)

# Apply rotate then scale then translate
w, h = img.size
img2 = img.rotate(-rot, resample=Image.BICUBIC, expand=False, center=(w/2, h/2))
new_w, new_h = int(w * sx), int(h * sy)
img2 = img2.resize((new_w, new_h), resample=Image.BICUBIC)

# Paste onto a canvas of original size, centered with offset
canvas = Image.new("RGB", (w, h), (0,0,0))
off_x = int((w - new_w)//2 + tx)
off_y = int((h - new_h)//2 + ty)
canvas.paste(img2, (off_x, off_y))
return canvas

# Main entry ------------------------------------------------------------------
def main(argv: List[str]):
# Accept JSON payload via argv[1]
payload: Dict[str, Any] = {}
if len(argv) >= 2:
try:
payload = json.loads(argv[1])
except Exception:
payload = {}

mode = str(payload.get("mode", "classify")).lower()
input_path = payload.get("input") or ""
output_path = payload.get("output") # may be None; we'll decide
if not input_path:
print(json.dumps({"ok": False, "error": "Missing input path"}))
return

def denoise_image(img: np.ndarray) -> np.ndarray:
"""Reduce noise using a median filter."""
return cv2.medianBlur(img, 3)
in_abs = abspath(input_path)
try:
img = load_image(in_abs)
except Exception as e:
print(json.dumps({"ok": False, "error": f"Failed to open image: {e}"}))
return

root = Path(__file__).resolve().parent.parent
outputs = root / "outputs"
ensure_dir(outputs)

def style_transfer_image(img: np.ndarray) -> np.ndarray:
"""Placeholder for style transfer – returns the original image."""
return img
if mode == "classify":
segs = DEFAULT_SEGMENTS.copy()
annotated = annotate(img.copy(), segs)
out = Path(output_path) if output_path else outputs / (Path(in_abs).stem + "_annotated.png")
out_abs = save_image(annotated, out)
print(json.dumps({"ok": True, "mode": mode, "segments": segs, "used_segments": segs, "output": out_abs}))
return

elif mode == "warp":
controls = payload.get("controls", {})
warped = warp_image(img.copy(), controls)
out = Path(output_path) if output_path else outputs / (Path(in_abs).stem + "_warped.png")
out_abs = save_image(warped, out)
print(json.dumps({"ok": True, "mode": mode, "output": out_abs}))
return

COMMANDS: dict[str, Callable[[np.ndarray], np.ndarray]] = {
"enhance": enhance_image,
"denoise": denoise_image,
"style": style_transfer_image,
}


def process_request(request: dict) -> str:
"""Process a JSON command and return a path to the processed image.

Parameters
----------
request : dict
Dictionary with keys ``command``, ``image_path`` and optional
``params``.

Returns
-------
str
Path to the processed image. The image is saved in the same
directory as the input with suffix ``_propfix``.
"""
cmd = request.get("command")
img_path = Path(request.get("image_path"))
if cmd not in COMMANDS:
raise ValueError(f"Unsupported command: {cmd}")
# Load image using OpenCV (BGR format)
img = cv2.imread(str(img_path))
if img is None:
raise FileNotFoundError(f"Cannot read image: {img_path}")
processed = COMMANDS[cmd](img)
# Save result
out_path = img_path.with_name(img_path.stem + "_propfix" + img_path.suffix)
cv2.imwrite(str(out_path), processed)
return str(out_path)


def main() -> None:
# Read entire message from stdin (python‑shell sends a JSON string)
data = sys.stdin.read().strip()
if not data:
elif mode in ("enhance", "denoise", "style"):
# Placeholder: just save copy with a suffix
suffix = {"enhance": "_enhanced", "denoise": "_denoised", "style": "_styled"}[mode]
out = Path(output_path) if output_path else outputs / (Path(in_abs).stem + f"{suffix}.png")
out_abs = save_image(img.copy(), out)
print(json.dumps({"ok": True, "mode": mode, "output": out_abs}))
return
try:
request = json.loads(data)
output_path = process_request(request)
# Print the path back to Node/Electron
print(output_path)
except Exception as exc:
# In case of error, print error message so Node can handle it
print(f"error: {exc}")
finally:
sys.stdout.flush()

else:
print(json.dumps({"ok": False, "error": f"Unknown mode: {mode}"}))
return

if __name__ == "__main__":
main()
main(sys.argv)
79 changes: 0 additions & 79 deletions python/model.py

This file was deleted.

Empty file added src/__init__.py
Empty file.
Loading