Commit
·
7fa5d1a
1
Parent(s):
c5411c1
Only log to admin media_clicks if user_id is provided; regular MongoDB logging always happens
Browse files- api/main.py +292 -203
api/main.py
CHANGED
|
@@ -158,8 +158,12 @@ def _coerce_category_id(category_id: Optional[str]) -> ObjectId:
|
|
| 158 |
|
| 159 |
|
| 160 |
def log_media_click(user_id: Optional[str], category_id: Optional[str]) -> None:
|
|
|
|
| 161 |
if admin_media_clicks is None:
|
| 162 |
return
|
|
|
|
|
|
|
|
|
|
| 163 |
try:
|
| 164 |
user_obj = _coerce_object_id(user_id)
|
| 165 |
category_obj = _coerce_category_id(category_id)
|
|
@@ -442,27 +446,56 @@ def inpaint(req: InpaintRequest, _: None = Depends(bearer_auth)) -> Dict[str, st
|
|
| 442 |
@app.post("/inpaint-url")
|
| 443 |
def inpaint_url(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
|
| 444 |
"""Same as /inpaint but returns a JSON with a public download URL instead of image bytes."""
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
|
|
|
|
|
|
| 453 |
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
result = process_inpaint(np.array(img_rgba), mask_rgba, invert_mask=req.invert_mask)
|
| 458 |
-
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 459 |
-
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 460 |
-
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
| 461 |
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
|
| 467 |
|
| 468 |
@app.post("/inpaint-multipart")
|
|
@@ -477,13 +510,115 @@ def inpaint_multipart(
|
|
| 477 |
category_id: Optional[str] = Form(None),
|
| 478 |
_: None = Depends(bearer_auth),
|
| 479 |
) -> Dict[str, str]:
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
|
| 484 |
-
|
| 485 |
-
#
|
| 486 |
-
|
|
|
|
|
|
|
| 487 |
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 488 |
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 489 |
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
|
@@ -504,216 +639,170 @@ def inpaint_multipart(
|
|
| 504 |
resp["url"] = url
|
| 505 |
log_media_click(user_id, category_id)
|
| 506 |
return resp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
|
| 513 |
-
|
|
|
|
|
|
|
| 514 |
|
| 515 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
lower = np.array([150, 0, 100], dtype=np.uint8)
|
| 517 |
upper = np.array([255, 120, 255], dtype=np.uint8)
|
| 518 |
-
|
| 519 |
-
(
|
| 520 |
-
(
|
| 521 |
-
(
|
| 522 |
).astype(np.uint8) * 255
|
| 523 |
|
| 524 |
-
#
|
| 525 |
-
if img is not None:
|
| 526 |
-
img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
|
| 527 |
-
if img_rgb.shape == m_rgb.shape:
|
| 528 |
-
diff = cv2.absdiff(img_rgb, m_rgb)
|
| 529 |
-
gray_diff = cv2.cvtColor(diff, cv2.COLOR_RGB2GRAY)
|
| 530 |
-
# Any significant difference (>50) could be paint
|
| 531 |
-
diff_mask = (gray_diff > 50).astype(np.uint8) * 255
|
| 532 |
-
# Combine with magenta detection
|
| 533 |
-
binmask = cv2.bitwise_or(magenta_detected, diff_mask)
|
| 534 |
-
else:
|
| 535 |
-
binmask = magenta_detected
|
| 536 |
-
else:
|
| 537 |
-
# No original image provided, use magenta detection only
|
| 538 |
-
binmask = magenta_detected
|
| 539 |
-
|
| 540 |
-
# Clean up the mask: remove noise and fill small holes
|
| 541 |
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
| 542 |
-
# Close small gaps in the mask
|
| 543 |
binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
|
| 544 |
-
# Remove small noise
|
| 545 |
binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
|
| 546 |
|
| 547 |
nonzero = int((binmask > 0).sum())
|
| 548 |
-
|
|
|
|
| 549 |
|
| 550 |
-
#
|
| 551 |
-
|
| 552 |
|
| 553 |
if nonzero < 50:
|
| 554 |
-
log.error("
|
| 555 |
-
result = np.array(img.convert("RGB"))
|
| 556 |
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 557 |
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 558 |
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
| 559 |
-
return {
|
|
|
|
|
|
|
|
|
|
| 560 |
|
| 561 |
# Create binary mask: Pink pixels → white (255), Everything else → black (0)
|
| 562 |
-
# Encode in RGBA format
|
| 563 |
# process_inpaint does: mask = 255 - mask[:,:,3]
|
| 564 |
# So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
|
| 565 |
# alpha=255 (opaque/keep) → becomes 0 (black/keep)
|
| 566 |
mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
|
| 567 |
-
|
| 568 |
-
mask_rgba[:, :,
|
|
|
|
| 569 |
mask_rgba[:, :, 2] = binmask # B: white where pink
|
| 570 |
-
# Alpha:
|
| 571 |
-
|
|
|
|
| 572 |
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
url = None
|
| 593 |
-
|
| 594 |
-
entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
|
| 595 |
-
if url:
|
| 596 |
-
entry["url"] = url
|
| 597 |
-
logs.append(entry)
|
| 598 |
-
resp: Dict[str, str] = {"result": result_name}
|
| 599 |
-
if url:
|
| 600 |
-
resp["url"] = url
|
| 601 |
-
log_media_click(user_id, category_id)
|
| 602 |
-
return resp
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
@app.post("/remove-pink")
|
| 606 |
-
def remove_pink_segments(
|
| 607 |
-
image: UploadFile = File(...),
|
| 608 |
-
request: Request = None,
|
| 609 |
-
user_id: Optional[str] = Form(None),
|
| 610 |
-
category_id: Optional[str] = Form(None),
|
| 611 |
-
_: None = Depends(bearer_auth),
|
| 612 |
-
) -> Dict[str, str]:
|
| 613 |
-
"""
|
| 614 |
-
Simple endpoint: upload an image with pink/magenta segments to remove.
|
| 615 |
-
- Pink/Magenta segments → automatically removed (white in mask)
|
| 616 |
-
- Everything else → automatically kept (black in mask)
|
| 617 |
-
Just paint pink/magenta on areas you want to remove, upload the image, and it works!
|
| 618 |
-
"""
|
| 619 |
-
log.info(f"Simple remove-pink: processing image {image.filename}")
|
| 620 |
-
|
| 621 |
-
# Load the image (with pink paint on it)
|
| 622 |
-
img = Image.open(image.file).convert("RGBA")
|
| 623 |
-
img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
|
| 624 |
-
|
| 625 |
-
# Auto-detect pink/magenta segments to remove
|
| 626 |
-
# Pink/Magenta → white in mask (remove)
|
| 627 |
-
# Everything else (natural image colors, including dark areas) → black in mask (keep)
|
| 628 |
-
|
| 629 |
-
# Detect pink/magenta using fixed RGB bounds per requested logic
|
| 630 |
-
lower = np.array([150, 0, 100], dtype=np.uint8)
|
| 631 |
-
upper = np.array([255, 120, 255], dtype=np.uint8)
|
| 632 |
-
binmask = (
|
| 633 |
-
(img_rgb[:, :, 0] >= lower[0]) & (img_rgb[:, :, 0] <= upper[0]) &
|
| 634 |
-
(img_rgb[:, :, 1] >= lower[1]) & (img_rgb[:, :, 1] <= upper[1]) &
|
| 635 |
-
(img_rgb[:, :, 2] >= lower[2]) & (img_rgb[:, :, 2] <= upper[2])
|
| 636 |
-
).astype(np.uint8) * 255
|
| 637 |
-
|
| 638 |
-
# Clean up the pink mask
|
| 639 |
-
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
| 640 |
-
binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
|
| 641 |
-
binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
|
| 642 |
-
|
| 643 |
-
nonzero = int((binmask > 0).sum())
|
| 644 |
-
total_pixels = binmask.shape[0] * binmask.shape[1]
|
| 645 |
-
log.info(f"Detected {nonzero} pink pixels ({100*nonzero/total_pixels:.2f}% of image) to remove")
|
| 646 |
-
|
| 647 |
-
# Debug: log bounds used
|
| 648 |
-
log.info("Pink detection bounds used: lower=[150,0,100], upper=[255,120,255]")
|
| 649 |
-
|
| 650 |
-
if nonzero < 50:
|
| 651 |
-
log.error("No pink segments detected! Returning original image.")
|
| 652 |
-
result = np.array(img.convert("RGB"))
|
| 653 |
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 654 |
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 655 |
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
| 656 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
"result": result_name,
|
| 658 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 659 |
}
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
|
| 667 |
-
# RGB channels don't matter for process_inpaint, but set them to white where pink for visualization
|
| 668 |
-
mask_rgba[:, :, 0] = binmask # R: white where pink
|
| 669 |
-
mask_rgba[:, :, 1] = binmask # G: white where pink
|
| 670 |
-
mask_rgba[:, :, 2] = binmask # B: white where pink
|
| 671 |
-
# Alpha: 0 (transparent) where pink → will become white after 255-alpha
|
| 672 |
-
# 255 (opaque) everywhere else → will become black after 255-alpha
|
| 673 |
-
mask_rgba[:, :, 3] = 255 - binmask # Invert: pink areas get alpha=0, rest get alpha=255
|
| 674 |
-
|
| 675 |
-
# Verify mask encoding
|
| 676 |
-
alpha_zero_count = int((mask_rgba[:,:,3] == 0).sum())
|
| 677 |
-
alpha_255_count = int((mask_rgba[:,:,3] == 255).sum())
|
| 678 |
-
total_pixels = binmask.shape[0] * binmask.shape[1]
|
| 679 |
-
log.info(f"Mask encoding: {alpha_zero_count} pixels with alpha=0 (pink), {alpha_255_count} pixels with alpha=255 (keep)")
|
| 680 |
-
log.info(f"After 255-alpha conversion: {alpha_zero_count} will become white (255/remove), {alpha_255_count} will become black (0/keep)")
|
| 681 |
-
|
| 682 |
-
# IMPORTANT: We need to use the ORIGINAL image WITHOUT pink paint for inpainting!
|
| 683 |
-
# Remove pink from the original image before processing
|
| 684 |
-
# Create a clean version: where pink was detected, keep original image colors
|
| 685 |
-
img_clean = np.array(img.convert("RGBA"))
|
| 686 |
-
# Where pink is detected, we want to inpaint, so we can leave it (or blend it out)
|
| 687 |
-
# Actually, the model will inpaint over those areas, so we can pass the original
|
| 688 |
-
# But for better results, we might want to remove the pink overlay first
|
| 689 |
-
|
| 690 |
-
# Process with invert_mask=True (default) because process_inpaint expects alpha=0 for removal
|
| 691 |
-
log.info(f"Starting inpainting process...")
|
| 692 |
-
result = process_inpaint(img_clean, mask_rgba, invert_mask=True)
|
| 693 |
-
log.info(f"Inpainting complete, result shape: {result.shape}")
|
| 694 |
-
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 695 |
-
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 696 |
-
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
| 697 |
-
|
| 698 |
-
url: Optional[str] = None
|
| 699 |
-
try:
|
| 700 |
-
if request is not None:
|
| 701 |
-
url = str(request.url_for("download_file", filename=result_name))
|
| 702 |
-
except Exception:
|
| 703 |
-
url = None
|
| 704 |
-
|
| 705 |
-
logs.append({
|
| 706 |
-
"result": result_name,
|
| 707 |
-
"filename": image.filename,
|
| 708 |
-
"pink_pixels": nonzero,
|
| 709 |
-
"timestamp": datetime.utcnow().isoformat()
|
| 710 |
-
})
|
| 711 |
-
|
| 712 |
-
resp: Dict[str, str] = {"result": result_name, "pink_segments_detected": str(nonzero)}
|
| 713 |
-
if url:
|
| 714 |
-
resp["url"] = url
|
| 715 |
-
log_media_click(user_id, category_id)
|
| 716 |
-
return resp
|
| 717 |
|
| 718 |
|
| 719 |
@app.get("/download/{filename}")
|
|
|
|
| 158 |
|
| 159 |
|
| 160 |
def log_media_click(user_id: Optional[str], category_id: Optional[str]) -> None:
|
| 161 |
+
"""Log to admin media_clicks collection only if user_id is provided."""
|
| 162 |
if admin_media_clicks is None:
|
| 163 |
return
|
| 164 |
+
# Only log if user_id is provided (not None/empty)
|
| 165 |
+
if not user_id or not user_id.strip():
|
| 166 |
+
return
|
| 167 |
try:
|
| 168 |
user_obj = _coerce_object_id(user_id)
|
| 169 |
category_obj = _coerce_category_id(category_id)
|
|
|
|
| 446 |
@app.post("/inpaint-url")
|
| 447 |
def inpaint_url(req: InpaintRequest, request: Request, _: None = Depends(bearer_auth)) -> Dict[str, str]:
|
| 448 |
"""Same as /inpaint but returns a JSON with a public download URL instead of image bytes."""
|
| 449 |
+
start_time = time.time()
|
| 450 |
+
status = "success"
|
| 451 |
+
error_msg = None
|
| 452 |
+
result_name = None
|
| 453 |
|
| 454 |
+
try:
|
| 455 |
+
if req.image_id not in file_store or file_store[req.image_id]["type"] != "image":
|
| 456 |
+
raise HTTPException(status_code=404, detail="image_id not found")
|
| 457 |
+
if req.mask_id not in file_store or file_store[req.mask_id]["type"] != "mask":
|
| 458 |
+
raise HTTPException(status_code=404, detail="mask_id not found")
|
| 459 |
|
| 460 |
+
img_rgba = _load_rgba_image(file_store[req.image_id]["path"])
|
| 461 |
+
mask_img = Image.open(file_store[req.mask_id]["path"]) # may be RGB/gray/RGBA
|
| 462 |
+
mask_rgba = _load_rgba_mask_from_image(mask_img)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
|
| 464 |
+
if req.passthrough:
|
| 465 |
+
result = np.array(img_rgba.convert("RGB"))
|
| 466 |
+
else:
|
| 467 |
+
result = process_inpaint(np.array(img_rgba), mask_rgba, invert_mask=req.invert_mask)
|
| 468 |
+
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 469 |
+
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 470 |
+
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
| 471 |
+
|
| 472 |
+
url = str(request.url_for("download_file", filename=result_name))
|
| 473 |
+
logs.append({"result": result_name, "url": url, "timestamp": datetime.utcnow().isoformat()})
|
| 474 |
+
log_media_click(req.user_id, req.category_id)
|
| 475 |
+
return {"result": result_name, "url": url}
|
| 476 |
+
except Exception as e:
|
| 477 |
+
status = "fail"
|
| 478 |
+
error_msg = str(e)
|
| 479 |
+
raise
|
| 480 |
+
finally:
|
| 481 |
+
# Always log to regular MongoDB (mandatory)
|
| 482 |
+
end_time = time.time()
|
| 483 |
+
response_time_ms = (end_time - start_time) * 1000
|
| 484 |
+
log_doc = {
|
| 485 |
+
"input_image_id": req.image_id,
|
| 486 |
+
"input_mask_id": req.mask_id,
|
| 487 |
+
"output_id": result_name,
|
| 488 |
+
"status": status,
|
| 489 |
+
"timestamp": datetime.utcnow(),
|
| 490 |
+
"ts": int(time.time()),
|
| 491 |
+
"response_time_ms": response_time_ms,
|
| 492 |
+
}
|
| 493 |
+
if error_msg:
|
| 494 |
+
log_doc["error"] = error_msg
|
| 495 |
+
try:
|
| 496 |
+
mongo_logs.insert_one(log_doc)
|
| 497 |
+
except Exception as mongo_err:
|
| 498 |
+
log.error("Mongo log insert failed: %s", mongo_err)
|
| 499 |
|
| 500 |
|
| 501 |
@app.post("/inpaint-multipart")
|
|
|
|
| 510 |
category_id: Optional[str] = Form(None),
|
| 511 |
_: None = Depends(bearer_auth),
|
| 512 |
) -> Dict[str, str]:
|
| 513 |
+
start_time = time.time()
|
| 514 |
+
status = "success"
|
| 515 |
+
error_msg = None
|
| 516 |
+
result_name = None
|
| 517 |
+
|
| 518 |
+
try:
|
| 519 |
+
# Load in-memory
|
| 520 |
+
img = Image.open(image.file).convert("RGBA")
|
| 521 |
+
m = Image.open(mask.file).convert("RGBA")
|
| 522 |
+
|
| 523 |
+
if passthrough:
|
| 524 |
+
# Just echo the input image, ignore mask
|
| 525 |
+
result = np.array(img.convert("RGB"))
|
| 526 |
+
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 527 |
+
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 528 |
+
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
| 529 |
+
|
| 530 |
+
url: Optional[str] = None
|
| 531 |
+
try:
|
| 532 |
+
if request is not None:
|
| 533 |
+
url = str(request.url_for("download_file", filename=result_name))
|
| 534 |
+
except Exception:
|
| 535 |
+
url = None
|
| 536 |
+
|
| 537 |
+
entry: Dict[str, str] = {"result": result_name, "timestamp": datetime.utcnow().isoformat()}
|
| 538 |
+
if url:
|
| 539 |
+
entry["url"] = url
|
| 540 |
+
logs.append(entry)
|
| 541 |
+
resp: Dict[str, str] = {"result": result_name}
|
| 542 |
+
if url:
|
| 543 |
+
resp["url"] = url
|
| 544 |
+
log_media_click(user_id, category_id)
|
| 545 |
+
return resp
|
| 546 |
+
|
| 547 |
+
if mask_is_painted:
|
| 548 |
+
# Auto-detect pink/magenta paint and convert to black/white mask
|
| 549 |
+
# White pixels = areas to remove, Black pixels = areas to keep
|
| 550 |
+
log.info("Auto-detecting pink/magenta paint from uploaded image...")
|
| 551 |
+
|
| 552 |
+
m_rgb = cv2.cvtColor(np.array(m), cv2.COLOR_RGBA2RGB)
|
| 553 |
+
|
| 554 |
+
# Detect pink/magenta using fixed RGB bounds (same as /remove-pink)
|
| 555 |
+
lower = np.array([150, 0, 100], dtype=np.uint8)
|
| 556 |
+
upper = np.array([255, 120, 255], dtype=np.uint8)
|
| 557 |
+
magenta_detected = (
|
| 558 |
+
(m_rgb[:, :, 0] >= lower[0]) & (m_rgb[:, :, 0] <= upper[0]) &
|
| 559 |
+
(m_rgb[:, :, 1] >= lower[1]) & (m_rgb[:, :, 1] <= upper[1]) &
|
| 560 |
+
(m_rgb[:, :, 2] >= lower[2]) & (m_rgb[:, :, 2] <= upper[2])
|
| 561 |
+
).astype(np.uint8) * 255
|
| 562 |
+
|
| 563 |
+
# Method 2: Also check if original image was provided to find differences
|
| 564 |
+
if img is not None:
|
| 565 |
+
img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
|
| 566 |
+
if img_rgb.shape == m_rgb.shape:
|
| 567 |
+
diff = cv2.absdiff(img_rgb, m_rgb)
|
| 568 |
+
gray_diff = cv2.cvtColor(diff, cv2.COLOR_RGB2GRAY)
|
| 569 |
+
# Any significant difference (>50) could be paint
|
| 570 |
+
diff_mask = (gray_diff > 50).astype(np.uint8) * 255
|
| 571 |
+
# Combine with magenta detection
|
| 572 |
+
binmask = cv2.bitwise_or(magenta_detected, diff_mask)
|
| 573 |
+
else:
|
| 574 |
+
binmask = magenta_detected
|
| 575 |
+
else:
|
| 576 |
+
# No original image provided, use magenta detection only
|
| 577 |
+
binmask = magenta_detected
|
| 578 |
+
|
| 579 |
+
# Clean up the mask: remove noise and fill small holes
|
| 580 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
| 581 |
+
# Close small gaps in the mask
|
| 582 |
+
binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
|
| 583 |
+
# Remove small noise
|
| 584 |
+
binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
|
| 585 |
+
|
| 586 |
+
nonzero = int((binmask > 0).sum())
|
| 587 |
+
log.info("Pink/magenta paint detected: %d pixels marked for removal (white)", nonzero)
|
| 588 |
+
|
| 589 |
+
# If very few pixels detected, assume the user may already be providing a BW mask
|
| 590 |
+
# and proceed without forcing strict detection
|
| 591 |
+
|
| 592 |
+
if nonzero < 50:
|
| 593 |
+
log.error("CRITICAL: Could not detect pink/magenta paint! Returning original image.")
|
| 594 |
+
result = np.array(img.convert("RGB")) if img else np.array(m.convert("RGB"))
|
| 595 |
+
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 596 |
+
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 597 |
+
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
| 598 |
+
return {"result": result_name, "error": "pink/magenta paint detection failed - very few pixels detected"}
|
| 599 |
+
|
| 600 |
+
# Create binary mask: Pink pixels → white (255), Everything else → black (0)
|
| 601 |
+
# Encode in RGBA format for process_inpaint
|
| 602 |
+
# process_inpaint does: mask = 255 - mask[:,:,3]
|
| 603 |
+
# So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
|
| 604 |
+
# alpha=255 (opaque/keep) → becomes 0 (black/keep)
|
| 605 |
+
mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
|
| 606 |
+
mask_rgba[:, :, 0] = binmask # R: white where pink (for visualization)
|
| 607 |
+
mask_rgba[:, :, 1] = binmask # G: white where pink
|
| 608 |
+
mask_rgba[:, :, 2] = binmask # B: white where pink
|
| 609 |
+
# Alpha: invert so pink areas get alpha=0 → will become white after 255-alpha
|
| 610 |
+
mask_rgba[:, :, 3] = 255 - binmask
|
| 611 |
+
|
| 612 |
+
log.info("Successfully created binary mask: %d pink pixels → white (255), %d pixels → black (0)",
|
| 613 |
+
nonzero, binmask.shape[0] * binmask.shape[1] - nonzero)
|
| 614 |
+
else:
|
| 615 |
+
mask_rgba = _load_rgba_mask_from_image(m)
|
| 616 |
|
| 617 |
+
# When mask_is_painted=true, we encode pink as alpha=0, so process_inpaint's default invert_mask=True works correctly
|
| 618 |
+
actual_invert = invert_mask # Use default True for painted masks
|
| 619 |
+
log.info("Using invert_mask=%s (mask_is_painted=%s)", actual_invert, mask_is_painted)
|
| 620 |
+
|
| 621 |
+
result = process_inpaint(np.array(img), mask_rgba, invert_mask=actual_invert)
|
| 622 |
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 623 |
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 624 |
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
|
|
|
| 639 |
resp["url"] = url
|
| 640 |
log_media_click(user_id, category_id)
|
| 641 |
return resp
|
| 642 |
+
except Exception as e:
|
| 643 |
+
status = "fail"
|
| 644 |
+
error_msg = str(e)
|
| 645 |
+
raise
|
| 646 |
+
finally:
|
| 647 |
+
# Always log to regular MongoDB (mandatory)
|
| 648 |
+
end_time = time.time()
|
| 649 |
+
response_time_ms = (end_time - start_time) * 1000
|
| 650 |
+
log_doc = {
|
| 651 |
+
"endpoint": "inpaint-multipart",
|
| 652 |
+
"output_id": result_name,
|
| 653 |
+
"status": status,
|
| 654 |
+
"timestamp": datetime.utcnow(),
|
| 655 |
+
"ts": int(time.time()),
|
| 656 |
+
"response_time_ms": response_time_ms,
|
| 657 |
+
}
|
| 658 |
+
if error_msg:
|
| 659 |
+
log_doc["error"] = error_msg
|
| 660 |
+
try:
|
| 661 |
+
mongo_logs.insert_one(log_doc)
|
| 662 |
+
except Exception as mongo_err:
|
| 663 |
+
log.error("Mongo log insert failed: %s", mongo_err)
|
| 664 |
|
| 665 |
+
|
| 666 |
+
@app.post("/remove-pink")
|
| 667 |
+
def remove_pink_segments(
|
| 668 |
+
image: UploadFile = File(...),
|
| 669 |
+
request: Request = None,
|
| 670 |
+
user_id: Optional[str] = Form(None),
|
| 671 |
+
category_id: Optional[str] = Form(None),
|
| 672 |
+
_: None = Depends(bearer_auth),
|
| 673 |
+
) -> Dict[str, str]:
|
| 674 |
+
"""
|
| 675 |
+
Simple endpoint: upload an image with pink/magenta segments to remove.
|
| 676 |
+
- Pink/Magenta segments → automatically removed (white in mask)
|
| 677 |
+
- Everything else → automatically kept (black in mask)
|
| 678 |
+
Just paint pink/magenta on areas you want to remove, upload the image, and it works!
|
| 679 |
+
"""
|
| 680 |
+
start_time = time.time()
|
| 681 |
+
status = "success"
|
| 682 |
+
error_msg = None
|
| 683 |
+
result_name = None
|
| 684 |
+
|
| 685 |
+
try:
|
| 686 |
+
log.info(f"Simple remove-pink: processing image {image.filename}")
|
| 687 |
|
| 688 |
+
# Load the image (with pink paint on it)
|
| 689 |
+
img = Image.open(image.file).convert("RGBA")
|
| 690 |
+
img_rgb = cv2.cvtColor(np.array(img), cv2.COLOR_RGBA2RGB)
|
| 691 |
|
| 692 |
+
# Auto-detect pink/magenta segments to remove
|
| 693 |
+
# Pink/Magenta → white in mask (remove)
|
| 694 |
+
# Everything else (natural image colors, including dark areas) → black in mask (keep)
|
| 695 |
+
|
| 696 |
+
# Detect pink/magenta using fixed RGB bounds per requested logic
|
| 697 |
lower = np.array([150, 0, 100], dtype=np.uint8)
|
| 698 |
upper = np.array([255, 120, 255], dtype=np.uint8)
|
| 699 |
+
binmask = (
|
| 700 |
+
(img_rgb[:, :, 0] >= lower[0]) & (img_rgb[:, :, 0] <= upper[0]) &
|
| 701 |
+
(img_rgb[:, :, 1] >= lower[1]) & (img_rgb[:, :, 1] <= upper[1]) &
|
| 702 |
+
(img_rgb[:, :, 2] >= lower[2]) & (img_rgb[:, :, 2] <= upper[2])
|
| 703 |
).astype(np.uint8) * 255
|
| 704 |
|
| 705 |
+
# Clean up the pink mask
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 706 |
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
|
|
|
|
| 707 |
binmask = cv2.morphologyEx(binmask, cv2.MORPH_CLOSE, kernel, iterations=2)
|
|
|
|
| 708 |
binmask = cv2.morphologyEx(binmask, cv2.MORPH_OPEN, kernel, iterations=1)
|
| 709 |
|
| 710 |
nonzero = int((binmask > 0).sum())
|
| 711 |
+
total_pixels = binmask.shape[0] * binmask.shape[1]
|
| 712 |
+
log.info(f"Detected {nonzero} pink pixels ({100*nonzero/total_pixels:.2f}% of image) to remove")
|
| 713 |
|
| 714 |
+
# Debug: log bounds used
|
| 715 |
+
log.info("Pink detection bounds used: lower=[150,0,100], upper=[255,120,255]")
|
| 716 |
|
| 717 |
if nonzero < 50:
|
| 718 |
+
log.error("No pink segments detected! Returning original image.")
|
| 719 |
+
result = np.array(img.convert("RGB"))
|
| 720 |
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 721 |
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 722 |
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
| 723 |
+
return {
|
| 724 |
+
"result": result_name,
|
| 725 |
+
"error": "No pink/magenta segments detected. Please paint areas to remove with magenta/pink color (RGB 255,0,255)."
|
| 726 |
+
}
|
| 727 |
|
| 728 |
# Create binary mask: Pink pixels → white (255), Everything else → black (0)
|
| 729 |
+
# Encode in RGBA format that process_inpaint expects
|
| 730 |
# process_inpaint does: mask = 255 - mask[:,:,3]
|
| 731 |
# So: alpha=0 (transparent/pink) → becomes 255 (white/remove)
|
| 732 |
# alpha=255 (opaque/keep) → becomes 0 (black/keep)
|
| 733 |
mask_rgba = np.zeros((binmask.shape[0], binmask.shape[1], 4), dtype=np.uint8)
|
| 734 |
+
# RGB channels don't matter for process_inpaint, but set them to white where pink for visualization
|
| 735 |
+
mask_rgba[:, :, 0] = binmask # R: white where pink
|
| 736 |
+
mask_rgba[:, :, 1] = binmask # G: white where pink
|
| 737 |
mask_rgba[:, :, 2] = binmask # B: white where pink
|
| 738 |
+
# Alpha: 0 (transparent) where pink → will become white after 255-alpha
|
| 739 |
+
# 255 (opaque) everywhere else → will become black after 255-alpha
|
| 740 |
+
mask_rgba[:, :, 3] = 255 - binmask # Invert: pink areas get alpha=0, rest get alpha=255
|
| 741 |
|
| 742 |
+
# Verify mask encoding
|
| 743 |
+
alpha_zero_count = int((mask_rgba[:,:,3] == 0).sum())
|
| 744 |
+
alpha_255_count = int((mask_rgba[:,:,3] == 255).sum())
|
| 745 |
+
total_pixels = binmask.shape[0] * binmask.shape[1]
|
| 746 |
+
log.info(f"Mask encoding: {alpha_zero_count} pixels with alpha=0 (pink), {alpha_255_count} pixels with alpha=255 (keep)")
|
| 747 |
+
log.info(f"After 255-alpha conversion: {alpha_zero_count} will become white (255/remove), {alpha_255_count} will become black (0/keep)")
|
| 748 |
+
|
| 749 |
+
# IMPORTANT: We need to use the ORIGINAL image WITHOUT pink paint for inpainting!
|
| 750 |
+
# Remove pink from the original image before processing
|
| 751 |
+
# Create a clean version: where pink was detected, keep original image colors
|
| 752 |
+
img_clean = np.array(img.convert("RGBA"))
|
| 753 |
+
# Where pink is detected, we want to inpaint, so we can leave it (or blend it out)
|
| 754 |
+
# Actually, the model will inpaint over those areas, so we can pass the original
|
| 755 |
+
# But for better results, we might want to remove the pink overlay first
|
| 756 |
+
|
| 757 |
+
# Process with invert_mask=True (default) because process_inpaint expects alpha=0 for removal
|
| 758 |
+
log.info(f"Starting inpainting process...")
|
| 759 |
+
result = process_inpaint(img_clean, mask_rgba, invert_mask=True)
|
| 760 |
+
log.info(f"Inpainting complete, result shape: {result.shape}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 761 |
result_name = f"output_{uuid.uuid4().hex}.png"
|
| 762 |
result_path = os.path.join(OUTPUT_DIR, result_name)
|
| 763 |
Image.fromarray(result).save(result_path, "PNG", optimize=False, compress_level=1)
|
| 764 |
+
|
| 765 |
+
url: Optional[str] = None
|
| 766 |
+
try:
|
| 767 |
+
if request is not None:
|
| 768 |
+
url = str(request.url_for("download_file", filename=result_name))
|
| 769 |
+
except Exception:
|
| 770 |
+
url = None
|
| 771 |
+
|
| 772 |
+
logs.append({
|
| 773 |
"result": result_name,
|
| 774 |
+
"filename": image.filename,
|
| 775 |
+
"pink_pixels": nonzero,
|
| 776 |
+
"timestamp": datetime.utcnow().isoformat()
|
| 777 |
+
})
|
| 778 |
+
|
| 779 |
+
resp: Dict[str, str] = {"result": result_name, "pink_segments_detected": str(nonzero)}
|
| 780 |
+
if url:
|
| 781 |
+
resp["url"] = url
|
| 782 |
+
log_media_click(user_id, category_id)
|
| 783 |
+
return resp
|
| 784 |
+
except Exception as e:
|
| 785 |
+
status = "fail"
|
| 786 |
+
error_msg = str(e)
|
| 787 |
+
raise
|
| 788 |
+
finally:
|
| 789 |
+
# Always log to regular MongoDB (mandatory)
|
| 790 |
+
end_time = time.time()
|
| 791 |
+
response_time_ms = (end_time - start_time) * 1000
|
| 792 |
+
log_doc = {
|
| 793 |
+
"endpoint": "remove-pink",
|
| 794 |
+
"output_id": result_name,
|
| 795 |
+
"status": status,
|
| 796 |
+
"timestamp": datetime.utcnow(),
|
| 797 |
+
"ts": int(time.time()),
|
| 798 |
+
"response_time_ms": response_time_ms,
|
| 799 |
}
|
| 800 |
+
if error_msg:
|
| 801 |
+
log_doc["error"] = error_msg
|
| 802 |
+
try:
|
| 803 |
+
mongo_logs.insert_one(log_doc)
|
| 804 |
+
except Exception as mongo_err:
|
| 805 |
+
log.error("Mongo log insert failed: %s", mongo_err)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
|
| 807 |
|
| 808 |
@app.get("/download/{filename}")
|