Skip to content
Open
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
13 changes: 11 additions & 2 deletions Caddyfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
(routes) {
handle_path /vectorize* {
reverse_proxy vectorization:80
}
handle {
reverse_proxy backend:80
}
}

doodlebot.media.mit.edu {
reverse_proxy backend:80
import routes
}

http://doodlebot.media.mit.edu {
reverse_proxy backend:80
import routes
}
2 changes: 1 addition & 1 deletion cli/prod.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ COMPOSE_FILE="$DOCKER_DIR/compose.yml"

docker compose -f "$COMPOSE_FILE" up --build frontend playground

docker compose -f "$COMPOSE_FILE" up --build --detach backend caddy
docker compose -f "$COMPOSE_FILE" up --build --detach vectorization backend caddy
12 changes: 9 additions & 3 deletions docker/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ services:
dockerfile: ./playground.Dockerfile
restart: "no"

vectorization:
container_name: doodlebot-vectorization
build:
context: ..
dockerfile: ./vectorization/Dockerfile
networks:
- app-network

backend:
container_name: doodlebot-backend
build:
context: ..
dockerfile: ./docker/backend.Dockerfile
ports:
- "8000:80"
volumes:
- "./.frontend:/app/frontend"
- "./.playground:/app/frontend/playground"
Expand Down Expand Up @@ -51,4 +57,4 @@ networks:

volumes:
caddy_data:
caddy_config:
caddy_config:
11 changes: 11 additions & 0 deletions vectorization/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM python:3.12

WORKDIR /app

COPY ./vectorization/requirements.txt /app/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

COPY ./vectorization /app

CMD ["uvicorn", "app:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
97 changes: 97 additions & 0 deletions vectorization/Dockerfile.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Tests
**/__tests__/
conftest.py

# Dev / database migrations
**/alembic*

# Git
.git
.gitignore
.gitattributes


# CI
.codeclimate.yml
.travis.yml
.taskcluster.yml

# Docker
docker-compose.yml
Dockerfile
*Dockerfile*
.docker
.dockerignore

# Byte-compiled / optimized / DLL files
**/__pycache__/
**/*.py[cod]

# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml

# Translations
*.mo
*.pot

# Django stuff:
*.log

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Virtual environment
.env
.venv/
venv/

# PyCharm
.idea

# Python mode for VIM
.ropeproject
**/.ropeproject

# Vim swap files
**/*.swp

# VS Code
.vscode/
105 changes: 105 additions & 0 deletions vectorization/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from arc_line_vectorization_suede import default_pipeline, DrawingCommand
from arc_line_vectorization_suede.visualize import commands_to_svg_compare

from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.middleware.cors import CORSMiddleware

import numpy as np
from PIL import Image

import asyncio
from functools import wraps
import io
from typing import Sequence

app = FastAPI(
title="Vectorization API",
description="An API that converts images into vectorized drawing commands",
version="1.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # Allows all origins
# "http://localhost:8602", "http://doodlebot.media.mit.edu"
allow_credentials=True,
allow_methods=["GET", "POST"], # Allows all methods
allow_headers=["*"], # Allows all headers
)


def _commands_to_jsonable(commands: Sequence[DrawingCommand]):
"""Strip numpy scalar wrappers so DrawingCommand dicts serialize cleanly."""
out = []
for cmd in commands:
item = {}
for key, value in cmd.items():
if isinstance(value, np.bool_):
item[key] = bool(value)
elif isinstance(value, np.integer):
item[key] = int(value)
elif isinstance(value, np.floating):
item[key] = float(value)
else:
item[key] = value
out.append(item)
return out


def run_vectorization(image_array: np.ndarray):
_, _, _, _, _, low_geometry_optimized, high_geometry_optimized = default_pipeline(
image_array
)
svg = commands_to_svg_compare(
low_geometry_optimized.commands,
high_geometry_optimized.commands,
label_a="low_geometry_optimized.commands",
label_b="high_geometry_optimized.commands",
)
return {
"low_geometry": _commands_to_jsonable(low_geometry_optimized.commands),
"high_geometry": _commands_to_jsonable(high_geometry_optimized.commands),
"svg": svg,
}


class VectorizationError(Exception):
"""Custom exception for vectorization errors"""

pass


def handle_errors(func):
"""Decorator for error handling"""

@wraps(func)
async def wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except VectorizationError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Internal server error: {str(e)}"
)

return wrapper


@app.post("/")
@handle_errors
async def vectorize_endpoint(image_file: UploadFile = File(...)):
"""Vectorize an uploaded image into robot drawing commands.

Returns the low-geometry consolidated commands, the high-geometry
commands, and a side-by-side comparison SVG of the two.
"""
image_bytes = await image_file.read()
if not image_bytes:
raise VectorizationError("Empty image upload")
try:
pil_image = Image.open(io.BytesIO(image_bytes))
pil_image.load()
except Exception as e:
raise VectorizationError(f"Could not decode image: {e}")
image_array = np.asarray(pil_image)
return await asyncio.to_thread(run_vectorization, image_array)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
networkx==3.6.1
numpy==2.4.4
pillow==12.2.0
scikit-image==0.26.0
scipy==1.17.1
Loading