cutout-studio / app.py
akswaroop
fix: load only one model at a time to fix OOM crash
7fccb80
Raw
History Blame Contribute Delete
6.27 kB
import os
import sys
import threading
import warnings
import io as _io
warnings.filterwarnings("ignore")
os.environ["CUDA_VISIBLE_DEVICES"] = ""
os.environ["OMP_NUM_THREADS"] = "2"
os.environ["ORT_LOGGING_LEVEL"] = "3"
# Suppress GPU stderr on import
_old_stderr = sys.stderr
sys.stderr = _io.StringIO()
try:
import onnxruntime as ort
ort.set_default_logger_severity(3)
finally:
sys.stderr = _old_stderr
from flask import Flask, request, render_template, send_file, jsonify
from flask_cors import CORS
from rembg import remove, new_session
from PIL import Image, ImageFilter
import io
import time
app = Flask(__name__)
CORS(app)
# Only ONE model loaded at a time to stay within 512MB RAM
MODELS = {
"general": "u2net",
"portrait": "u2net_human_seg",
"anime": "isnet-anime",
"product": "silueta", # smallest at 43MB
}
SESS_OPTS = ort.SessionOptions()
SESS_OPTS.inter_op_num_threads = 1
SESS_OPTS.intra_op_num_threads = 1
SESS_OPTS.log_severity_level = 3
CPU_PROVIDERS = ["CPUExecutionProvider"]
# Only keep ONE session at a time to save memory
_current_session = None
_current_model = None
_lock = threading.Lock()
def get_session(model_name):
global _current_session, _current_model
if _current_model == model_name and _current_session is not None:
return _current_session
with _lock:
if _current_model != model_name:
print(f"[CutOut] Switching model: {_current_model}{model_name}", flush=True)
# Free old session from memory
_current_session = None
_current_model = None
import gc
gc.collect()
try:
_current_session = new_session(
model_name,
sess_options=SESS_OPTS,
providers=CPU_PROVIDERS,
)
_current_model = model_name
print(f"[CutOut] Ready: {model_name}", flush=True)
except Exception as e:
print(f"[CutOut] Failed {model_name}: {e}", flush=True)
raise e
return _current_session
def preload_default():
"""Only preload the general model at startup — 176MB fits fine."""
time.sleep(2)
try:
get_session("u2net")
print("[CutOut] Default model ready!", flush=True)
except Exception as e:
print(f"[CutOut] Preload failed: {e}", flush=True)
threading.Thread(target=preload_default, daemon=True).start()
@app.route("/")
def index():
return render_template("index.html")
@app.route("/ping")
def ping():
return "pong", 200
@app.route("/health")
def health():
return jsonify({
"status": "ok",
"models": list(MODELS.keys()),
"loaded": _current_model,
"version": "2.6",
})
@app.route("/remove-background", methods=["POST"])
def remove_background():
start = time.time()
if "image" not in request.files:
return jsonify({"error": "No file uploaded."}), 400
file = request.files["image"]
if not file.filename:
return jsonify({"error": "No file selected."}), 400
model_key = request.form.get("model", "general")
bg_color = request.form.get("bg_color", "").strip()
feather = int(request.form.get("feather", 0))
shadow = request.form.get("shadow", "false") == "true"
shadow_blur = int(request.form.get("shadow_blur", 12))
shadow_opac = int(request.form.get("shadow_opacity", 60))
out_format = request.form.get("format", "png").lower()
scale = float(request.form.get("scale", 1.0))
model_name = MODELS.get(model_key, "u2net")
try:
input_bytes = file.read()
session = get_session(model_name)
output_bytes = remove(input_bytes, session=session)
except Exception as e:
print(f"[CutOut] Error: {e}", flush=True)
return jsonify({"error": str(e)}), 500
img = Image.open(io.BytesIO(output_bytes)).convert("RGBA")
if feather > 0:
r, g, b, a = img.split()
a = a.filter(ImageFilter.GaussianBlur(radius=feather))
img = Image.merge("RGBA", (r, g, b, a))
if scale != 1.0:
img = img.resize(
(int(img.width * scale), int(img.height * scale)),
Image.LANCZOS
)
if bg_color or shadow:
canvas = Image.new("RGBA", img.size, (0, 0, 0, 0))
if shadow:
_, _, _, mask = img.split()
sh = Image.new("RGBA", img.size, (0, 0, 0, int(shadow_opac * 2.55)))
sh.putalpha(mask)
sh = sh.filter(ImageFilter.GaussianBlur(radius=shadow_blur))
sc = Image.new("RGBA", img.size, (0, 0, 0, 0))
sc.paste(sh, (6, 8))
canvas = Image.alpha_composite(canvas, sc)
if bg_color.startswith("#") and len(bg_color) >= 7:
try:
h = bg_color.lstrip("#")
bl = Image.new("RGBA", img.size, (
int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), 255
))
canvas = Image.alpha_composite(bl, canvas)
except ValueError:
pass
img = Image.alpha_composite(canvas, img)
buf = io.BytesIO()
elapsed = round((time.time() - start) * 1000)
if out_format == "jpg":
flat = Image.new("RGB", img.size, (255, 255, 255))
flat.paste(img, mask=img.split()[3])
flat.save(buf, "JPEG", quality=95)
mime, fname = "image/jpeg", "cutout.jpg"
elif out_format == "webp":
img.save(buf, "WEBP", quality=95)
mime, fname = "image/webp", "cutout.webp"
else:
img.save(buf, "PNG")
mime, fname = "image/png", "cutout.png"
buf.seek(0)
print(f"[CutOut] {elapsed}ms {model_name} {img.width}x{img.height}", flush=True)
resp = send_file(buf, mimetype=mime, as_attachment=False, download_name=fname)
resp.headers["X-Processing-Time"] = str(elapsed)
resp.headers["X-Image-Width"] = str(img.width)
resp.headers["X-Image-Height"] = str(img.height)
return resp
if __name__ == "__main__":
port = int(os.environ.get("PORT", 5000))
print(f"[CutOut] Starting on port {port}", flush=True)
app.run(host="0.0.0.0", port=port, debug=False)