Spaces:
No application file
No application file
Upload 90 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +7 -0
- README.md +108 -14
- app.py +183 -0
- backend/__init__.py +1 -0
- backend/__pycache__/__init__.cpython-311.pyc +0 -0
- backend/__pycache__/__init__.cpython-314.pyc +0 -0
- backend/__pycache__/edge_detector.cpython-311.pyc +0 -0
- backend/__pycache__/edge_detector.cpython-314.pyc +0 -0
- backend/__pycache__/image_processor.cpython-311.pyc +0 -0
- backend/__pycache__/image_processor.cpython-314.pyc +0 -0
- backend/__pycache__/segmentation.cpython-311.pyc +0 -0
- backend/__pycache__/segmentation.cpython-314.pyc +0 -0
- backend/__pycache__/shadow_generator.cpython-311.pyc +0 -0
- backend/__pycache__/shadow_generator.cpython-314.pyc +0 -0
- backend/__pycache__/utilities.cpython-311.pyc +0 -0
- backend/__pycache__/utilities.cpython-314.pyc +0 -0
- backend/bg_remover/__init__.py +1 -0
- backend/bg_remover/__pycache__/__init__.cpython-311.pyc +0 -0
- backend/bg_remover/__pycache__/__init__.cpython-314.pyc +0 -0
- backend/bg_remover/__pycache__/edge_detection.cpython-311.pyc +0 -0
- backend/bg_remover/__pycache__/edge_detection.cpython-314.pyc +0 -0
- backend/bg_remover/__pycache__/image_processor.cpython-311.pyc +0 -0
- backend/bg_remover/__pycache__/image_processor.cpython-314.pyc +0 -0
- backend/bg_remover/__pycache__/segmentation.cpython-311.pyc +0 -0
- backend/bg_remover/__pycache__/segmentation.cpython-314.pyc +0 -0
- backend/bg_remover/__pycache__/shadow_generator.cpython-311.pyc +0 -0
- backend/bg_remover/__pycache__/shadow_generator.cpython-314.pyc +0 -0
- backend/bg_remover/edge_detection.py +85 -0
- backend/bg_remover/image_processor.py +194 -0
- backend/bg_remover/segmentation.py +137 -0
- backend/bg_remover/shadow_generator.py +68 -0
- backend/dslr_blur/__pycache__/blur_processor.cpython-311.pyc +0 -0
- backend/dslr_blur/__pycache__/depth_blur.cpython-311.pyc +0 -0
- backend/dslr_blur/blur_processor.py +172 -0
- backend/dslr_blur/depth_blur.py +170 -0
- backend/enhancement/__pycache__/onnx_engine.cpython-311.pyc +0 -0
- backend/enhancement/__pycache__/photo_enhancer.cpython-311.pyc +0 -0
- backend/enhancement/onnx_engine.py +205 -0
- backend/enhancement/photo_enhancer.py +150 -0
- backend/models/depth_anything.onnx +3 -0
- backend/text_editor/__init__.py +2 -0
- backend/text_editor/__pycache__/__init__.cpython-311.pyc +0 -0
- backend/text_editor/__pycache__/__init__.cpython-314.pyc +0 -0
- backend/text_editor/__pycache__/color_detector.cpython-311.pyc +0 -0
- backend/text_editor/__pycache__/color_detector.cpython-314.pyc +0 -0
- backend/text_editor/__pycache__/font_detector.cpython-311.pyc +0 -0
- backend/text_editor/__pycache__/font_detector.cpython-314.pyc +0 -0
- backend/text_editor/__pycache__/font_downloader.cpython-311.pyc +0 -0
- backend/text_editor/__pycache__/ocr_engine.cpython-311.pyc +0 -0
- backend/text_editor/__pycache__/ocr_engine.cpython-314.pyc +0 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
backend/text_editor/fonts/hind_regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
backend/text_editor/fonts/kalam_regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
backend/text_editor/fonts/martel_regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
backend/text_editor/fonts/notosansdevanagari_regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
backend/text_editor/fonts/rozhaone_regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
backend/text_editor/fonts/teko_regular.ttf filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
backend/text_editor/fonts/yatraone_regular.ttf filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,19 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
-
#
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ⚡ Antigravity Studio - AI Image & Document Workspace
|
| 2 |
+
|
| 3 |
+
A complete, production-grade, state-of-the-art **Image & PDF Manipulation Web Application** consisting of two primary workspaces:
|
| 4 |
+
1. **AI Background Remover Workspace**: Isolate foreground subjects with U²-Net deep learning or GrabCut, refine fine hair details with Guided Filter matting, and generate customizable realistic drop shadows.
|
| 5 |
+
2. **AI In-Image Text Editor Workspace**: Automatically detect, erase, and replace text in images or multi-page PDFs while perfectly matching original fonts, sizes, and ink colors.
|
| 6 |
+
|
| 7 |
+
Built with **Streamlit** (frontend) and **Python + OpenCV + Deep Learning** (backend).
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## ✨ Workspace 1: AI Background Remover
|
| 12 |
+
|
| 13 |
+
Allows users to upload an image, extract the subject with professional-grade borders, and customize visual compositions.
|
| 14 |
+
|
| 15 |
+
- **AI U²-Net Neural Network Mode**: Leverages pre-trained deep learning to automatically segment complex shapes (like portraits, hair, and soft details) without bounding box configurations.
|
| 16 |
+
- **GrabCut General Subject Mode**: Detects bounding boxes automatically or supports manual coordinate tuning.
|
| 17 |
+
- **Signature & Text (Ink Extraction) Mode**: Uses color distance maps to isolate handwritten ink from paper backdrops (perfect for signatures).
|
| 18 |
+
- **Advanced Edge Matting**:
|
| 19 |
+
- *Guided Filter Matting*: An edge-preserving linear filtering algorithm that aligns alpha transitions to individual hair fibers.
|
| 20 |
+
- *Morphological Closing*: Fills inner holes and smooths contours.
|
| 21 |
+
- *Speckle Filtering*: Cleans isolated floating noise.
|
| 22 |
+
- **Realistic Drop Shadow Engine**: Generates natural drop shadows beneath isolated subjects. Customize opacity, blur softness, offset distance, and angle.
|
| 23 |
+
- **Interactive Composite Studio**: Composite transparent cutouts over Checkerboard grids, Solid colors, gradients (*Midnight Glow, Sunset Studio, Neon Cyber*), or uploaded background images.
|
| 24 |
+
|
| 25 |
---
|
| 26 |
+
|
| 27 |
+
## ✨ Workspace 2: AI Document & PDF Text Editor
|
| 28 |
+
|
| 29 |
+
Allows users to upload an image or multi-page PDF, find text, and perform clean in-place corrections.
|
| 30 |
+
|
| 31 |
+
- **Automated Layout Scan**: Scans images or document page textures using **EasyOCR** to discover bounding boxes of all text segments.
|
| 32 |
+
- **Translucent OCR Overlays**: Renders glowing overlays displaying editable text fields in the document.
|
| 33 |
+
- **Zero-Configuration PDF Engine**: Powered by **PyMuPDF (Fitz)**. Render multi-page PDFs, navigate pages, and compile edited page images back into a single multi-page PDF document.
|
| 34 |
+
- **Adaptive Ink & Paper Color Sampling**: Extracts the median BGR ink color (foreground character strokes) and surrounding paper texture color (background) using Otsu's adaptive binarization of characters.
|
| 35 |
+
- **Seamless Texture-Preserving Eraser**: Creates a character-level binary mask, dilates it by 2px, and runs OpenCV Fast Inpainting (**Telea's algorithm**). This erases old text while preserving paper folds, grain, gridlines, and shadows.
|
| 36 |
+
- **Adaptive Text Synthesis**: Scales the replacement text according to the bounding box height, matches standard font families (Serif, Sans-Serif, Monospace), and renders it anti-aliased with the sampled ink color.
|
| 37 |
+
|
| 38 |
+
---
|
| 39 |
+
|
| 40 |
+
## 📂 Project Structure
|
| 41 |
+
|
| 42 |
+
```text
|
| 43 |
+
e:\bg remover\
|
| 44 |
+
├── app.py # Main Streamlit dashboard routing entrypoint & CSS styles
|
| 45 |
+
├── requirements.txt # Unified dependency manifest
|
| 46 |
+
├── README.md # Detailed workspace and setup documentation
|
| 47 |
+
│
|
| 48 |
+
├── frontend/ # Modern UI Layout Modules
|
| 49 |
+
│ ├── __init__.py
|
| 50 |
+
│ ├── home.py # Premium Glassmorphic Studio Navigation landing page
|
| 51 |
+
│ ├── bg_remover_ui.py # Custom Background Remover controls and workflows
|
| 52 |
+
│ ├── text_editor_ui.py # PDF page navigation, OCR list, Find/Replace editor
|
| 53 |
+
│ ├── preview_ui.py # Before/After comparison and backdrop composites
|
| 54 |
+
│ └── download_ui.py # Downscaled/lazy high-res download compiler
|
| 55 |
+
│
|
| 56 |
+
└── backend/ # Algorithms and Heavy Processing Backends
|
| 57 |
+
├── __init__.py
|
| 58 |
+
├── utilities.py # Image format converters and checkerboard pattern generators
|
| 59 |
+
│
|
| 60 |
+
├── bg_remover/ # Background Remover Subpackage
|
| 61 |
+
│ ├── __init__.py
|
| 62 |
+
│ ├── segmentation.py # GrabCut, contour analysis, and corner color GMM seeding
|
| 63 |
+
│ ├── edge_detection.py # Fast Guided Filter matting and morphological refinements
|
| 64 |
+
│ ├── shadow_generator.py# Affine transformation drop shadow translations and blenders
|
| 65 |
+
│ └── image_processor.py # Background remover high-level pipeline orchestrator
|
| 66 |
+
│
|
| 67 |
+
└── text_editor/ # Text Editor & Document Subpackage
|
| 68 |
+
├── __init__.py
|
| 69 |
+
├── pdf_processor.py # PyMuPDF page extractors and PDF compilers
|
| 70 |
+
├── ocr_engine.py # EasyOCR layout scanning engine
|
| 71 |
+
├── color_detector.py # Character-level ink/paper color samplers
|
| 72 |
+
├── font_detector.py # System TTF font loader and height-based size mapper
|
| 73 |
+
├── text_replacer.py # Telea inpainter and PIL anti-aliased text renderer
|
| 74 |
+
└── orchestrator.py # Text correction orchestrator
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
---
|
| 78 |
+
|
| 79 |
+
## 🚀 Installation & Running Locally
|
| 80 |
+
|
| 81 |
+
Ensure you have **Python 3.10+** installed. Follow these steps:
|
| 82 |
+
|
| 83 |
+
### 1. Install Dependencies
|
| 84 |
+
Open a terminal in the root of the workspace (`e:\bg remover\`) and run:
|
| 85 |
+
```bash
|
| 86 |
+
pip install -r requirements.txt
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
### 2. Run the Application
|
| 90 |
+
Start the Streamlit dashboard:
|
| 91 |
+
```bash
|
| 92 |
+
streamlit run app.py
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### 3. Open in Browser
|
| 96 |
+
The local server will start and prompt you to open the dashboard at:
|
| 97 |
+
- **Local URL**: `http://localhost:8501`
|
| 98 |
+
|
| 99 |
---
|
| 100 |
|
| 101 |
+
## 🛠️ Offline Verification & Test Suites
|
| 102 |
|
| 103 |
+
We have built extensive automated tests to guarantee system stability and code correctness:
|
| 104 |
|
| 105 |
+
- **Background Remover Tests**:
|
| 106 |
+
```bash
|
| 107 |
+
python "C:\Users\Asus\.gemini\antigravity\brain\76a54bfc-09a7-4a96-809d-8b8c797bd925\scratch\test_pipeline.py"
|
| 108 |
+
```
|
| 109 |
+
- **Document Text Editor Tests**:
|
| 110 |
+
```bash
|
| 111 |
+
python "C:\Users\Asus\.gemini\antigravity\brain\76a54bfc-09a7-4a96-809d-8b8c797bd925\scratch\test_text_editor.py"
|
| 112 |
+
```
|
| 113 |
+
Both test suites should print `[SUCCESS]` and pass seamlessly!
|
app.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
# Add current workspace directory to sys.path to allow absolute imports
|
| 6 |
+
sys.path.append(os.path.abspath(os.path.dirname(__file__)))
|
| 7 |
+
|
| 8 |
+
from frontend.home import render_sidebar_brand, render_home_dashboard
|
| 9 |
+
from frontend.bg_remover_ui import render_bg_remover_ui
|
| 10 |
+
from frontend.text_editor_ui import render_text_editor_ui
|
| 11 |
+
from frontend.dslr_blur_ui import render_dslr_blur_ui
|
| 12 |
+
|
| 13 |
+
# Set Streamlit page config
|
| 14 |
+
st.set_page_config(
|
| 15 |
+
page_title="Antigravity Studio - AI Image & Document Workspace",
|
| 16 |
+
page_icon="⚡",
|
| 17 |
+
layout="wide",
|
| 18 |
+
initial_sidebar_state="expanded"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
# Custom CSS for Glassmorphic Dark Theme with glowing neon accents
|
| 22 |
+
st.markdown(
|
| 23 |
+
"""
|
| 24 |
+
<style>
|
| 25 |
+
/* Import modern Google Fonts */
|
| 26 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
| 27 |
+
|
| 28 |
+
/* Apply fonts and background styles */
|
| 29 |
+
.stApp {
|
| 30 |
+
background-color: #0E1117;
|
| 31 |
+
font-family: 'Inter', sans-serif;
|
| 32 |
+
color: #E2E8F0;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
/* Header Custom Styles */
|
| 36 |
+
.gradient-title {
|
| 37 |
+
font-family: 'Inter', sans-serif;
|
| 38 |
+
font-weight: 800;
|
| 39 |
+
font-size: 2.8rem;
|
| 40 |
+
background: linear-gradient(135deg, #00F0FF 0%, #FF007F 100%);
|
| 41 |
+
-webkit-background-clip: text;
|
| 42 |
+
-webkit-text-fill-color: transparent;
|
| 43 |
+
text-align: center;
|
| 44 |
+
margin-top: 10px;
|
| 45 |
+
margin-bottom: 5px;
|
| 46 |
+
letter-spacing: -1px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.subtitle {
|
| 50 |
+
text-align: center;
|
| 51 |
+
color: #8A99AD;
|
| 52 |
+
font-size: 1.1rem;
|
| 53 |
+
font-weight: 300;
|
| 54 |
+
margin-bottom: 30px;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* Glassmorphism Containers */
|
| 58 |
+
.glass-card {
|
| 59 |
+
background: rgba(255, 255, 255, 0.03);
|
| 60 |
+
border-radius: 16px;
|
| 61 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 62 |
+
padding: 24px;
|
| 63 |
+
backdrop-filter: blur(12px);
|
| 64 |
+
-webkit-backdrop-filter: blur(12px);
|
| 65 |
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
| 66 |
+
margin-bottom: 20px;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/* Upload UI wrapper */
|
| 70 |
+
.upload-container {
|
| 71 |
+
padding: 40px;
|
| 72 |
+
background: rgba(0, 240, 255, 0.01);
|
| 73 |
+
border: 1px dashed rgba(0, 240, 255, 0.3);
|
| 74 |
+
transition: all 0.3s ease;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.upload-container:hover {
|
| 78 |
+
border-color: #FF007F;
|
| 79 |
+
background: rgba(255, 0, 127, 0.01);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* Custom Success Badge */
|
| 83 |
+
.success-badge {
|
| 84 |
+
background: rgba(0, 240, 255, 0.1);
|
| 85 |
+
color: #00F0FF;
|
| 86 |
+
border: 1px solid rgba(0, 240, 255, 0.2);
|
| 87 |
+
border-radius: 8px;
|
| 88 |
+
padding: 10px 15px;
|
| 89 |
+
font-size: 0.9rem;
|
| 90 |
+
margin: 15px 0;
|
| 91 |
+
text-align: center;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* Labels and Headers */
|
| 95 |
+
.preview-label {
|
| 96 |
+
font-size: 0.8rem;
|
| 97 |
+
font-weight: 700;
|
| 98 |
+
letter-spacing: 1.5px;
|
| 99 |
+
color: #FF007F;
|
| 100 |
+
margin-bottom: 10px;
|
| 101 |
+
text-align: center;
|
| 102 |
+
text-transform: uppercase;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* Custom Styled Sliders & Widgets in Sidebar */
|
| 106 |
+
[data-testid="stSidebar"] {
|
| 107 |
+
background-color: #0A0D14;
|
| 108 |
+
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
[data-testid="stSidebar"] .stMarkdown h2, [data-testid="stSidebar"] .stMarkdown h3 {
|
| 112 |
+
color: #00F0FF;
|
| 113 |
+
font-weight: 600;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/* Streamlit buttons customized */
|
| 117 |
+
div.stButton > button {
|
| 118 |
+
background: linear-gradient(135deg, #00F0FF 0%, #7000FF 100%);
|
| 119 |
+
color: white;
|
| 120 |
+
border: none;
|
| 121 |
+
border-radius: 8px;
|
| 122 |
+
padding: 10px 24px;
|
| 123 |
+
font-weight: 600;
|
| 124 |
+
transition: all 0.2s ease-in-out;
|
| 125 |
+
box-shadow: 0 4px 15px rgba(0, 240, 255, 0.2);
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
div.stButton > button:hover {
|
| 129 |
+
background: linear-gradient(135deg, #FF007F 0%, #7000FF 100%);
|
| 130 |
+
transform: translateY(-2px);
|
| 131 |
+
box-shadow: 0 6px 20px rgba(255, 0, 127, 0.35);
|
| 132 |
+
color: white;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/* Glowing accents */
|
| 136 |
+
.glow-accent {
|
| 137 |
+
text-shadow: 0 0 10px rgba(0, 240, 255, 0.5);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
</style>
|
| 141 |
+
""",
|
| 142 |
+
unsafe_allow_html=True
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
# ----------------- SESSION STATE INITIALIZATION -----------------
|
| 146 |
+
if "active_workspace" not in st.session_state:
|
| 147 |
+
st.session_state.active_workspace = "Home"
|
| 148 |
+
|
| 149 |
+
# ----------------- SIDEBAR BRANDING & ROUTING -----------------
|
| 150 |
+
render_sidebar_brand()
|
| 151 |
+
|
| 152 |
+
st.sidebar.markdown("### 🧭 NAVIGATION")
|
| 153 |
+
nav_choice = st.sidebar.selectbox(
|
| 154 |
+
"Active Workspace",
|
| 155 |
+
["Home Dashboard", "AI Background Remover", "AI DSLR Background Blur", "AI In-Image Text Editor"],
|
| 156 |
+
index=["Home Dashboard", "AI Background Remover", "AI DSLR Background Blur", "AI In-Image Text Editor"].index(
|
| 157 |
+
"Home Dashboard" if st.session_state.active_workspace == "Home" else st.session_state.active_workspace
|
| 158 |
+
),
|
| 159 |
+
key="nav_choice_select"
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
# Sync sidebar navigation selection with active session state
|
| 163 |
+
choice_mapped = "Home" if nav_choice == "Home Dashboard" else nav_choice
|
| 164 |
+
if choice_mapped != st.session_state.active_workspace:
|
| 165 |
+
st.session_state.active_workspace = choice_mapped
|
| 166 |
+
st.rerun()
|
| 167 |
+
|
| 168 |
+
# Quick nav back to Home from sidebar footer
|
| 169 |
+
st.sidebar.markdown("<br><br><br>", unsafe_allow_html=True)
|
| 170 |
+
if st.session_state.active_workspace != "Home":
|
| 171 |
+
if st.sidebar.button("← Back to Dashboard", use_container_width=True):
|
| 172 |
+
st.session_state.active_workspace = "Home"
|
| 173 |
+
st.rerun()
|
| 174 |
+
|
| 175 |
+
# ----------------- WORKSPACE RENDERING -----------------
|
| 176 |
+
if st.session_state.active_workspace == "Home":
|
| 177 |
+
render_home_dashboard()
|
| 178 |
+
elif st.session_state.active_workspace == "AI Background Remover":
|
| 179 |
+
render_bg_remover_ui()
|
| 180 |
+
elif st.session_state.active_workspace == "AI DSLR Background Blur":
|
| 181 |
+
render_dslr_blur_ui()
|
| 182 |
+
elif st.session_state.active_workspace == "AI In-Image Text Editor":
|
| 183 |
+
render_text_editor_ui()
|
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Backend package
|
backend/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (142 Bytes). View file
|
|
|
backend/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (132 Bytes). View file
|
|
|
backend/__pycache__/edge_detector.cpython-311.pyc
ADDED
|
Binary file (4.67 kB). View file
|
|
|
backend/__pycache__/edge_detector.cpython-314.pyc
ADDED
|
Binary file (5.12 kB). View file
|
|
|
backend/__pycache__/image_processor.cpython-311.pyc
ADDED
|
Binary file (8.18 kB). View file
|
|
|
backend/__pycache__/image_processor.cpython-314.pyc
ADDED
|
Binary file (7.75 kB). View file
|
|
|
backend/__pycache__/segmentation.cpython-311.pyc
ADDED
|
Binary file (7.28 kB). View file
|
|
|
backend/__pycache__/segmentation.cpython-314.pyc
ADDED
|
Binary file (7.59 kB). View file
|
|
|
backend/__pycache__/shadow_generator.cpython-311.pyc
ADDED
|
Binary file (4.74 kB). View file
|
|
|
backend/__pycache__/shadow_generator.cpython-314.pyc
ADDED
|
Binary file (4.64 kB). View file
|
|
|
backend/__pycache__/utilities.cpython-311.pyc
ADDED
|
Binary file (3.28 kB). View file
|
|
|
backend/__pycache__/utilities.cpython-314.pyc
ADDED
|
Binary file (3.92 kB). View file
|
|
|
backend/bg_remover/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Background remover subpackage
|
backend/bg_remover/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (153 Bytes). View file
|
|
|
backend/bg_remover/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (143 Bytes). View file
|
|
|
backend/bg_remover/__pycache__/edge_detection.cpython-311.pyc
ADDED
|
Binary file (4.49 kB). View file
|
|
|
backend/bg_remover/__pycache__/edge_detection.cpython-314.pyc
ADDED
|
Binary file (4.96 kB). View file
|
|
|
backend/bg_remover/__pycache__/image_processor.cpython-311.pyc
ADDED
|
Binary file (8.3 kB). View file
|
|
|
backend/bg_remover/__pycache__/image_processor.cpython-314.pyc
ADDED
|
Binary file (6.68 kB). View file
|
|
|
backend/bg_remover/__pycache__/segmentation.cpython-311.pyc
ADDED
|
Binary file (7.17 kB). View file
|
|
|
backend/bg_remover/__pycache__/segmentation.cpython-314.pyc
ADDED
|
Binary file (7.48 kB). View file
|
|
|
backend/bg_remover/__pycache__/shadow_generator.cpython-311.pyc
ADDED
|
Binary file (4.23 kB). View file
|
|
|
backend/bg_remover/__pycache__/shadow_generator.cpython-314.pyc
ADDED
|
Binary file (4.18 kB). View file
|
|
|
backend/bg_remover/edge_detection.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
def guided_filter(I: np.ndarray, p: np.ndarray, r: int, eps: float) -> np.ndarray:
|
| 5 |
+
"""
|
| 6 |
+
Fast Guided Filter implementation for edge-preserving matting.
|
| 7 |
+
I: Guidance image (BGR, uint8)
|
| 8 |
+
p: Grayscale mask to filter (uint8 or float, normalized to [0, 1])
|
| 9 |
+
r: Local window radius
|
| 10 |
+
eps: Regularization parameter
|
| 11 |
+
"""
|
| 12 |
+
if I.dtype == np.uint8:
|
| 13 |
+
I = I.astype(np.float32) / 255.0
|
| 14 |
+
else:
|
| 15 |
+
I = I.astype(np.float32)
|
| 16 |
+
|
| 17 |
+
p = p.astype(np.float32)
|
| 18 |
+
|
| 19 |
+
# Extract grayscale guidance for fast processing
|
| 20 |
+
if len(I.shape) == 3:
|
| 21 |
+
I_gray = cv2.cvtColor(I, cv2.COLOR_BGR2GRAY)
|
| 22 |
+
else:
|
| 23 |
+
I_gray = I
|
| 24 |
+
|
| 25 |
+
# Local window means
|
| 26 |
+
mean_I = cv2.boxFilter(I_gray, -1, (r, r))
|
| 27 |
+
mean_p = cv2.boxFilter(p, -1, (r, r))
|
| 28 |
+
|
| 29 |
+
mean_II = cv2.boxFilter(I_gray * I_gray, -1, (r, r))
|
| 30 |
+
mean_Ip = cv2.boxFilter(I_gray * p, -1, (r, r))
|
| 31 |
+
|
| 32 |
+
# Variance & Covariance
|
| 33 |
+
var_I = mean_II - mean_I * mean_I
|
| 34 |
+
cov_Ip = mean_Ip - mean_I * mean_p
|
| 35 |
+
|
| 36 |
+
# Solve linear coefficients
|
| 37 |
+
a = cov_Ip / (var_I + eps)
|
| 38 |
+
b = mean_p - a * mean_I
|
| 39 |
+
|
| 40 |
+
# Average coefficients
|
| 41 |
+
mean_a = cv2.boxFilter(a, -1, (r, r))
|
| 42 |
+
mean_b = cv2.boxFilter(b, -1, (r, r))
|
| 43 |
+
|
| 44 |
+
# Reconstruct filtered mask
|
| 45 |
+
q = mean_a * I_gray + mean_b
|
| 46 |
+
return np.clip(q, 0.0, 1.0)
|
| 47 |
+
|
| 48 |
+
def refine_mask(mask: np.ndarray,
|
| 49 |
+
img: np.ndarray = None,
|
| 50 |
+
closing_size: int = 5,
|
| 51 |
+
keep_largest_only: bool = True,
|
| 52 |
+
feather_radius: int = 3,
|
| 53 |
+
matting_enabled: bool = False,
|
| 54 |
+
matting_radius: int = 10,
|
| 55 |
+
matting_eps: float = 1e-3) -> np.ndarray:
|
| 56 |
+
"""
|
| 57 |
+
Refines a binary mask by morphological closing, speckle filtering, and edge matting/feathering.
|
| 58 |
+
"""
|
| 59 |
+
h, w = mask.shape[:2]
|
| 60 |
+
refined = mask.copy()
|
| 61 |
+
|
| 62 |
+
# 1. Morphological closing to fill holes inside the object
|
| 63 |
+
if closing_size > 0:
|
| 64 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (closing_size, closing_size))
|
| 65 |
+
refined = cv2.morphologyEx(refined, cv2.MORPH_CLOSE, kernel)
|
| 66 |
+
refined = cv2.morphologyEx(refined, cv2.MORPH_OPEN, kernel) # clean small edge noise
|
| 67 |
+
|
| 68 |
+
# 2. Keep only the largest connected component to eliminate floating background noise
|
| 69 |
+
if keep_largest_only:
|
| 70 |
+
num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(refined)
|
| 71 |
+
if num_labels > 1:
|
| 72 |
+
largest_label = 1 + np.argmax(stats[1:, cv2.CC_STAT_AREA])
|
| 73 |
+
refined = np.where(labels == largest_label, 255, 0).astype(np.uint8)
|
| 74 |
+
|
| 75 |
+
# 3. Apply Edge Matting (Guided Filter) or Edge Feathering
|
| 76 |
+
if matting_enabled and img is not None:
|
| 77 |
+
mask_norm = refined.astype(np.float32) / 255.0
|
| 78 |
+
filtered_mask = guided_filter(img, mask_norm, matting_radius, matting_eps)
|
| 79 |
+
refined = (filtered_mask * 255.0).astype(np.uint8)
|
| 80 |
+
elif feather_radius > 0:
|
| 81 |
+
ksize = 2 * feather_radius + 1
|
| 82 |
+
refined_floats = cv2.GaussianBlur(refined.astype(float), (ksize, ksize), 0)
|
| 83 |
+
refined = np.clip(refined_floats, 0, 255).astype(np.uint8)
|
| 84 |
+
|
| 85 |
+
return refined
|
backend/bg_remover/image_processor.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from backend.utilities import pil_to_cv, cv_to_pil, resize_for_processing
|
| 5 |
+
from backend.bg_remover.segmentation import detect_automatic_bounding_box, run_grabcut
|
| 6 |
+
from backend.bg_remover.edge_detection import refine_mask
|
| 7 |
+
from backend.bg_remover.shadow_generator import generate_drop_shadow
|
| 8 |
+
|
| 9 |
+
try:
|
| 10 |
+
from rembg import remove as rembg_remove
|
| 11 |
+
REMBG_AVAILABLE = True
|
| 12 |
+
except ImportError:
|
| 13 |
+
REMBG_AVAILABLE = False
|
| 14 |
+
|
| 15 |
+
_session_cache = {}
|
| 16 |
+
|
| 17 |
+
def get_rembg_session(model_name: str):
|
| 18 |
+
"""Retrieves or initializes a cached rembg model session to prevent reloading weights."""
|
| 19 |
+
global _session_cache
|
| 20 |
+
if model_name not in _session_cache:
|
| 21 |
+
try:
|
| 22 |
+
from rembg import new_session
|
| 23 |
+
_session_cache[model_name] = new_session(model_name)
|
| 24 |
+
except Exception as e:
|
| 25 |
+
_session_cache[model_name] = None
|
| 26 |
+
return _session_cache[model_name]
|
| 27 |
+
|
| 28 |
+
class ImageProcessor:
|
| 29 |
+
"""
|
| 30 |
+
High-level orchestrator class to execute the background removal image processing pipeline.
|
| 31 |
+
"""
|
| 32 |
+
@staticmethod
|
| 33 |
+
def process_image(
|
| 34 |
+
pil_image: Image.Image,
|
| 35 |
+
rect: tuple = None,
|
| 36 |
+
margin_percentage: float = 5.0,
|
| 37 |
+
iter_count: int = 5,
|
| 38 |
+
bg_seed_sensitivity: float = 35.0,
|
| 39 |
+
closing_size: int = 5,
|
| 40 |
+
keep_largest_only: bool = True,
|
| 41 |
+
feather_radius: int = 3,
|
| 42 |
+
matting_enabled: bool = True,
|
| 43 |
+
matting_radius: int = 10,
|
| 44 |
+
matting_eps: float = 1e-3,
|
| 45 |
+
shadow_enabled: bool = False,
|
| 46 |
+
shadow_opacity: float = 0.5,
|
| 47 |
+
shadow_blur: int = 15,
|
| 48 |
+
shadow_distance: int = 20,
|
| 49 |
+
shadow_angle: float = 45.0,
|
| 50 |
+
max_preview_dim: int = None,
|
| 51 |
+
subject_mode: str = "AI Neural Network (U²-Net)"
|
| 52 |
+
) -> dict:
|
| 53 |
+
"""
|
| 54 |
+
Processes the input PIL image and returns a dictionary of output PIL images.
|
| 55 |
+
"""
|
| 56 |
+
# 1. Automatically normalize EXIF camera orientation tags
|
| 57 |
+
from PIL import ImageOps
|
| 58 |
+
pil_image = ImageOps.exif_transpose(pil_image)
|
| 59 |
+
|
| 60 |
+
# Convert PIL to OpenCV (BGR)
|
| 61 |
+
cv_raw = pil_to_cv(pil_image)
|
| 62 |
+
|
| 63 |
+
# 2. Downscale for interactive preview speed if requested
|
| 64 |
+
if max_preview_dim is not None:
|
| 65 |
+
cv_img = resize_for_processing(cv_raw, max_preview_dim)
|
| 66 |
+
else:
|
| 67 |
+
cv_img = cv_raw.copy()
|
| 68 |
+
|
| 69 |
+
h, w = cv_img.shape[:2]
|
| 70 |
+
|
| 71 |
+
# 3. Bounding Box & Segmentation Determination
|
| 72 |
+
is_neural = (subject_mode in ["AI BiRefNet (SOTA General)", "AI U²-Net (Legacy Neural)", "AI Neural Network (U²-Net)"] and REMBG_AVAILABLE)
|
| 73 |
+
|
| 74 |
+
if is_neural:
|
| 75 |
+
try:
|
| 76 |
+
# Resolve correct model session
|
| 77 |
+
if "BiRefNet" in subject_mode:
|
| 78 |
+
session = get_rembg_session("birefnet-general")
|
| 79 |
+
else:
|
| 80 |
+
session = get_rembg_session("u2net")
|
| 81 |
+
|
| 82 |
+
if max_preview_dim is not None:
|
| 83 |
+
w_p, h_p = cv_img.shape[1], cv_img.shape[0]
|
| 84 |
+
pil_preview = pil_image.resize((w_p, h_p), Image.Resampling.LANCZOS)
|
| 85 |
+
cutout_pil = rembg_remove(pil_preview, session=session)
|
| 86 |
+
else:
|
| 87 |
+
cutout_pil = rembg_remove(pil_image, session=session)
|
| 88 |
+
|
| 89 |
+
cv_cutout = pil_to_cv(cutout_pil)
|
| 90 |
+
refined_mask = cv_cutout[:, :, 3].copy()
|
| 91 |
+
|
| 92 |
+
# Resilient shape normalization to handle EXIF or transpose mismatches from rembg
|
| 93 |
+
if refined_mask.shape[:2] != (h, w):
|
| 94 |
+
if refined_mask.shape[0] == w and refined_mask.shape[1] == h:
|
| 95 |
+
refined_mask = refined_mask.T
|
| 96 |
+
else:
|
| 97 |
+
refined_mask = cv2.resize(refined_mask, (w, h), interpolation=cv2.INTER_LINEAR)
|
| 98 |
+
|
| 99 |
+
actual_rect = (0, 0, w, h)
|
| 100 |
+
except Exception as e:
|
| 101 |
+
is_neural = False
|
| 102 |
+
|
| 103 |
+
if not is_neural:
|
| 104 |
+
if subject_mode == "Signature & Text (Ink)":
|
| 105 |
+
pw = max(2, min(20, w // 20))
|
| 106 |
+
ph = max(2, min(20, h // 20))
|
| 107 |
+
c_tl = np.mean(cv_img[0:ph, 0:pw, :3], axis=(0, 1))
|
| 108 |
+
c_tr = np.mean(cv_img[0:ph, w-pw:w, :3], axis=(0, 1))
|
| 109 |
+
c_bg = (c_tl + c_tr) / 2.0
|
| 110 |
+
|
| 111 |
+
dist = np.sqrt(np.sum((cv_img[:, :, :3] - c_bg) ** 2, axis=2))
|
| 112 |
+
|
| 113 |
+
low_t = 15.0
|
| 114 |
+
high_t = 45.0
|
| 115 |
+
alpha = np.clip((dist - low_t) / (high_t - low_t) * 255.0, 0, 255).astype(np.uint8)
|
| 116 |
+
|
| 117 |
+
refined_mask = alpha
|
| 118 |
+
if closing_size > 0:
|
| 119 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (closing_size, closing_size))
|
| 120 |
+
refined_mask = cv2.morphologyEx(refined_mask, cv2.MORPH_CLOSE, kernel)
|
| 121 |
+
|
| 122 |
+
actual_rect = (0, 0, w, h)
|
| 123 |
+
else:
|
| 124 |
+
if rect is None:
|
| 125 |
+
actual_rect = detect_automatic_bounding_box(cv_img, margin_percentage)
|
| 126 |
+
else:
|
| 127 |
+
if max_preview_dim is not None:
|
| 128 |
+
orig_h, orig_w = cv_raw.shape[:2]
|
| 129 |
+
scale_x = w / orig_w
|
| 130 |
+
scale_y = h / orig_h
|
| 131 |
+
rx, ry, rw, rh = rect
|
| 132 |
+
actual_rect = (
|
| 133 |
+
int(rx * scale_x),
|
| 134 |
+
int(ry * scale_y),
|
| 135 |
+
int(rw * scale_x),
|
| 136 |
+
int(rh * scale_y)
|
| 137 |
+
)
|
| 138 |
+
else:
|
| 139 |
+
actual_rect = rect
|
| 140 |
+
|
| 141 |
+
raw_mask = run_grabcut(cv_img, actual_rect, iter_count, bg_seed_sensitivity=bg_seed_sensitivity)
|
| 142 |
+
|
| 143 |
+
refined_mask = refine_mask(
|
| 144 |
+
mask=raw_mask,
|
| 145 |
+
img=cv_img,
|
| 146 |
+
closing_size=closing_size,
|
| 147 |
+
keep_largest_only=keep_largest_only,
|
| 148 |
+
feather_radius=feather_radius,
|
| 149 |
+
matting_enabled=matting_enabled,
|
| 150 |
+
matting_radius=matting_radius,
|
| 151 |
+
matting_eps=matting_eps
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
# 6. Generate Transparent PNG Cutout
|
| 155 |
+
cutout = np.zeros((h, w, 4), dtype=np.uint8)
|
| 156 |
+
cutout[:, :, :3] = cv_img[:, :, :3]
|
| 157 |
+
cutout[:, :, 3] = refined_mask
|
| 158 |
+
|
| 159 |
+
# 7. Generate Drop Shadow Composite
|
| 160 |
+
if shadow_enabled:
|
| 161 |
+
shadow_composite = generate_drop_shadow(
|
| 162 |
+
cv_img,
|
| 163 |
+
refined_mask,
|
| 164 |
+
opacity=shadow_opacity,
|
| 165 |
+
blur_radius=shadow_blur,
|
| 166 |
+
distance=shadow_distance,
|
| 167 |
+
angle_degrees=shadow_angle
|
| 168 |
+
)
|
| 169 |
+
else:
|
| 170 |
+
shadow_composite = cutout
|
| 171 |
+
|
| 172 |
+
# 8. Scale Bounding Box back to original coords if resized (for UI display overlay)
|
| 173 |
+
if max_preview_dim is not None:
|
| 174 |
+
orig_h, orig_w = cv_raw.shape[:2]
|
| 175 |
+
scale_x = orig_w / w
|
| 176 |
+
scale_y = orig_h / h
|
| 177 |
+
ax, ay, aw, ah = actual_rect
|
| 178 |
+
rect_out = (
|
| 179 |
+
int(ax * scale_x),
|
| 180 |
+
int(ay * scale_y),
|
| 181 |
+
int(aw * scale_x),
|
| 182 |
+
int(ah * scale_y)
|
| 183 |
+
)
|
| 184 |
+
else:
|
| 185 |
+
rect_out = actual_rect
|
| 186 |
+
|
| 187 |
+
# 9. Convert outputs back to PIL
|
| 188 |
+
return {
|
| 189 |
+
"original": cv_to_pil(cv_img),
|
| 190 |
+
"mask": Image.fromarray(refined_mask).convert("L"),
|
| 191 |
+
"transparent": cv_to_pil(cutout),
|
| 192 |
+
"shadow": cv_to_pil(shadow_composite),
|
| 193 |
+
"rect": rect_out
|
| 194 |
+
}
|
backend/bg_remover/segmentation.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
def detect_automatic_bounding_box(img: np.ndarray, margin_percentage: float = 5.0) -> tuple:
|
| 5 |
+
"""
|
| 6 |
+
Detect the main subject's bounding box using edge density and contour analysis.
|
| 7 |
+
Filters out full-frame border contours.
|
| 8 |
+
"""
|
| 9 |
+
h, w = img.shape[:2]
|
| 10 |
+
|
| 11 |
+
# 1. Preprocess: Convert to grayscale and blur
|
| 12 |
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| 13 |
+
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
|
| 14 |
+
|
| 15 |
+
# 2. Compute edge density using Canny
|
| 16 |
+
edges = cv2.Canny(blurred, 30, 100)
|
| 17 |
+
|
| 18 |
+
# 3. Morphological closing to join close edge segments
|
| 19 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
|
| 20 |
+
closed = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
|
| 21 |
+
dilated = cv2.dilate(closed, kernel, iterations=2)
|
| 22 |
+
|
| 23 |
+
# 4. Find contours of the edge-dense regions
|
| 24 |
+
contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
| 25 |
+
|
| 26 |
+
# If contours exist, find the bounding box of the largest ones
|
| 27 |
+
if contours:
|
| 28 |
+
# Filter contours by area and ignore frame border contours that cover the entire image
|
| 29 |
+
valid_contours = []
|
| 30 |
+
for c in contours:
|
| 31 |
+
cx, cy, ccw, cch = cv2.boundingRect(c)
|
| 32 |
+
if ccw >= w - 10 and cch >= h - 10:
|
| 33 |
+
continue
|
| 34 |
+
if cv2.contourArea(c) > (w * h * 0.002):
|
| 35 |
+
valid_contours.append(c)
|
| 36 |
+
|
| 37 |
+
# Sort valid contours by area descending
|
| 38 |
+
valid_contours = sorted(valid_contours, key=cv2.contourArea, reverse=True)
|
| 39 |
+
|
| 40 |
+
if valid_contours:
|
| 41 |
+
# Get the unified bounding box of the significant contours
|
| 42 |
+
x_min, y_min = w, h
|
| 43 |
+
x_max, y_max = 0, 0
|
| 44 |
+
for c in valid_contours[:3]: # Top 3 largest contours
|
| 45 |
+
x, y, cw, ch = cv2.boundingRect(c)
|
| 46 |
+
x_min = min(x_min, x)
|
| 47 |
+
y_min = min(y_min, y)
|
| 48 |
+
x_max = max(x_max, x + cw)
|
| 49 |
+
y_max = max(y_max, y + ch)
|
| 50 |
+
|
| 51 |
+
# Add a slight padding/margin
|
| 52 |
+
pad_x = int((x_max - x_min) * 0.05)
|
| 53 |
+
pad_y = int((y_max - y_min) * 0.05)
|
| 54 |
+
|
| 55 |
+
x = max(0, x_min - pad_x)
|
| 56 |
+
y = max(0, y_min - pad_y)
|
| 57 |
+
cw = min(w - x, (x_max - x_min) + 2 * pad_x)
|
| 58 |
+
ch = min(h - y, (y_max - y_min) + 2 * pad_y)
|
| 59 |
+
|
| 60 |
+
# Ensure it is valid
|
| 61 |
+
if cw > 10 and ch > 10:
|
| 62 |
+
return (x, y, cw, ch)
|
| 63 |
+
|
| 64 |
+
# Fallback: Center-based bounding box with specified margin
|
| 65 |
+
margin_w = int(w * (margin_percentage / 100.0))
|
| 66 |
+
margin_h = int(h * (margin_percentage / 100.0))
|
| 67 |
+
|
| 68 |
+
x = margin_w
|
| 69 |
+
y = margin_h
|
| 70 |
+
cw = w - (2 * margin_w)
|
| 71 |
+
ch = h - (2 * margin_h)
|
| 72 |
+
|
| 73 |
+
return (x, y, cw, ch)
|
| 74 |
+
|
| 75 |
+
def run_grabcut(img: np.ndarray, rect: tuple, iter_count: int = 5, bg_seed_sensitivity: float = 35.0) -> np.ndarray:
|
| 76 |
+
"""
|
| 77 |
+
Run GrabCut algorithm on BGR image using the provided bounding box.
|
| 78 |
+
Optionally initializes the GrabCut mask with background seeds from the top corners.
|
| 79 |
+
"""
|
| 80 |
+
h, w = img.shape[:2]
|
| 81 |
+
|
| 82 |
+
# Ensure rect is valid and within image boundaries
|
| 83 |
+
rx, ry, rw, rh = rect
|
| 84 |
+
rx = max(0, min(rx, w - 2))
|
| 85 |
+
ry = max(0, min(ry, h - 2))
|
| 86 |
+
rw = max(1, min(rw, w - rx))
|
| 87 |
+
rh = max(1, min(rh, h - ry))
|
| 88 |
+
safe_rect = (rx, ry, rw, rh)
|
| 89 |
+
|
| 90 |
+
# Initialize GrabCut background/foreground models
|
| 91 |
+
bgd_model = np.zeros((1, 65), dtype=np.float64)
|
| 92 |
+
fgd_model = np.zeros((1, 65), dtype=np.float64)
|
| 93 |
+
|
| 94 |
+
# Create GrabCut mask
|
| 95 |
+
mask = np.zeros((h, w), dtype=np.uint8)
|
| 96 |
+
|
| 97 |
+
try:
|
| 98 |
+
if bg_seed_sensitivity > 0.0 and len(img.shape) == 3:
|
| 99 |
+
# 1. Initialize entire mask inside the bounding box as Probable Foreground (3)
|
| 100 |
+
# and outside as Sure Background (0)
|
| 101 |
+
cv2.rectangle(mask, (rx, ry), (rx + rw, ry + rh), cv2.GC_PR_FGD, -1)
|
| 102 |
+
|
| 103 |
+
# 2. Sample only top corner regions (Top-Left and Top-Right) to detect background color
|
| 104 |
+
pw = max(2, min(20, w // 20))
|
| 105 |
+
ph = max(2, min(20, h // 20))
|
| 106 |
+
|
| 107 |
+
# Extract corner colors (BGR channels average)
|
| 108 |
+
c_tl = np.mean(img[0:ph, 0:pw, :3], axis=(0, 1))
|
| 109 |
+
c_tr = np.mean(img[0:ph, w-pw:w, :3], axis=(0, 1))
|
| 110 |
+
|
| 111 |
+
# 3. For each pixel, compute distance to nearest corner color
|
| 112 |
+
diff_tl = np.sqrt(np.sum((img[:, :, :3] - c_tl) ** 2, axis=2))
|
| 113 |
+
diff_tr = np.sqrt(np.sum((img[:, :, :3] - c_tr) ** 2, axis=2))
|
| 114 |
+
|
| 115 |
+
min_diff = np.minimum(diff_tl, diff_tr)
|
| 116 |
+
|
| 117 |
+
# 4. Mark pixels inside safe_rect that are very close to corner colors as Probable Background (2)
|
| 118 |
+
bg_mask = (min_diff < bg_seed_sensitivity)
|
| 119 |
+
bbox_mask = np.zeros((h, w), dtype=bool)
|
| 120 |
+
bbox_mask[ry:ry+rh, rx:rx+rw] = True
|
| 121 |
+
|
| 122 |
+
# Final probable background assignments
|
| 123 |
+
mask[bbox_mask & bg_mask] = cv2.GC_PR_BGD
|
| 124 |
+
|
| 125 |
+
# Run GrabCut in MASK mode
|
| 126 |
+
cv2.grabCut(img, mask, safe_rect, bgd_model, fgd_model, iter_count, cv2.GC_INIT_WITH_MASK)
|
| 127 |
+
else:
|
| 128 |
+
# Traditional RECT-only initialization
|
| 129 |
+
cv2.grabCut(img, mask, safe_rect, bgd_model, fgd_model, iter_count, cv2.GC_INIT_WITH_RECT)
|
| 130 |
+
|
| 131 |
+
# Convert output mask to binary
|
| 132 |
+
binary_mask = np.where((mask == cv2.GC_FGD) | (mask == cv2.GC_PR_FGD), 255, 0).astype(np.uint8)
|
| 133 |
+
return binary_mask
|
| 134 |
+
except Exception as e:
|
| 135 |
+
fallback_mask = np.zeros((h, w), dtype=np.uint8)
|
| 136 |
+
cv2.rectangle(fallback_mask, (rx, ry), (rx + rw, ry + rh), 255, -1)
|
| 137 |
+
return fallback_mask
|
backend/bg_remover/shadow_generator.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
import math
|
| 4 |
+
|
| 5 |
+
def blend_rgba(top: np.ndarray, bottom: np.ndarray) -> np.ndarray:
|
| 6 |
+
"""
|
| 7 |
+
Perform alpha blending of an RGBA image 'top' over an RGBA image 'bottom'.
|
| 8 |
+
"""
|
| 9 |
+
top_a = top[:, :, 3:4].astype(float) / 255.0
|
| 10 |
+
bottom_a = bottom[:, :, 3:4].astype(float) / 255.0
|
| 11 |
+
|
| 12 |
+
out_a = top_a + bottom_a * (1.0 - top_a)
|
| 13 |
+
out_a_safe = np.where(out_a == 0, 1.0, out_a)
|
| 14 |
+
|
| 15 |
+
top_rgb = top[:, :, :3].astype(float)
|
| 16 |
+
bottom_rgb = bottom[:, :, :3].astype(float)
|
| 17 |
+
|
| 18 |
+
out_rgb = (top_rgb * top_a + bottom_rgb * bottom_a * (1.0 - top_a)) / out_a_safe
|
| 19 |
+
|
| 20 |
+
out_img = np.zeros_like(top)
|
| 21 |
+
out_img[:, :, :3] = np.clip(out_rgb, 0, 255).astype(np.uint8)
|
| 22 |
+
out_img[:, :, 3] = np.clip(out_a * 255, 0, 255).astype(np.uint8)[:, :, 0]
|
| 23 |
+
|
| 24 |
+
return out_img
|
| 25 |
+
|
| 26 |
+
def generate_drop_shadow(img: np.ndarray,
|
| 27 |
+
mask: np.ndarray,
|
| 28 |
+
opacity: float = 0.5,
|
| 29 |
+
blur_radius: int = 15,
|
| 30 |
+
distance: int = 20,
|
| 31 |
+
angle_degrees: float = 45.0) -> np.ndarray:
|
| 32 |
+
"""
|
| 33 |
+
Generate a realistic drop shadow layer behind the foreground cutout.
|
| 34 |
+
"""
|
| 35 |
+
h, w = mask.shape[:2]
|
| 36 |
+
|
| 37 |
+
cutout = np.zeros((h, w, 4), dtype=np.uint8)
|
| 38 |
+
if img.shape[2] == 4:
|
| 39 |
+
cutout[:, :, :3] = img[:, :, :3]
|
| 40 |
+
else:
|
| 41 |
+
cutout[:, :, :3] = img
|
| 42 |
+
cutout[:, :, 3] = mask
|
| 43 |
+
|
| 44 |
+
if opacity <= 0.0 or (distance <= 0 and blur_radius <= 0):
|
| 45 |
+
return cutout
|
| 46 |
+
|
| 47 |
+
shadow = np.zeros((h, w, 4), dtype=np.uint8)
|
| 48 |
+
shadow[:, :, 3] = mask
|
| 49 |
+
|
| 50 |
+
angle_radians = math.radians(angle_degrees)
|
| 51 |
+
dx = int(distance * math.cos(angle_radians))
|
| 52 |
+
dy = int(distance * math.sin(angle_radians))
|
| 53 |
+
|
| 54 |
+
M = np.float32([[1, 0, dx], [0, 1, dy]])
|
| 55 |
+
translated_shadow = cv2.warpAffine(
|
| 56 |
+
shadow, M, (w, h),
|
| 57 |
+
borderMode=cv2.BORDER_CONSTANT,
|
| 58 |
+
borderValue=(0, 0, 0, 0)
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
if blur_radius > 0:
|
| 62 |
+
ksize = 2 * blur_radius + 1
|
| 63 |
+
translated_shadow = cv2.GaussianBlur(translated_shadow, (ksize, ksize), 0)
|
| 64 |
+
|
| 65 |
+
translated_shadow[:, :, 3] = (translated_shadow[:, :, 3] * opacity).astype(np.uint8)
|
| 66 |
+
|
| 67 |
+
final_composite = blend_rgba(top=cutout, bottom=translated_shadow)
|
| 68 |
+
return final_composite
|
backend/dslr_blur/__pycache__/blur_processor.cpython-311.pyc
ADDED
|
Binary file (8.49 kB). View file
|
|
|
backend/dslr_blur/__pycache__/depth_blur.cpython-311.pyc
ADDED
|
Binary file (8.28 kB). View file
|
|
|
backend/dslr_blur/blur_processor.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from backend.utilities import pil_to_cv, cv_to_pil
|
| 5 |
+
|
| 6 |
+
class DSLRBlurProcessor:
|
| 7 |
+
"""
|
| 8 |
+
Orchestrator class to execute the DSLR Background Blur image processing pipeline.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
@staticmethod
|
| 12 |
+
def apply_feathering(mask: np.ndarray, radius: int) -> np.ndarray:
|
| 13 |
+
"""
|
| 14 |
+
Feathers the binary mask to create a soft, anti-aliased edge transition.
|
| 15 |
+
Returns a float32 mask scaled between 0.0 and 1.0.
|
| 16 |
+
"""
|
| 17 |
+
if radius <= 0:
|
| 18 |
+
return mask.astype(np.float32) / 255.0
|
| 19 |
+
|
| 20 |
+
# Ensure kernel size is odd
|
| 21 |
+
k_size = radius * 2 + 1
|
| 22 |
+
feathered = cv2.GaussianBlur(mask, (k_size, k_size), 0)
|
| 23 |
+
return feathered.astype(np.float32) / 255.0
|
| 24 |
+
|
| 25 |
+
@staticmethod
|
| 26 |
+
def inpaint_background(img: np.ndarray, mask: np.ndarray) -> np.ndarray:
|
| 27 |
+
"""
|
| 28 |
+
Inpaints/erases the foreground subject out of the background.
|
| 29 |
+
Uses a highly optimized downscaled inpainting approach to prevent color bleeding
|
| 30 |
+
and edge-halos when the background gets blurred.
|
| 31 |
+
"""
|
| 32 |
+
h, w = img.shape[:2]
|
| 33 |
+
|
| 34 |
+
# 1. Dilate the mask by 15px to fully cover edge transition and anti-aliasing zones
|
| 35 |
+
kernel_size = 15
|
| 36 |
+
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
|
| 37 |
+
dilated_mask = cv2.dilate(mask, kernel, iterations=1)
|
| 38 |
+
|
| 39 |
+
# 2. Downscale the image and mask to 25% size for lightning-fast inpainting
|
| 40 |
+
scale = 0.25
|
| 41 |
+
down_w = int(w * scale)
|
| 42 |
+
down_h = int(h * scale)
|
| 43 |
+
|
| 44 |
+
img_small = cv2.resize(img, (down_w, down_h), interpolation=cv2.INTER_AREA)
|
| 45 |
+
mask_small = cv2.resize(dilated_mask, (down_w, down_h), interpolation=cv2.INTER_NEAREST)
|
| 46 |
+
|
| 47 |
+
# 3. Perform Fast Telea inpainting on the downscaled image
|
| 48 |
+
inpainted_small = cv2.inpaint(img_small, mask_small, 5, cv2.INPAINT_TELEA)
|
| 49 |
+
|
| 50 |
+
# 4. Upscale back to the original image dimensions
|
| 51 |
+
inpainted = cv2.resize(inpainted_small, (w, h), interpolation=cv2.INTER_CUBIC)
|
| 52 |
+
|
| 53 |
+
# 5. Composite back the real background pixels (keeping inpainted pixels only under dilated mask)
|
| 54 |
+
bg_only = img.copy()
|
| 55 |
+
mask_indices = dilated_mask > 0
|
| 56 |
+
bg_only[mask_indices] = inpainted[mask_indices]
|
| 57 |
+
|
| 58 |
+
return bg_only
|
| 59 |
+
|
| 60 |
+
@staticmethod
|
| 61 |
+
def apply_blur(img: np.ndarray, mode: str, strength: float, smoothness: float) -> np.ndarray:
|
| 62 |
+
"""
|
| 63 |
+
Applies a natural, aesthetically pleasing blur to the background.
|
| 64 |
+
Supports:
|
| 65 |
+
- "Gaussian Blur (Soft & Smooth)"
|
| 66 |
+
- "Lens Blur / Circular Bokeh (Realistic DSLR)"
|
| 67 |
+
"""
|
| 68 |
+
# Map strength (1 - 100) to actual kernel/radius dimensions
|
| 69 |
+
# For Gaussian: map to odd numbers from 3 to 101
|
| 70 |
+
g_strength = int(strength / 100.0 * 50.0) * 2 + 1
|
| 71 |
+
g_strength = max(3, g_strength)
|
| 72 |
+
|
| 73 |
+
# For Circular Bokeh: map circular kernel diameter from 3 to 61
|
| 74 |
+
l_diameter = int(strength / 100.0 * 30.0) * 2 + 1
|
| 75 |
+
l_diameter = max(3, l_diameter)
|
| 76 |
+
|
| 77 |
+
if mode == "Lens Blur / Circular Bokeh (Realistic DSLR)":
|
| 78 |
+
# Create a flat circular convolution kernel representing lens aperture
|
| 79 |
+
kernel = np.zeros((l_diameter, l_diameter), dtype=np.float32)
|
| 80 |
+
cv2.circle(kernel, (l_diameter // 2, l_diameter // 2), l_diameter // 2, 1, -1)
|
| 81 |
+
|
| 82 |
+
# Normalize the kernel
|
| 83 |
+
kernel_sum = np.sum(kernel)
|
| 84 |
+
if kernel_sum > 0:
|
| 85 |
+
kernel /= kernel_sum
|
| 86 |
+
else:
|
| 87 |
+
kernel[l_diameter // 2, l_diameter // 2] = 1.0
|
| 88 |
+
|
| 89 |
+
# Convolve background to form circular bokeh discs
|
| 90 |
+
blurred = cv2.filter2D(img, -1, kernel)
|
| 91 |
+
else:
|
| 92 |
+
# Gaussian Blur
|
| 93 |
+
blurred = cv2.GaussianBlur(img, (g_strength, g_strength), 0)
|
| 94 |
+
|
| 95 |
+
# Bilateral filter post-smoothing for a creamy, noise-free studio look
|
| 96 |
+
if smoothness > 0:
|
| 97 |
+
d = int(smoothness / 100.0 * 15)
|
| 98 |
+
d = max(3, d | 1) # must be odd
|
| 99 |
+
sigma_color = smoothness / 100.0 * 150.0
|
| 100 |
+
sigma_space = smoothness / 100.0 * 150.0
|
| 101 |
+
blurred = cv2.bilateralFilter(blurred, d, sigma_color, sigma_space)
|
| 102 |
+
|
| 103 |
+
return blurred
|
| 104 |
+
|
| 105 |
+
@staticmethod
|
| 106 |
+
def composite_layers(
|
| 107 |
+
fg_img: np.ndarray,
|
| 108 |
+
bg_img: np.ndarray,
|
| 109 |
+
alpha: np.ndarray,
|
| 110 |
+
subject_protection: float
|
| 111 |
+
) -> np.ndarray:
|
| 112 |
+
"""
|
| 113 |
+
Composites the sharp foreground subject over the blurred background.
|
| 114 |
+
alpha: float32 grayscale feathered mask in range [0, 1.0]. Shape is (H, W).
|
| 115 |
+
subject_protection: float (0 - 100) -> Protects original fine details.
|
| 116 |
+
"""
|
| 117 |
+
# Expand alpha to 3 channels for RGB broadcasting
|
| 118 |
+
alpha_3d = np.expand_dims(alpha, axis=2)
|
| 119 |
+
|
| 120 |
+
# Subject Protection clamps the minimum alpha of subject pixels to prevent them blurring
|
| 121 |
+
if subject_protection > 0:
|
| 122 |
+
protection_factor = subject_protection / 100.0
|
| 123 |
+
mask_fg = alpha > 0.05
|
| 124 |
+
alpha_3d[mask_fg] = np.maximum(alpha_3d[mask_fg], protection_factor)
|
| 125 |
+
|
| 126 |
+
# Alpha blend: out = fg * alpha + bg * (1 - alpha)
|
| 127 |
+
composited = fg_img.astype(np.float32) * alpha_3d + bg_img.astype(np.float32) * (1.0 - alpha_3d)
|
| 128 |
+
return np.clip(composited, 0, 255).astype(np.uint8)
|
| 129 |
+
|
| 130 |
+
@classmethod
|
| 131 |
+
def process_dslr_blur(
|
| 132 |
+
cls,
|
| 133 |
+
pil_image: Image.Image,
|
| 134 |
+
mask_pil: Image.Image,
|
| 135 |
+
blur_mode: str = "Lens Blur / Circular Bokeh (Realistic DSLR)",
|
| 136 |
+
blur_strength: float = 30.0,
|
| 137 |
+
edge_feathering: int = 5,
|
| 138 |
+
subject_protection: float = 80.0,
|
| 139 |
+
background_smoothness: float = 30.0
|
| 140 |
+
) -> Image.Image:
|
| 141 |
+
"""
|
| 142 |
+
Main entry point to execute the DSLR Background Blur pipeline.
|
| 143 |
+
"""
|
| 144 |
+
# 1. Convert to CV BGR/BGRA arrays
|
| 145 |
+
cv_img = pil_to_cv(pil_image)
|
| 146 |
+
mask = np.array(mask_pil.convert("L"))
|
| 147 |
+
|
| 148 |
+
# Ensure matching shapes
|
| 149 |
+
h, w = cv_img.shape[:2]
|
| 150 |
+
if mask.shape[:2] != (h, w):
|
| 151 |
+
mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)
|
| 152 |
+
|
| 153 |
+
# 2. Feather the mask to create anti-aliased subject edges
|
| 154 |
+
alpha = cls.apply_feathering(mask, edge_feathering)
|
| 155 |
+
|
| 156 |
+
# 3. Inpaint the background to erase the subject and prevent colored edge halos/bleeding
|
| 157 |
+
bg_inpainted = cls.inpaint_background(cv_img[:, :, :3], mask)
|
| 158 |
+
|
| 159 |
+
# 4. Apply Gaussian or circular lens bokeh blur to the background
|
| 160 |
+
bg_blurred = cls.apply_blur(bg_inpainted, blur_mode, blur_strength, background_smoothness)
|
| 161 |
+
|
| 162 |
+
# 5. Composite original sharp subject over the blurred background using feathered alpha
|
| 163 |
+
result_cv = cls.composite_layers(cv_img[:, :, :3], bg_blurred, alpha, subject_protection)
|
| 164 |
+
|
| 165 |
+
# 6. Re-apply alpha channel if original image was RGBA
|
| 166 |
+
if cv_img.shape[2] == 4:
|
| 167 |
+
result_rgba = np.zeros((h, w, 4), dtype=np.uint8)
|
| 168 |
+
result_rgba[:, :, :3] = result_cv
|
| 169 |
+
result_rgba[:, :, 3] = cv_img[:, :, 3]
|
| 170 |
+
return cv_to_pil(result_rgba)
|
| 171 |
+
else:
|
| 172 |
+
return cv_to_pil(result_cv)
|
backend/dslr_blur/depth_blur.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from backend.utilities import pil_to_cv, cv_to_pil
|
| 5 |
+
from backend.bg_remover.image_processor import ImageProcessor
|
| 6 |
+
from backend.dslr_blur.blur_processor import DSLRBlurProcessor
|
| 7 |
+
from backend.enhancement.onnx_engine import ONNXInferenceEngine
|
| 8 |
+
|
| 9 |
+
class DepthBlurEngine:
|
| 10 |
+
"""
|
| 11 |
+
Fuses Depth Anything V2 depth maps and BiRefNet masks to execute
|
| 12 |
+
spatially-varying, optical DSLR depth-of-field blurs.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
@classmethod
|
| 16 |
+
def generate_synthetic_depth_map(cls, h: int, w: int, mask: np.ndarray) -> np.ndarray:
|
| 17 |
+
"""
|
| 18 |
+
Fallback generator that constructs a smooth vertical linear perspective depth map
|
| 19 |
+
in case the neural ONNX model is offline or unavailable.
|
| 20 |
+
"""
|
| 21 |
+
# Create vertical gradient: 0 at top (infinity), 255 at bottom (foreground ground plane)
|
| 22 |
+
y, x = np.indices((h, w))
|
| 23 |
+
depth = (y / h * 255.0).astype(np.uint8)
|
| 24 |
+
|
| 25 |
+
# Clamp subject pixels to foreground (240) to keep the focus plane sharp
|
| 26 |
+
depth[mask > 127] = 230
|
| 27 |
+
return depth
|
| 28 |
+
|
| 29 |
+
@classmethod
|
| 30 |
+
def run_depth_estimation(cls, cv_img: np.ndarray, mask: np.ndarray) -> np.ndarray:
|
| 31 |
+
"""
|
| 32 |
+
Attempts to run Depth Anything V2 neural depth estimation with a graceful
|
| 33 |
+
vertical linear perspective fallback on network/load failures.
|
| 34 |
+
"""
|
| 35 |
+
h, w = cv_img.shape[:2]
|
| 36 |
+
try:
|
| 37 |
+
# Try running ONNX Depth Anything session
|
| 38 |
+
depth_map = ONNXInferenceEngine.run_depth_anything(cv_img[:, :, :3])
|
| 39 |
+
return depth_map
|
| 40 |
+
except Exception as e:
|
| 41 |
+
print(f"[Depth Engine] Depth Anything V2 failed: {e}. Generating synthetic linear depth gradient.")
|
| 42 |
+
return cls.generate_synthetic_depth_map(h, w, mask)
|
| 43 |
+
|
| 44 |
+
@classmethod
|
| 45 |
+
def process_depth_blur(
|
| 46 |
+
cls,
|
| 47 |
+
pil_image: Image.Image,
|
| 48 |
+
mask_pil: Image.Image,
|
| 49 |
+
blur_mode: str = "Lens Blur / Circular Bokeh (Realistic DSLR)",
|
| 50 |
+
blur_preset: str = "DSLR 85mm",
|
| 51 |
+
blur_strength: float = 45.0,
|
| 52 |
+
edge_feathering: int = 5,
|
| 53 |
+
subject_protection: float = 85.0,
|
| 54 |
+
background_smoothness: float = 30.0
|
| 55 |
+
) -> dict:
|
| 56 |
+
"""
|
| 57 |
+
Executes the complete spatially-varying optical depth blur pipeline.
|
| 58 |
+
Returns a dictionary containing:
|
| 59 |
+
- "result": PIL composite image with depth blur
|
| 60 |
+
- "depth_map": PIL depth map image (for visual developer previews)
|
| 61 |
+
"""
|
| 62 |
+
# 1. Convert formats
|
| 63 |
+
cv_img = pil_to_cv(pil_image)
|
| 64 |
+
mask = np.array(mask_pil.convert("L"))
|
| 65 |
+
h, w = cv_img.shape[:2]
|
| 66 |
+
|
| 67 |
+
# Ensure matching shapes
|
| 68 |
+
if mask.shape[:2] != (h, w):
|
| 69 |
+
mask = cv2.resize(mask, (w, h), interpolation=cv2.INTER_NEAREST)
|
| 70 |
+
|
| 71 |
+
# 2. Pre-generate or run depth estimation
|
| 72 |
+
depth = cls.run_depth_estimation(cv_img, mask)
|
| 73 |
+
|
| 74 |
+
# 3. Soft-feather the mask
|
| 75 |
+
alpha = DSLRBlurProcessor.apply_feathering(mask, edge_feathering)
|
| 76 |
+
|
| 77 |
+
# 4. Calibrate Focus Plane Depth (D_subject) to the isolated subject
|
| 78 |
+
subject_pixels = depth[mask > 127]
|
| 79 |
+
if len(subject_pixels) > 0:
|
| 80 |
+
D_subject = float(np.median(subject_pixels))
|
| 81 |
+
else:
|
| 82 |
+
D_subject = 220.0 # Default foreground depth focus
|
| 83 |
+
|
| 84 |
+
# 5. Map Preset parameters to Max Blur Strength
|
| 85 |
+
# Presets: Portrait, DSLR 50mm, DSLR 85mm, Studio, Cinematic, Custom
|
| 86 |
+
preset_max_blur = {
|
| 87 |
+
"Portrait": 25.0,
|
| 88 |
+
"DSLR 50mm": 40.0,
|
| 89 |
+
"DSLR 85mm": 55.0,
|
| 90 |
+
"Studio": 20.0,
|
| 91 |
+
"Cinematic": 75.0,
|
| 92 |
+
"Custom": blur_strength
|
| 93 |
+
}
|
| 94 |
+
max_blur = preset_max_blur.get(blur_preset, blur_strength)
|
| 95 |
+
|
| 96 |
+
# 6. Calculate Spatially-Varying Blur Radius map
|
| 97 |
+
# Dist from focus plane scaled [0.0, 1.0]
|
| 98 |
+
dist_from_focus = np.abs(depth.astype(np.float32) - D_subject) / 255.0
|
| 99 |
+
|
| 100 |
+
# Blur radius maps from 0 to max_blur (subject has 0 blur because of (1 - alpha))
|
| 101 |
+
radius_map = max_blur * (1.0 - alpha) * dist_from_focus
|
| 102 |
+
|
| 103 |
+
# 7. Reconstruct Background to prevent colored halos/subject bleeding
|
| 104 |
+
bg_inpainted = DSLRBlurProcessor.inpaint_background(cv_img[:, :, :3], mask)
|
| 105 |
+
|
| 106 |
+
# 8. Vectorized N-Layer Depth-of-Field Blending
|
| 107 |
+
# Generate N=6 discrete blur radius stops for linear interpolation
|
| 108 |
+
radii = [0, int(0.15 * max_blur), int(0.35 * max_blur), int(0.60 * max_blur), int(0.85 * max_blur), int(max_blur)]
|
| 109 |
+
# Filter duplicates and ensure they are sorted ascending
|
| 110 |
+
radii = sorted(list(set(radii)))
|
| 111 |
+
|
| 112 |
+
# Pre-compute N blurred background layers
|
| 113 |
+
blurred_layers = []
|
| 114 |
+
for r in radii:
|
| 115 |
+
if r == 0:
|
| 116 |
+
blurred_layers.append(bg_inpainted)
|
| 117 |
+
else:
|
| 118 |
+
# Ensure the radius maps to an odd number internally in apply_blur
|
| 119 |
+
blurred_layers.append(DSLRBlurProcessor.apply_blur(bg_inpainted, blur_mode, r, background_smoothness))
|
| 120 |
+
|
| 121 |
+
# Perform vectorized linear interpolation across depth intervals
|
| 122 |
+
composite_bg = np.zeros_like(cv_img[:, :, :3], dtype=np.float32)
|
| 123 |
+
|
| 124 |
+
for k in range(len(radii) - 1):
|
| 125 |
+
r_low = radii[k]
|
| 126 |
+
r_high = radii[k+1]
|
| 127 |
+
|
| 128 |
+
# Identify pixels whose target blur radius falls inside this interval
|
| 129 |
+
if k == len(radii) - 2:
|
| 130 |
+
interval_mask = (radius_map >= r_low) & (radius_map <= r_high)
|
| 131 |
+
else:
|
| 132 |
+
interval_mask = (radius_map >= r_low) & (radius_map < r_high)
|
| 133 |
+
|
| 134 |
+
if not np.any(interval_mask):
|
| 135 |
+
continue
|
| 136 |
+
|
| 137 |
+
# Grab target radii values in this interval
|
| 138 |
+
r_vals = radius_map[interval_mask]
|
| 139 |
+
|
| 140 |
+
# Linear interpolation weight: w = 1.0 at low_radius, 0.0 at high_radius
|
| 141 |
+
w_interp = (r_high - r_vals) / (r_high - r_low)
|
| 142 |
+
w_interp_3d = np.expand_dims(w_interp, axis=1) # shape (num_pixels, 1)
|
| 143 |
+
|
| 144 |
+
# Slice pixels from adjacent blurred layers
|
| 145 |
+
low_pixels = blurred_layers[k][interval_mask].astype(np.float32)
|
| 146 |
+
high_pixels = blurred_layers[k+1][interval_mask].astype(np.float32)
|
| 147 |
+
|
| 148 |
+
# Blend pixels and write back
|
| 149 |
+
blended_pixels = low_pixels * w_interp_3d + high_pixels * (1.0 - w_interp_3d)
|
| 150 |
+
composite_bg[interval_mask] = blended_pixels
|
| 151 |
+
|
| 152 |
+
# Clip to valid range and cast
|
| 153 |
+
bg_depth_blurred = np.clip(composite_bg, 0, 255).astype(np.uint8)
|
| 154 |
+
|
| 155 |
+
# 9. Composite the original sharp subject over this depth-blurred background
|
| 156 |
+
result_cv = DSLRBlurProcessor.composite_layers(cv_img[:, :, :3], bg_depth_blurred, alpha, subject_protection)
|
| 157 |
+
|
| 158 |
+
# 10. Re-apply alpha transparency if present
|
| 159 |
+
if cv_img.shape[2] == 4:
|
| 160 |
+
result_rgba = np.zeros((h, w, 4), dtype=np.uint8)
|
| 161 |
+
result_rgba[:, :, :3] = result_cv
|
| 162 |
+
result_rgba[:, :, 3] = cv_img[:, :, 3]
|
| 163 |
+
output_pil = cv_to_pil(result_rgba)
|
| 164 |
+
else:
|
| 165 |
+
output_pil = cv_to_pil(result_cv)
|
| 166 |
+
|
| 167 |
+
return {
|
| 168 |
+
"result": output_pil,
|
| 169 |
+
"depth_map": Image.fromarray(depth).convert("L")
|
| 170 |
+
}
|
backend/enhancement/__pycache__/onnx_engine.cpython-311.pyc
ADDED
|
Binary file (11.9 kB). View file
|
|
|
backend/enhancement/__pycache__/photo_enhancer.cpython-311.pyc
ADDED
|
Binary file (8.46 kB). View file
|
|
|
backend/enhancement/onnx_engine.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import cv2
|
| 3 |
+
import numpy as np
|
| 4 |
+
import urllib.request
|
| 5 |
+
from PIL import Image
|
| 6 |
+
|
| 7 |
+
class ONNXInferenceEngine:
|
| 8 |
+
"""
|
| 9 |
+
Handles downloading, caching, and inference of ONNX models for:
|
| 10 |
+
- Depth Anything V2 (Depth Estimation)
|
| 11 |
+
- GFPGAN v1.4 (Face Restoration)
|
| 12 |
+
- Real-ESRGAN x2 (Super Resolution & Artifact Removal)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
MODELS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "models")
|
| 16 |
+
|
| 17 |
+
MODEL_URLS = {
|
| 18 |
+
"depth_anything": "https://huggingface.co/onnx-community/depth-anything-v2-small/resolve/main/onnx/model.onnx", # 97 MB
|
| 19 |
+
"gfpgan": "https://huggingface.co/Gourieff/ReActor/resolve/main/models/facerestore_models/GFPGANv1.4.onnx", # 140 MB
|
| 20 |
+
"realesrgan": "https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x2.onnx" # 67 MB
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
@classmethod
|
| 24 |
+
def get_model_path(cls, model_key: str) -> str:
|
| 25 |
+
"""Gets local path to the model, downloading it if not present."""
|
| 26 |
+
os.makedirs(cls.MODELS_DIR, exist_ok=True)
|
| 27 |
+
filename = f"{model_key}.onnx"
|
| 28 |
+
dest_path = os.path.join(cls.MODELS_DIR, filename)
|
| 29 |
+
|
| 30 |
+
if os.path.exists(dest_path) and os.path.getsize(dest_path) > 10 * 1024 * 1024:
|
| 31 |
+
return dest_path
|
| 32 |
+
|
| 33 |
+
url = cls.MODEL_URLS[model_key]
|
| 34 |
+
temp_path = dest_path + ".tmp"
|
| 35 |
+
|
| 36 |
+
print(f"[ONNX Engine] Downloading {model_key} model weights (~{cls._get_size_str(model_key)})...")
|
| 37 |
+
try:
|
| 38 |
+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
| 39 |
+
with urllib.request.urlopen(req) as response, open(temp_path, "wb") as out_file:
|
| 40 |
+
block_size = 1024 * 1024 # 1 MB chunks
|
| 41 |
+
while True:
|
| 42 |
+
buffer = response.read(block_size)
|
| 43 |
+
if not buffer:
|
| 44 |
+
break
|
| 45 |
+
out_file.write(buffer)
|
| 46 |
+
os.replace(temp_path, dest_path)
|
| 47 |
+
print(f"[ONNX Engine] Successfully downloaded and cached {model_key} model.")
|
| 48 |
+
return dest_path
|
| 49 |
+
except Exception as e:
|
| 50 |
+
if os.path.exists(temp_path):
|
| 51 |
+
os.remove(temp_path)
|
| 52 |
+
print(f"[ONNX Engine] Failed to download {model_key}: {e}")
|
| 53 |
+
raise e
|
| 54 |
+
|
| 55 |
+
@staticmethod
|
| 56 |
+
def _get_size_str(model_key: str) -> str:
|
| 57 |
+
sizes = {"depth_anything": "97MB", "gfpgan": "140MB", "realesrgan": "67MB"}
|
| 58 |
+
return sizes.get(model_key, "Unknown")
|
| 59 |
+
|
| 60 |
+
@classmethod
|
| 61 |
+
def get_session(cls, model_key: str):
|
| 62 |
+
"""Initializes and returns an ONNX Runtime inference session."""
|
| 63 |
+
try:
|
| 64 |
+
import onnxruntime as ort
|
| 65 |
+
model_path = cls.get_model_path(model_key)
|
| 66 |
+
# Use CPU execution provider with optimized thread configurations
|
| 67 |
+
sess_options = ort.SessionOptions()
|
| 68 |
+
sess_options.intra_op_num_threads = 2
|
| 69 |
+
sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
|
| 70 |
+
session = ort.InferenceSession(model_path, sess_options, providers=["CPUExecutionProvider"])
|
| 71 |
+
return session
|
| 72 |
+
except Exception as e:
|
| 73 |
+
print(f"[ONNX Engine] Error loading {model_key} session: {e}")
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
@classmethod
|
| 77 |
+
def run_depth_anything(cls, cv_img: np.ndarray) -> np.ndarray:
|
| 78 |
+
"""
|
| 79 |
+
Executes Depth Anything V2 Small ONNX model to extract a relative depth map.
|
| 80 |
+
Input: cv_img (BGR numpy array). Output: Grayscale depth map normalized to [0, 255].
|
| 81 |
+
"""
|
| 82 |
+
session = cls.get_session("depth_anything")
|
| 83 |
+
if session is None:
|
| 84 |
+
raise RuntimeError("Depth Anything V2 ONNX session is not available.")
|
| 85 |
+
|
| 86 |
+
h, w = cv_img.shape[:2]
|
| 87 |
+
|
| 88 |
+
# 1. Preprocess: Resize to 518x518 (multiple of 14, model input standard)
|
| 89 |
+
input_size = 518
|
| 90 |
+
img_rgb = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB)
|
| 91 |
+
img_resized = cv2.resize(img_rgb, (input_size, input_size), interpolation=cv2.INTER_AREA)
|
| 92 |
+
|
| 93 |
+
# Normalize with ImageNet stats
|
| 94 |
+
img_float = img_resized.astype(np.float32) / 255.0
|
| 95 |
+
mean = np.array([0.485, 0.456, 0.406], dtype=np.float32)
|
| 96 |
+
std = np.array([0.229, 0.224, 0.225], dtype=np.float32)
|
| 97 |
+
img_norm = (img_float - mean) / std
|
| 98 |
+
|
| 99 |
+
# Transpose to CHW (3, 518, 518) and add batch dim
|
| 100 |
+
img_tensor = np.transpose(img_norm, (2, 0, 1))
|
| 101 |
+
img_tensor = np.expand_dims(img_tensor, axis=0)
|
| 102 |
+
|
| 103 |
+
# 2. Run Inference
|
| 104 |
+
inputs = {session.get_inputs()[0].name: img_tensor}
|
| 105 |
+
depth_out = session.run(None, inputs)[0]
|
| 106 |
+
|
| 107 |
+
# Depth output shape is (1, 1, 518, 518) or (1, 518, 518)
|
| 108 |
+
depth_map = np.squeeze(depth_out)
|
| 109 |
+
|
| 110 |
+
# 3. Postprocess: Min-Max normalize to [0, 255]
|
| 111 |
+
d_min, d_max = depth_map.min(), depth_map.max()
|
| 112 |
+
if d_max > d_min:
|
| 113 |
+
depth_map = (depth_map - d_min) / (d_max - d_min) * 255.0
|
| 114 |
+
else:
|
| 115 |
+
depth_map = np.zeros_like(depth_map)
|
| 116 |
+
|
| 117 |
+
depth_map = depth_map.astype(np.uint8)
|
| 118 |
+
|
| 119 |
+
# Resize back to original dimensions using bilinear interpolation
|
| 120 |
+
depth_original = cv2.resize(depth_map, (w, h), interpolation=cv2.INTER_LINEAR)
|
| 121 |
+
return depth_original
|
| 122 |
+
|
| 123 |
+
@classmethod
|
| 124 |
+
def run_gfpgan(cls, face_bgr: np.ndarray) -> np.ndarray:
|
| 125 |
+
"""
|
| 126 |
+
Executes GFPGAN v1.4 ONNX model on a cropped facial BGR image.
|
| 127 |
+
Input: face_bgr (BGR array, expected 512x512). Output: Restored BGR face.
|
| 128 |
+
"""
|
| 129 |
+
session = cls.get_session("gfpgan")
|
| 130 |
+
if session is None:
|
| 131 |
+
raise RuntimeError("GFPGAN ONNX session is not available.")
|
| 132 |
+
|
| 133 |
+
# 1. Preprocess: Resize to 512x512
|
| 134 |
+
face_resized = cv2.resize(face_bgr, (512, 512), interpolation=cv2.INTER_AREA)
|
| 135 |
+
face_rgb = cv2.cvtColor(face_resized, cv2.COLOR_BGR2RGB)
|
| 136 |
+
|
| 137 |
+
# Normalize to [-1.0, 1.0]
|
| 138 |
+
face_float = face_rgb.astype(np.float32)
|
| 139 |
+
face_norm = (face_float - 127.5) / 127.5
|
| 140 |
+
|
| 141 |
+
# Transpose to CHW and expand batch
|
| 142 |
+
face_tensor = np.transpose(face_norm, (2, 0, 1))
|
| 143 |
+
face_tensor = np.expand_dims(face_tensor, axis=0)
|
| 144 |
+
|
| 145 |
+
# 2. Run Inference
|
| 146 |
+
inputs = {session.get_inputs()[0].name: face_tensor}
|
| 147 |
+
restored_out = session.run(None, inputs)[0]
|
| 148 |
+
|
| 149 |
+
# 3. Postprocess: Scale back to [0, 255]
|
| 150 |
+
restored_face = np.squeeze(restored_out)
|
| 151 |
+
restored_face = np.transpose(restored_face, (1, 2, 0)) # CHW -> HWC
|
| 152 |
+
restored_face = np.clip((restored_face + 1.0) * 127.5, 0, 255).astype(np.uint8)
|
| 153 |
+
|
| 154 |
+
# Convert RGB back to BGR
|
| 155 |
+
restored_bgr = cv2.cvtColor(restored_face, cv2.COLOR_RGB2BGR)
|
| 156 |
+
return restored_bgr
|
| 157 |
+
|
| 158 |
+
@classmethod
|
| 159 |
+
def run_realesrgan(cls, cv_img: np.ndarray) -> np.ndarray:
|
| 160 |
+
"""
|
| 161 |
+
Executes Real-ESRGAN x2 ONNX model to upscale and remove compression artifacts.
|
| 162 |
+
Input: cv_img (BGR). Output: 2x Super-Resolved BGR image.
|
| 163 |
+
"""
|
| 164 |
+
session = cls.get_session("realesrgan")
|
| 165 |
+
if session is None:
|
| 166 |
+
raise RuntimeError("Real-ESRGAN ONNX session is not available.")
|
| 167 |
+
|
| 168 |
+
h, w = cv_img.shape[:2]
|
| 169 |
+
|
| 170 |
+
# For CPU safety, if the input image is already large, we scale it down before upscaling
|
| 171 |
+
# to prevent memory overflows and extremely slow processing.
|
| 172 |
+
max_input_dim = 700
|
| 173 |
+
if max(h, w) > max_input_dim:
|
| 174 |
+
if h > w:
|
| 175 |
+
new_h = max_input_dim
|
| 176 |
+
new_w = int(w * (max_input_dim / h))
|
| 177 |
+
else:
|
| 178 |
+
new_w = max_input_dim
|
| 179 |
+
new_h = int(h * (max_input_dim / w))
|
| 180 |
+
processing_img = cv2.resize(cv_img, (new_w, new_h), interpolation=cv2.INTER_AREA)
|
| 181 |
+
else:
|
| 182 |
+
processing_img = cv_img.copy()
|
| 183 |
+
|
| 184 |
+
ph, pw = processing_img.shape[:2]
|
| 185 |
+
|
| 186 |
+
# 1. Preprocess: Convert to RGB and normalize to [0.0, 1.0]
|
| 187 |
+
img_rgb = cv2.cvtColor(processing_img, cv2.COLOR_BGR2RGB)
|
| 188 |
+
img_float = img_rgb.astype(np.float32) / 255.0
|
| 189 |
+
|
| 190 |
+
# Transpose to CHW and add batch dimension
|
| 191 |
+
img_tensor = np.transpose(img_float, (2, 0, 1))
|
| 192 |
+
img_tensor = np.expand_dims(img_tensor, axis=0)
|
| 193 |
+
|
| 194 |
+
# 2. Run Inference
|
| 195 |
+
inputs = {session.get_inputs()[0].name: img_tensor}
|
| 196 |
+
upscaled_out = session.run(None, inputs)[0]
|
| 197 |
+
|
| 198 |
+
# 3. Postprocess
|
| 199 |
+
upscaled_img = np.squeeze(upscaled_out)
|
| 200 |
+
upscaled_img = np.transpose(upscaled_img, (1, 2, 0)) # CHW -> HWC
|
| 201 |
+
upscaled_img = np.clip(upscaled_img * 255.0, 0, 255).astype(np.uint8)
|
| 202 |
+
|
| 203 |
+
# Convert RGB to BGR
|
| 204 |
+
upscaled_bgr = cv2.cvtColor(upscaled_img, cv2.COLOR_RGB2BGR)
|
| 205 |
+
return upscaled_bgr
|
backend/enhancement/photo_enhancer.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import cv2
|
| 2 |
+
import numpy as np
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from backend.utilities import pil_to_cv, cv_to_pil
|
| 5 |
+
from backend.enhancement.onnx_engine import ONNXInferenceEngine
|
| 6 |
+
|
| 7 |
+
class PhotoEnhancer:
|
| 8 |
+
"""
|
| 9 |
+
Orchestrates AI Photo Enhancement, Face Restoration, and Color Grading.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
@staticmethod
|
| 13 |
+
def apply_color_grading(img: np.ndarray, vibrancy_boost: float = 1.15, contrast_boost: float = 1.02) -> np.ndarray:
|
| 14 |
+
"""
|
| 15 |
+
Preserves 100% of the original photo's natural color palette, balance, and contrast.
|
| 16 |
+
Returns the input image completely untouched.
|
| 17 |
+
"""
|
| 18 |
+
return img
|
| 19 |
+
|
| 20 |
+
@classmethod
|
| 21 |
+
def restore_faces(cls, img: np.ndarray) -> np.ndarray:
|
| 22 |
+
"""
|
| 23 |
+
Detects faces in the image, runs GFPGAN ONNX inference on crops,
|
| 24 |
+
and blends them back seamlessly using feathered alpha blending.
|
| 25 |
+
"""
|
| 26 |
+
# Load OpenCV Haar Cascade face detector
|
| 27 |
+
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml')
|
| 28 |
+
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
| 29 |
+
|
| 30 |
+
# Detect faces
|
| 31 |
+
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(40, 40))
|
| 32 |
+
if len(faces) == 0:
|
| 33 |
+
return img
|
| 34 |
+
|
| 35 |
+
result = img.copy()
|
| 36 |
+
h, w = img.shape[:2]
|
| 37 |
+
|
| 38 |
+
print(f"[Photo Enhancer] Detected {len(faces)} face(s) for GFPGAN restoration.")
|
| 39 |
+
for idx, (fx, fy, fw, fh) in enumerate(faces):
|
| 40 |
+
try:
|
| 41 |
+
# Add 40% padding around the detected face region for natural context
|
| 42 |
+
pad_x = int(fw * 0.4)
|
| 43 |
+
pad_y = int(fh * 0.4)
|
| 44 |
+
|
| 45 |
+
x1 = max(0, fx - pad_x)
|
| 46 |
+
y1 = max(0, fy - pad_y)
|
| 47 |
+
x2 = min(w, fx + fw + pad_x)
|
| 48 |
+
y2 = min(h, fy + fh + pad_y)
|
| 49 |
+
|
| 50 |
+
crop_w = x2 - x1
|
| 51 |
+
crop_h = y2 - y1
|
| 52 |
+
|
| 53 |
+
if crop_w < 20 or crop_h < 20:
|
| 54 |
+
continue
|
| 55 |
+
|
| 56 |
+
face_crop = img[y1:y2, x1:x2]
|
| 57 |
+
|
| 58 |
+
# Execute GFPGAN ONNX restoration
|
| 59 |
+
restored_face_512 = ONNXInferenceEngine.run_gfpgan(face_crop)
|
| 60 |
+
|
| 61 |
+
# Resize the restored face back to the crop's original dimensions
|
| 62 |
+
restored_face = cv2.resize(restored_face_512, (crop_w, crop_h), interpolation=cv2.INTER_CUBIC)
|
| 63 |
+
|
| 64 |
+
# Create a feathered ellipse blending mask to overlay the face crop seamlessly
|
| 65 |
+
mask = np.zeros((crop_h, crop_w), dtype=np.float32)
|
| 66 |
+
cx = crop_w // 2
|
| 67 |
+
cy = crop_h // 2
|
| 68 |
+
rx = int(crop_w * 0.45)
|
| 69 |
+
ry = int(crop_h * 0.45)
|
| 70 |
+
cv2.ellipse(mask, (cx, cy), (rx, ry), 0, 0, 360, 1.0, -1)
|
| 71 |
+
|
| 72 |
+
# Blur the mask to create a smooth, linear alpha transition
|
| 73 |
+
mask_blurred = cv2.GaussianBlur(mask, (31, 31), 0)
|
| 74 |
+
mask_3d = np.expand_dims(mask_blurred, axis=2)
|
| 75 |
+
|
| 76 |
+
# Blend the restored face back onto the image
|
| 77 |
+
original_crop = result[y1:y2, x1:x2].astype(np.float32)
|
| 78 |
+
restored_crop = restored_face.astype(np.float32)
|
| 79 |
+
|
| 80 |
+
blended_crop = restored_crop * mask_3d + original_crop * (1.0 - mask_3d)
|
| 81 |
+
result[y1:y2, x1:x2] = np.clip(blended_crop, 0, 255).astype(np.uint8)
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"[Photo Enhancer] Face {idx+1} restoration failed: {e}. Falling back to original face.")
|
| 84 |
+
|
| 85 |
+
return result
|
| 86 |
+
|
| 87 |
+
@classmethod
|
| 88 |
+
def apply_opencv_enhancement(cls, img: np.ndarray) -> np.ndarray:
|
| 89 |
+
"""
|
| 90 |
+
High-performance zero-dependency detail enhancer (Bilateral denoising + Unsharp Masking).
|
| 91 |
+
Preserves 100% original color, contrast, and histogram distributions.
|
| 92 |
+
"""
|
| 93 |
+
# 1. Bilateral filtering (reduces noise, preserves sharp object edges)
|
| 94 |
+
denoised = cv2.bilateralFilter(img, 9, 35, 35)
|
| 95 |
+
|
| 96 |
+
# 2. High-Pass Unsharp Masking (recovers micro-details and textures)
|
| 97 |
+
blurred = cv2.GaussianBlur(denoised, (0, 0), 3)
|
| 98 |
+
sharpened = cv2.addWeighted(denoised, 1.4, blurred, -0.4, 0)
|
| 99 |
+
|
| 100 |
+
return sharpened
|
| 101 |
+
|
| 102 |
+
@classmethod
|
| 103 |
+
def process_enhancement(
|
| 104 |
+
cls,
|
| 105 |
+
pil_image: Image.Image,
|
| 106 |
+
mode: str = "Professional DSLR",
|
| 107 |
+
face_restoration: bool = True
|
| 108 |
+
) -> Image.Image:
|
| 109 |
+
"""
|
| 110 |
+
Main entry point to execute the photo enhancement pipeline.
|
| 111 |
+
Modes:
|
| 112 |
+
- "Standard": Fast, zero-dependency detail sharpener + color grading.
|
| 113 |
+
- "High Quality (Neural)": Real-ESRGAN super-resolution + color grading.
|
| 114 |
+
- "Professional DSLR": Full pipeline (Real-ESRGAN + GFPGAN Face Restore + Color Grading).
|
| 115 |
+
"""
|
| 116 |
+
cv_img = pil_to_cv(pil_image)
|
| 117 |
+
|
| 118 |
+
# 1. Photo Upscaling & Detail Sharpness
|
| 119 |
+
if mode in ["High Quality (Neural)", "Professional DSLR"]:
|
| 120 |
+
try:
|
| 121 |
+
# Try running Real-ESRGAN ONNX
|
| 122 |
+
enhanced_cv = ONNXInferenceEngine.run_realesrgan(cv_img[:, :, :3])
|
| 123 |
+
except Exception as e:
|
| 124 |
+
print(f"[Photo Enhancer] Real-ESRGAN upscale failed: {e}. Falling back to OpenCV sharpener.")
|
| 125 |
+
# Fallback to high-speed OpenCV sharpener
|
| 126 |
+
enhanced_cv = cls.apply_opencv_enhancement(cv_img[:, :, :3])
|
| 127 |
+
else:
|
| 128 |
+
# Standard Mode (OpenCV sharpener)
|
| 129 |
+
enhanced_cv = cls.apply_opencv_enhancement(cv_img[:, :, :3])
|
| 130 |
+
|
| 131 |
+
# 2. Face Restoration (GFPGAN)
|
| 132 |
+
if face_restoration and mode == "Professional DSLR":
|
| 133 |
+
try:
|
| 134 |
+
enhanced_cv = cls.restore_faces(enhanced_cv)
|
| 135 |
+
except Exception as e:
|
| 136 |
+
print(f"[Photo Enhancer] GFPGAN face restoration failed: {e}.")
|
| 137 |
+
|
| 138 |
+
# 3. Color Grading & Aesthetics
|
| 139 |
+
final_cv = cls.apply_color_grading(enhanced_cv)
|
| 140 |
+
|
| 141 |
+
# Apply original transparency alpha channel if present
|
| 142 |
+
if cv_img.shape[2] == 4:
|
| 143 |
+
h, w = final_cv.shape[:2]
|
| 144 |
+
alpha = cv2.resize(cv_img[:, :, 3], (w, h), interpolation=cv2.INTER_LINEAR)
|
| 145 |
+
result_rgba = np.zeros((h, w, 4), dtype=np.uint8)
|
| 146 |
+
result_rgba[:, :, :3] = final_cv
|
| 147 |
+
result_rgba[:, :, 3] = alpha
|
| 148 |
+
return cv_to_pil(result_rgba)
|
| 149 |
+
else:
|
| 150 |
+
return cv_to_pil(final_cv)
|
backend/models/depth_anything.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:afb6a5c28f3b6bf1618c6e43f02073ef9dfdc70e937502d51603e57b0a1df10c
|
| 3 |
+
size 99060839
|
backend/text_editor/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from backend.text_editor.orchestrator import TextEditorOrchestrator
|
| 2 |
+
from backend.text_editor.pdf_processor import get_pdf_page_count, pdf_page_to_pil, compile_images_to_pdf
|
backend/text_editor/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (409 Bytes). View file
|
|
|
backend/text_editor/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (364 Bytes). View file
|
|
|
backend/text_editor/__pycache__/color_detector.cpython-311.pyc
ADDED
|
Binary file (3.11 kB). View file
|
|
|
backend/text_editor/__pycache__/color_detector.cpython-314.pyc
ADDED
|
Binary file (3.01 kB). View file
|
|
|
backend/text_editor/__pycache__/font_detector.cpython-311.pyc
ADDED
|
Binary file (4.06 kB). View file
|
|
|
backend/text_editor/__pycache__/font_detector.cpython-314.pyc
ADDED
|
Binary file (2.93 kB). View file
|
|
|
backend/text_editor/__pycache__/font_downloader.cpython-311.pyc
ADDED
|
Binary file (5.2 kB). View file
|
|
|
backend/text_editor/__pycache__/ocr_engine.cpython-311.pyc
ADDED
|
Binary file (3.01 kB). View file
|
|
|
backend/text_editor/__pycache__/ocr_engine.cpython-314.pyc
ADDED
|
Binary file (2.62 kB). View file
|
|
|