Kaushal05 commited on
Commit
6c07fdf
·
verified ·
1 Parent(s): da82c54

Upload 90 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +7 -0
  2. README.md +108 -14
  3. app.py +183 -0
  4. backend/__init__.py +1 -0
  5. backend/__pycache__/__init__.cpython-311.pyc +0 -0
  6. backend/__pycache__/__init__.cpython-314.pyc +0 -0
  7. backend/__pycache__/edge_detector.cpython-311.pyc +0 -0
  8. backend/__pycache__/edge_detector.cpython-314.pyc +0 -0
  9. backend/__pycache__/image_processor.cpython-311.pyc +0 -0
  10. backend/__pycache__/image_processor.cpython-314.pyc +0 -0
  11. backend/__pycache__/segmentation.cpython-311.pyc +0 -0
  12. backend/__pycache__/segmentation.cpython-314.pyc +0 -0
  13. backend/__pycache__/shadow_generator.cpython-311.pyc +0 -0
  14. backend/__pycache__/shadow_generator.cpython-314.pyc +0 -0
  15. backend/__pycache__/utilities.cpython-311.pyc +0 -0
  16. backend/__pycache__/utilities.cpython-314.pyc +0 -0
  17. backend/bg_remover/__init__.py +1 -0
  18. backend/bg_remover/__pycache__/__init__.cpython-311.pyc +0 -0
  19. backend/bg_remover/__pycache__/__init__.cpython-314.pyc +0 -0
  20. backend/bg_remover/__pycache__/edge_detection.cpython-311.pyc +0 -0
  21. backend/bg_remover/__pycache__/edge_detection.cpython-314.pyc +0 -0
  22. backend/bg_remover/__pycache__/image_processor.cpython-311.pyc +0 -0
  23. backend/bg_remover/__pycache__/image_processor.cpython-314.pyc +0 -0
  24. backend/bg_remover/__pycache__/segmentation.cpython-311.pyc +0 -0
  25. backend/bg_remover/__pycache__/segmentation.cpython-314.pyc +0 -0
  26. backend/bg_remover/__pycache__/shadow_generator.cpython-311.pyc +0 -0
  27. backend/bg_remover/__pycache__/shadow_generator.cpython-314.pyc +0 -0
  28. backend/bg_remover/edge_detection.py +85 -0
  29. backend/bg_remover/image_processor.py +194 -0
  30. backend/bg_remover/segmentation.py +137 -0
  31. backend/bg_remover/shadow_generator.py +68 -0
  32. backend/dslr_blur/__pycache__/blur_processor.cpython-311.pyc +0 -0
  33. backend/dslr_blur/__pycache__/depth_blur.cpython-311.pyc +0 -0
  34. backend/dslr_blur/blur_processor.py +172 -0
  35. backend/dslr_blur/depth_blur.py +170 -0
  36. backend/enhancement/__pycache__/onnx_engine.cpython-311.pyc +0 -0
  37. backend/enhancement/__pycache__/photo_enhancer.cpython-311.pyc +0 -0
  38. backend/enhancement/onnx_engine.py +205 -0
  39. backend/enhancement/photo_enhancer.py +150 -0
  40. backend/models/depth_anything.onnx +3 -0
  41. backend/text_editor/__init__.py +2 -0
  42. backend/text_editor/__pycache__/__init__.cpython-311.pyc +0 -0
  43. backend/text_editor/__pycache__/__init__.cpython-314.pyc +0 -0
  44. backend/text_editor/__pycache__/color_detector.cpython-311.pyc +0 -0
  45. backend/text_editor/__pycache__/color_detector.cpython-314.pyc +0 -0
  46. backend/text_editor/__pycache__/font_detector.cpython-311.pyc +0 -0
  47. backend/text_editor/__pycache__/font_detector.cpython-314.pyc +0 -0
  48. backend/text_editor/__pycache__/font_downloader.cpython-311.pyc +0 -0
  49. backend/text_editor/__pycache__/ocr_engine.cpython-311.pyc +0 -0
  50. 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
- title: Helpful AI
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: For Background remove , DSLR Blurring , Text Editing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  ---
13
 
14
- # Welcome to Streamlit!
15
 
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
 
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
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