walidadebayo commited on
Commit
eb4231e
·
1 Parent(s): 4b7ca80

Refactor API structure: remove Streamlit integration, update Flask app, and enhance documentation

Browse files
Files changed (13) hide show
  1. .gitattributes +0 -2
  2. .gitignore +1 -5
  3. .streamlit/config.toml +0 -6
  4. Dockerfile +1 -5
  5. README.md +286 -5
  6. api.py +0 -76
  7. app.py +68 -123
  8. assets/demo.gif +0 -3
  9. assets/demo.png +0 -3
  10. entrypoint.py +0 -37
  11. requirements.txt +0 -2
  12. src/st_style.py +0 -42
  13. templates/index.html +317 -0
.gitattributes CHANGED
@@ -33,5 +33,3 @@ 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
36
- assets/demo.gif filter=lfs diff=lfs merge=lfs -text
37
- assets/demo.png 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
 
 
.gitignore CHANGED
@@ -121,8 +121,4 @@ venv.bak/
121
  dmypy.json
122
 
123
  # Pyre type checker
124
- .pyre/
125
-
126
- frontend
127
- magic-eraser-api
128
- magic-eraser
 
121
  dmypy.json
122
 
123
  # Pyre type checker
124
+ .pyre/
 
 
 
 
.streamlit/config.toml DELETED
@@ -1,6 +0,0 @@
1
- [server]
2
- maxUploadSize = 10
3
-
4
- [theme]
5
- base="light"
6
- primaryColor="#0074ff"
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -15,10 +15,6 @@ COPY . .
15
 
16
  RUN pip install -r requirements.txt
17
 
18
- # Default to running both Flask API and Streamlit app
19
- ENV RUN_MODE=all
20
  ENV PORT=7860
21
- ENV STREAMLIT_PORT=8501
22
 
23
- # Use our entrypoint script
24
- CMD [ "python", "entrypoint.py" ]
 
15
 
16
  RUN pip install -r requirements.txt
17
 
 
 
18
  ENV PORT=7860
 
19
 
20
+ CMD [ "python", "app.py" ]
 
README.md CHANGED
@@ -1,12 +1,293 @@
1
  ---
2
- title: Magic Eraser
3
- emoji: 👁
4
- colorFrom: blue
5
  colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
- short_description: Erase unwanted object in your images
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Magic Eraser API
3
+ emoji: 🧹
4
+ colorFrom: indigo
5
  colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
+ short_description: Image processing API for removing objects from images
10
  ---
11
 
12
+ # Magic Eraser API
13
+
14
+ An AI-powered REST API that helps you remove unwanted objects from images and perform content-aware resizing.
15
+
16
+ ## Features
17
+
18
+ - **Object Removal**: Remove unwanted objects from images with advanced AI inpainting
19
+ - **Seam Carving**: Content-aware image resizing and object removal
20
+ - **REST API**: Simple endpoints for integrating image processing into your applications
21
+
22
+ ## API Documentation
23
+
24
+ The REST API provides these endpoints for image processing:
25
+
26
+ - `GET /` - Health check endpoint
27
+ - `POST /api/inpaint` - For inpainting/object removal
28
+ - `POST /api/seam-carve` - For seam carving/content-aware resizing
29
+
30
+ ### Inpainting API
31
+
32
+ **Endpoint:** `POST /api/inpaint`
33
+
34
+ **Request Body:**
35
+ ```json
36
+ {
37
+ "image": "data:image/png;base64,<base64-encoded-image>",
38
+ "mask": "data:image/png;base64,<base64-encoded-mask>"
39
+ }
40
+ ```
41
+
42
+ **Response:**
43
+ ```json
44
+ {
45
+ "result": "data:image/png;base64,<base64-encoded-result>"
46
+ }
47
+ ```
48
+
49
+ **JavaScript Example:**
50
+ ```javascript
51
+ async function removeObject(imageBase64, maskBase64) {
52
+ const response = await fetch('https://yourdeployment.hf.space/api/inpaint', {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json',
56
+ },
57
+ body: JSON.stringify({
58
+ image: imageBase64,
59
+ mask: maskBase64,
60
+ }),
61
+ });
62
+
63
+ const data = await response.json();
64
+ return data.result; // Base64 encoded result image
65
+ }
66
+ ```
67
+
68
+ ### Seam Carving API
69
+
70
+ **Endpoint:** `POST /api/seam-carve`
71
+
72
+ **Request Body:**
73
+ ```json
74
+ {
75
+ "image": "data:image/png;base64,<base64-encoded-image>",
76
+ "mask": "data:image/png;base64,<base64-encoded-mask>",
77
+ "vs": 50,
78
+ "hs": 30,
79
+ "mode": "resize"
80
+ }
81
+ ```
82
+
83
+ **Response:**
84
+ ```json
85
+ {
86
+ "result": "data:image/png;base64,<base64-encoded-result>"
87
+ }
88
+ ```
89
+ **JavaScript Example:**
90
+ ```javascript
91
+ async function resizeImage(imageBase64, maskBase64) {
92
+ const response = await fetch('https://yourdeployment.hf.space/api/seam-carve', {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ },
97
+ body: JSON.stringify({
98
+ image: imageBase64,
99
+ mask: maskBase64,
100
+ vs: -50, // Remove 50 vertical seams (make narrower)
101
+ hs: 30, // Add 30 horizontal seams (make taller)
102
+ mode: 'resize', // or 'remove' to remove objects
103
+ }),
104
+ });
105
+
106
+ const data = await response.json();
107
+ return data.result;
108
+ }
109
+ ```
110
+
111
+ ### Creating a Drawing Canvas for Masks
112
+
113
+ To integrate with our API, you'll need a way for users to draw masks over images. Here's how to set up a canvas in different frameworks:
114
+
115
+ #### Using HTML5 Canvas (JavaScript)
116
+
117
+ ```javascript
118
+ function setupCanvas(imageUrl) {
119
+ // Create canvas elements
120
+ const imageCanvas = document.createElement('canvas');
121
+ const maskCanvas = document.createElement('canvas');
122
+ const container = document.getElementById('canvas-container');
123
+ container.appendChild(imageCanvas);
124
+ container.appendChild(maskCanvas);
125
+
126
+ // Load image
127
+ const img = new Image();
128
+ img.onload = function() {
129
+ // Set canvas dimensions
130
+ imageCanvas.width = maskCanvas.width = img.width;
131
+ imageCanvas.height = maskCanvas.height = img.height;
132
+
133
+ // Draw image on image canvas
134
+ const imgCtx = imageCanvas.getContext('2d');
135
+ imgCtx.drawImage(img, 0, 0);
136
+
137
+ // Setup mask canvas for drawing
138
+ const maskCtx = maskCanvas.getContext('2d');
139
+ maskCtx.fillStyle = 'rgba(255, 0, 0, 0.5)';
140
+
141
+ // Drawing state
142
+ let isDrawing = false;
143
+
144
+ // Mouse/touch event handlers
145
+ maskCanvas.addEventListener('mousedown', startDrawing);
146
+ maskCanvas.addEventListener('mousemove', draw);
147
+ maskCanvas.addEventListener('mouseup', stopDrawing);
148
+ maskCanvas.addEventListener('mouseleave', stopDrawing);
149
+
150
+ function startDrawing(e) {
151
+ isDrawing = true;
152
+ draw(e);
153
+ }
154
+
155
+ function draw(e) {
156
+ if (!isDrawing) return;
157
+ const rect = maskCanvas.getBoundingClientRect();
158
+ const x = e.clientX - rect.left;
159
+ const y = e.clientY - rect.top;
160
+ maskCtx.beginPath();
161
+ maskCtx.arc(x, y, 15, 0, Math.PI * 2);
162
+ maskCtx.fill();
163
+ }
164
+
165
+ function stopDrawing() {
166
+ isDrawing = false;
167
+ }
168
+
169
+ // Add button to get mask data
170
+ const button = document.createElement('button');
171
+ button.textContent = 'Process Image';
172
+ button.onclick = function() {
173
+ const maskData = maskCanvas.toDataURL('image/png');
174
+ const imageData = imageCanvas.toDataURL('image/png');
175
+
176
+ // Send to API
177
+ sendToAPI(imageData, maskData);
178
+ };
179
+ container.appendChild(button);
180
+ };
181
+ img.src = imageUrl;
182
+ }
183
+
184
+ function sendToAPI(imageData, maskData) {
185
+ fetch('https://yourdeployment.hf.space/api/inpaint', {
186
+ method: 'POST',
187
+ headers: {
188
+ 'Content-Type': 'application/json',
189
+ },
190
+ body: JSON.stringify({
191
+ image: imageData,
192
+ mask: maskData
193
+ }),
194
+ })
195
+ .then(response => response.json())
196
+ .then(data => {
197
+ // Display the result
198
+ const resultImg = document.createElement('img');
199
+ resultImg.src = data.result;
200
+ document.getElementById('result-container').appendChild(resultImg);
201
+ });
202
+ }
203
+ ```
204
+
205
+
206
+ #### Using React with Canvas
207
+
208
+ For React applications, consider using libraries like:
209
+
210
+ - `react-konva` for general canvas operations
211
+ - `react-canvas-draw` for simple drawing tools
212
+ - `fabric.js` wrapped in a React component for more complex drawing
213
+
214
+ Example with react-canvas-draw:
215
+
216
+ ```jsx
217
+ import React, { useRef, useState } from 'react';
218
+ import CanvasDraw from 'react-canvas-draw';
219
+
220
+ function MaskDrawer({ imageUrl, onSubmit }) {
221
+ const canvasRef = useRef(null);
222
+ const [brushSize, setBrushSize] = useState(15);
223
+
224
+ const handleSubmit = () => {
225
+ if (canvasRef.current) {
226
+ // Get the mask data
227
+ const maskData = canvasRef.current.canvas.drawing.toDataURL();
228
+ onSubmit(imageUrl, maskData);
229
+ }
230
+ };
231
+
232
+ const handleClear = () => {
233
+ if (canvasRef.current) {
234
+ canvasRef.current.clear();
235
+ }
236
+ };
237
+
238
+ return (
239
+ <div>
240
+ <div>
241
+ <label>
242
+ Brush Size:
243
+ <input
244
+ type="range"
245
+ min="5"
246
+ max="50"
247
+ value={brushSize}
248
+ onChange={e => setBrushSize(parseInt(e.target.value))}
249
+ />
250
+ </label>
251
+ </div>
252
+
253
+ <CanvasDraw
254
+ ref={canvasRef}
255
+ brushColor="rgba(255, 0, 0, 0.5)"
256
+ brushRadius={brushSize}
257
+ lazyRadius={0}
258
+ canvasWidth={800}
259
+ canvasHeight={600}
260
+ imgSrc={imageUrl}
261
+ />
262
+
263
+ <div>
264
+ <button onClick={handleClear}>Clear</button>
265
+ <button onClick={handleSubmit}>Process Image</button>
266
+ </div>
267
+ </div>
268
+ );
269
+ }
270
+ ```
271
+
272
+ #### Tips for Creating Effective Masks
273
+
274
+ 1. Use a contrasting color for the mask (red or magenta works well)
275
+ 2. Keep brush strokes within the boundaries of the object to remove
276
+ 3. For best results, completely cover the object you want to remove
277
+ 4. When sending to the API, make sure the mask's alpha channel is 0 for areas to remove
278
+
279
+ ## Local Development
280
+
281
+ To run the API locally:
282
+
283
+ ```bash
284
+ # Install dependencies
285
+ pip install -r requirements.txt
286
+
287
+ # Run Flask API
288
+ python api.py
289
+ ```
290
+
291
+ ## License
292
+
293
+ This project is licensed under the MIT License - see the LICENSE file for details.
api.py DELETED
@@ -1,76 +0,0 @@
1
- from flask import Flask, request, jsonify
2
- from flask_cors import CORS
3
- import numpy as np
4
- import cv2
5
- import base64
6
- from src.core import process_inpaint, s_image
7
- import os
8
-
9
- app = Flask(__name__)
10
- CORS(app)
11
-
12
- # endpoint for health checks
13
- @app.route('/', methods=['GET'])
14
- def health_check():
15
- return jsonify({"status": "API is running"})
16
-
17
- @app.route('/api/inpaint', methods=['POST'])
18
- def inpaint():
19
- # Get data from request
20
- data = request.json
21
- image_data = data.get('image')
22
- mask_data = data.get('mask')
23
-
24
- # Convert base64 to numpy arrays
25
- image = base64_to_image(image_data)
26
- mask = base64_to_image(mask_data)
27
-
28
- # Process the image
29
- result = process_inpaint(image, mask)
30
-
31
- # Convert back to base64
32
- result_base64 = image_to_base64(result)
33
-
34
- return jsonify({'result': result_base64})
35
-
36
- @app.route('/api/seam-carve', methods=['POST'])
37
- def seam_carve():
38
- # Get data from request
39
- data = request.json
40
- image_data = data.get('image')
41
- mask_data = data.get('mask')
42
- vs = int(data.get('vs', 0)) # vertical seams
43
- hs = int(data.get('hs', 0)) # horizontal seams
44
- mode = data.get('mode', 'resize') # resize or remove
45
-
46
- # Convert base64 to numpy arrays
47
- image = base64_to_image(image_data)
48
- mask = base64_to_image(mask_data)
49
-
50
- # Process the image
51
- result = s_image(image, mask, vs, hs, mode)
52
-
53
- # Convert back to base64
54
- result_base64 = image_to_base64(result)
55
-
56
- return jsonify({'result': result_base64})
57
-
58
- def base64_to_image(base64_str):
59
- img_bytes = base64.b64decode(base64_str.split(',')[1])
60
- img_array = np.frombuffer(img_bytes, np.uint8)
61
- img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
62
- return img
63
-
64
- def image_to_base64(image):
65
- # Convert to BGR if it's RGB
66
- if len(image.shape) > 2 and image.shape[2] == 3:
67
- image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
68
-
69
- _, buffer = cv2.imencode('.png', image)
70
- img_bytes = base64.b64encode(buffer).decode('utf-8')
71
- return f"data:image/png;base64,{img_bytes}"
72
-
73
- if __name__ == '__main__':
74
- # Use the PORT environment variable provided by Hugging Face or default to 7860
75
- port = int(os.environ.get("PORT", 7860))
76
- app.run(debug=True, host='0.0.0.0', port=port)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,138 +1,83 @@
 
 
1
  import numpy as np
2
- import pandas as pd
3
- import streamlit as st
 
4
  import os
5
- from datetime import datetime
6
- from PIL import Image
7
- from streamlit_drawable_canvas import st_canvas
8
- from io import BytesIO
9
- from copy import deepcopy
10
 
11
- from src.core import process_inpaint
 
12
 
13
- # API availability
14
- st.sidebar.info("""
15
- ## API Endpoints
16
- REST API endpoints are available at:
17
- - `GET /` - Health check
18
- - `POST /api/inpaint` - For inpainting/object removal
19
- - `POST /api/seam-carve` - For seam carving
20
- """)
21
 
22
- def image_download_button(pil_image, filename: str, fmt: str, label="Download"):
23
- if fmt not in ["jpg", "png"]:
24
- raise Exception(f"Unknown image format (Available: {fmt} - case sensitive)")
 
 
 
25
 
26
- pil_format = "JPEG" if fmt == "jpg" else "PNG"
27
- file_format = "jpg" if fmt == "jpg" else "png"
28
- mime = "image/jpeg" if fmt == "jpg" else "image/png"
 
 
 
 
 
 
29
 
30
- buf = BytesIO()
31
- pil_image.save(buf, format=pil_format)
 
32
 
33
- return st.download_button(
34
- label=label,
35
- data=buf.getvalue(),
36
- file_name=f'{filename}.{file_format}',
37
- mime=mime,
38
- )
39
-
40
- if "button_id" not in st.session_state:
41
- st.session_state["button_id"] = ""
42
- if "color_to_label" not in st.session_state:
43
- st.session_state["color_to_label"] = {}
44
-
45
- if 'reuse_image' not in st.session_state:
46
- st.session_state.reuse_image = None
47
- def set_image(img):
48
- st.session_state.reuse_image = img
49
-
50
- st.title("Magic-Eraser")
51
-
52
- st.image(open("assets/demo.png", "rb").read())
53
-
54
- st.markdown(
55
- """
56
- You don't have to worry about mastering photo editing techniques to remove an object from your photo. **Simply mark over the areas you want to erase, and our AI will take care of the rest.**
57
- """
58
- )
59
- uploaded_file = st.file_uploader("Choose image", accept_multiple_files=False, type=["png", "jpg", "jpeg"])
60
-
61
- if uploaded_file is not None:
62
 
63
- if st.session_state.reuse_image is not None:
64
- img_input = Image.fromarray(st.session_state.reuse_image)
65
- else:
66
- bytes_data = uploaded_file.getvalue()
67
- img_input = Image.open(BytesIO(bytes_data)).convert("RGBA")
68
-
69
- #resize
70
- max_size = 2000
71
- img_width, img_height = img_input.size
72
- if img_width > max_size or img_height > max_size:
73
- if img_width > img_height:
74
- new_width = max_size
75
- new_height = int((max_size / img_width) * img_height)
76
- else:
77
- new_height = max_size
78
- new_width = int((max_size / img_height) * img_width)
79
- img_input = img_input.resize((new_width, new_height))
80
 
81
- stroke_width = st.slider("Brush size", 1, 100, 50)
82
 
83
- st.write("**Now draw (brush) the part of image that you want to remove.**")
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
- canvas_bg = deepcopy(img_input)
86
- aspect_ratio = canvas_bg.width / canvas_bg.height
87
- streamlit_width = 720
88
 
89
- if canvas_bg.width > streamlit_width:
90
- canvas_bg = canvas_bg.resize((streamlit_width, int(streamlit_width / aspect_ratio)))
91
 
92
- canvas_result = st_canvas(
93
- stroke_color="rgba(255, 0, 255, 1)",
94
- stroke_width=stroke_width,
95
- background_image=canvas_bg,
96
- width=canvas_bg.width,
97
- height=canvas_bg.height,
98
- drawing_mode="freedraw",
99
- key="compute_arc_length",
100
- )
 
 
 
101
 
102
- if canvas_result.image_data is not None:
103
- im = np.array(Image.fromarray(canvas_result.image_data.astype(np.uint8)).resize(img_input.size))
104
- background = np.where(
105
- (im[:, :, 0] == 0) &
106
- (im[:, :, 1] == 0) &
107
- (im[:, :, 2] == 0)
108
- )
109
- drawing = np.where(
110
- (im[:, :, 0] == 255) &
111
- (im[:, :, 1] == 0) &
112
- (im[:, :, 2] == 255)
113
- )
114
- im[background]=[0,0,0,255]
115
- im[drawing]=[0,0,0,0] # RGBA
116
-
117
- reuse = False
118
-
119
- if st.button('Submit'):
120
-
121
- with st.spinner("AI is doing the magic!"):
122
- output = process_inpaint(np.array(img_input), np.array(im)) #TODO Put button here
123
- img_output = Image.fromarray(output).convert("RGB")
124
-
125
- st.write("AI has finished the job!")
126
- st.image(img_output)
127
- # reuse = st.button('Edit again (Re-use this image)', on_click=set_image, args=(inpainted_img, ))
128
-
129
- uploaded_name = os.path.splitext(uploaded_file.name)[0]
130
- image_download_button(
131
- pil_image=img_output,
132
- filename=uploaded_name,
133
- fmt="jpg",
134
- label="Download Image"
135
- )
136
-
137
- st.info("**TIP**: If the result is not perfect, you can download it then "
138
- "upload then remove the artifacts.")
 
1
+ from flask import Flask, request, jsonify, render_template
2
+ from flask_cors import CORS
3
  import numpy as np
4
+ import cv2
5
+ import base64
6
+ from src.core import process_inpaint, s_image
7
  import os
 
 
 
 
 
8
 
9
+ app = Flask(__name__)
10
+ CORS(app)
11
 
12
+ # Add the API URL
13
+ API_URL = os.environ.get("API_URL", "https://walidadebayo-image-eraser-api.hf.space/")
 
 
 
 
 
 
14
 
15
+ # endpoint for health checks
16
+ @app.route('/', methods=['GET'])
17
+ def health_check():
18
+ # For browser requests, return HTML documentation
19
+ if request.headers.get('Accept', '').find('text/html') != -1:
20
+ return render_template('index.html', api_url=API_URL)
21
 
22
+ # For API health checks, return JSON
23
+ return jsonify({"status": "API is running"})
24
+
25
+ @app.route('/api/inpaint', methods=['POST'])
26
+ def inpaint():
27
+ # Get data from request
28
+ data = request.json
29
+ image_data = data.get('image')
30
+ mask_data = data.get('mask')
31
 
32
+ # Convert base64 to numpy arrays
33
+ image = base64_to_image(image_data)
34
+ mask = base64_to_image(mask_data)
35
 
36
+ # Process the image
37
+ result = process_inpaint(image, mask)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
+ # Convert back to base64
40
+ result_base64 = image_to_base64(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ return jsonify({'result': result_base64})
43
 
44
+ @app.route('/api/seam-carve', methods=['POST'])
45
+ def seam_carve():
46
+ # Get data from request
47
+ data = request.json
48
+ image_data = data.get('image')
49
+ mask_data = data.get('mask')
50
+ vs = int(data.get('vs', 0)) # vertical seams
51
+ hs = int(data.get('hs', 0)) # horizontal seams
52
+ mode = data.get('mode', 'resize') # resize or remove
53
+
54
+ # Convert base64 to numpy arrays
55
+ image = base64_to_image(image_data)
56
+ mask = base64_to_image(mask_data)
57
 
58
+ # Process the image
59
+ result = s_image(image, mask, vs, hs, mode)
 
60
 
61
+ # Convert back to base64
62
+ result_base64 = image_to_base64(result)
63
 
64
+ return jsonify({'result': result_base64})
65
+
66
+ def base64_to_image(base64_str):
67
+ img_bytes = base64.b64decode(base64_str.split(',')[1])
68
+ img_array = np.frombuffer(img_bytes, np.uint8)
69
+ img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
70
+ return img
71
+
72
+ def image_to_base64(image):
73
+ # Convert to BGR if it's RGB
74
+ if len(image.shape) > 2 and image.shape[2] == 3:
75
+ image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
76
 
77
+ _, buffer = cv2.imencode('.png', image)
78
+ img_bytes = base64.b64encode(buffer).decode('utf-8')
79
+ return f"data:image/png;base64,{img_bytes}"
80
+
81
+ if __name__ == '__main__':
82
+ port = int(os.environ.get("PORT", 7860))
83
+ app.run(debug=True, host='0.0.0.0', port=port)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
assets/demo.gif DELETED

Git LFS Details

  • SHA256: 008972f8d1d49ee16fe2cce2669ea5397788bc7885db40ce3c55aca0df6b411d
  • Pointer size: 132 Bytes
  • Size of remote file: 2.88 MB
assets/demo.png DELETED

Git LFS Details

  • SHA256: 1e895e324b9fbf8aed7e512222d89d3ea48fa96263d3a01d0a6af2cf0749a4d7
  • Pointer size: 131 Bytes
  • Size of remote file: 159 kB
entrypoint.py DELETED
@@ -1,37 +0,0 @@
1
- import os
2
- import subprocess
3
- import threading
4
- import sys
5
-
6
- def run_flask():
7
- print("Starting Flask API server...")
8
- from api import app
9
- # Use the PORT environment variable provided by Hugging Face or default to 7860
10
- port = int(os.environ.get("PORT", 7860))
11
- app.run(host='0.0.0.0', port=port)
12
-
13
- def run_streamlit():
14
- print("Starting Streamlit app...")
15
- # Start Streamlit on a different port
16
- streamlit_port = int(os.environ.get("STREAMLIT_PORT", 8501))
17
- subprocess.run([
18
- "streamlit", "run", "app.py",
19
- "--server.port", str(streamlit_port),
20
- "--server.address", "0.0.0.0"
21
- ])
22
-
23
- if __name__ == "__main__":
24
- mode = os.environ.get("RUN_MODE", "all").lower()
25
-
26
- if mode == "flask":
27
- run_flask()
28
- elif mode == "streamlit":
29
- run_streamlit()
30
- else:
31
- # Run both in different threads
32
- flask_thread = threading.Thread(target=run_flask)
33
- flask_thread.daemon = True
34
- flask_thread.start()
35
-
36
- # Run Streamlit in main thread
37
- run_streamlit()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,5 +1,3 @@
1
- streamlit
2
- streamlit-drawable-canvas
3
  flask
4
  flask-cors
5
  numpy
 
 
 
1
  flask
2
  flask-cors
3
  numpy
src/st_style.py DELETED
@@ -1,42 +0,0 @@
1
- button_style = """
2
- <style>
3
- div.stButton > button:first-child {
4
- background-color: rgb(255, 75, 75);
5
- color: rgb(255, 255, 255);
6
- }
7
- div.stButton > button:hover {
8
- background-color: rgb(255, 75, 75);
9
- color: rgb(255, 255, 255);
10
- }
11
- div.stButton > button:active {
12
- background-color: rgb(255, 75, 75);
13
- color: rgb(255, 255, 255);
14
- }
15
- div.stButton > button:focus {
16
- background-color: rgb(255, 75, 75);
17
- color: rgb(255, 255, 255);
18
- }
19
- .css-1cpxqw2:focus:not(:active) {
20
- background-color: rgb(255, 75, 75);
21
- border-color: rgb(255, 75, 75);
22
- color: rgb(255, 255, 255);
23
- }
24
- """
25
-
26
- style = """
27
- <style>
28
- #MainMenu {
29
- visibility: hidden;
30
- }
31
- footer {
32
- visibility: hidden;
33
- }
34
- header {
35
- visibility: hidden;
36
- }
37
- </style>
38
- """
39
-
40
-
41
- def apply_prod_style(st):
42
- return st.markdown(style, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
templates/index.html ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Magic Eraser API Documentation</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ max-width: 1100px;
13
+ margin: 0 auto;
14
+ padding: 20px;
15
+ }
16
+ header {
17
+ border-bottom: 1px solid #eee;
18
+ padding-bottom: 20px;
19
+ margin-bottom: 30px;
20
+ }
21
+ h1 {
22
+ color: #2c3e50;
23
+ }
24
+ h2 {
25
+ color: #3498db;
26
+ margin-top: 40px;
27
+ }
28
+ h3 {
29
+ color: #2980b9;
30
+ }
31
+ pre {
32
+ background-color: #f5f7f9;
33
+ border-radius: 5px;
34
+ padding: 15px;
35
+ overflow-x: auto;
36
+ }
37
+ code {
38
+ font-family: 'Courier New', Courier, monospace;
39
+ }
40
+ .endpoint {
41
+ background-color: #f8f9fa;
42
+ border-left: 4px solid #3498db;
43
+ padding: 15px;
44
+ margin-bottom: 20px;
45
+ }
46
+ .method {
47
+ display: inline-block;
48
+ padding: 3px 6px;
49
+ border-radius: 4px;
50
+ color: white;
51
+ font-size: 14px;
52
+ margin-right: 10px;
53
+ }
54
+ .get {
55
+ background-color: #3498db;
56
+ }
57
+ .post {
58
+ background-color: #2ecc71;
59
+ }
60
+ table {
61
+ border-collapse: collapse;
62
+ width: 100%;
63
+ }
64
+ th, td {
65
+ text-align: left;
66
+ padding: 12px;
67
+ border-bottom: 1px solid #ddd;
68
+ }
69
+ th {
70
+ background-color: #f2f2f2;
71
+ }
72
+ .container {
73
+ margin-top: 20px;
74
+ }
75
+ .demo-section {
76
+ margin-top: 40px;
77
+ padding: 20px;
78
+ background-color: #f9f9f9;
79
+ border-radius: 8px;
80
+ }
81
+ .btn {
82
+ display: inline-block;
83
+ padding: 10px 15px;
84
+ background-color: #3498db;
85
+ color: white;
86
+ text-decoration: none;
87
+ border-radius: 4px;
88
+ border: none;
89
+ cursor: pointer;
90
+ }
91
+ .btn:hover {
92
+ background-color: #2980b9;
93
+ }
94
+ </style>
95
+ </head>
96
+ <body>
97
+ <header>
98
+ <h1>📷 Magic Eraser API</h1>
99
+ <p>An AI-powered REST API for removing unwanted objects from images and performing content-aware resizing.</p>
100
+ <p>Check out the InnoAI Space for the streamline demo frontend: <a href="https://huggingface.co/spaces/innoai/Magic-Eraser-Pub" target="_blank">Magic Eraser Demo</a></p>
101
+ </header>
102
+
103
+ <section>
104
+ <h2>Features</h2>
105
+ <ul>
106
+ <li><strong>Object Removal</strong>: Remove unwanted objects from images with advanced AI inpainting</li>
107
+ <li><strong>Seam Carving</strong>: Content-aware image resizing and object removal</li>
108
+ <li><strong>Simple Integration</strong>: Easy-to-use REST API endpoints</li>
109
+ </ul>
110
+ </section>
111
+
112
+ <section>
113
+ <h2>API Endpoints</h2>
114
+
115
+ <div class="endpoint">
116
+ <h3><span class="method get">GET</span> /</h3>
117
+ <p>Health check endpoint. Returns a status message if the API is running.</p>
118
+ <h4>Response</h4>
119
+ <pre><code>{
120
+ "status": "API is running"
121
+ }</code></pre>
122
+ </div>
123
+
124
+ <div class="endpoint">
125
+ <h3><span class="method post">POST</span> /api/inpaint</h3>
126
+ <p>Removes objects from images and fills in the background using AI.</p>
127
+ <h4>Request Body</h4>
128
+ <pre><code>{
129
+ "image": "data:image/png;base64,<base64-encoded-image>",
130
+ "mask": "data:image/png;base64,<base64-encoded-mask>"
131
+ }</code></pre>
132
+ <table>
133
+ <tr>
134
+ <th>Parameter</th>
135
+ <th>Type</th>
136
+ <th>Description</th>
137
+ </tr>
138
+ <tr>
139
+ <td>image</td>
140
+ <td>string</td>
141
+ <td>Base64-encoded image with data URL prefix</td>
142
+ </tr>
143
+ <tr>
144
+ <td>mask</td>
145
+ <td>string</td>
146
+ <td>Base64-encoded mask where transparent areas (alpha=0) indicate what to remove</td>
147
+ </tr>
148
+ </table>
149
+ <h4>Response</h4>
150
+ <pre><code>{
151
+ "result": "data:image/png;base64,<base64-encoded-result>"
152
+ }</code></pre>
153
+ </div>
154
+
155
+ <div class="endpoint">
156
+ <h3><span class="method post">POST</span> /api/seam-carve</h3>
157
+ <p>Content-aware resizing or object removal using the seam carving algorithm.</p>
158
+ <h4>Request Body</h4>
159
+ <pre><code>{
160
+ "image": "data:image/png;base64,<base64-encoded-image>",
161
+ "mask": "data:image/png;base64,<base64-encoded-mask>",
162
+ "vs": 50,
163
+ "hs": 30,
164
+ "mode": "resize"
165
+ }</code></pre>
166
+ <table>
167
+ <tr>
168
+ <th>Parameter</th>
169
+ <th>Type</th>
170
+ <th>Description</th>
171
+ </tr>
172
+ <tr>
173
+ <td>image</td>
174
+ <td>string</td>
175
+ <td>Base64-encoded image with data URL prefix</td>
176
+ </tr>
177
+ <tr>
178
+ <td>mask</td>
179
+ <td>string</td>
180
+ <td>Base64-encoded mask (optional for resize mode, required for remove mode)</td>
181
+ </tr>
182
+ <tr>
183
+ <td>vs</td>
184
+ <td>integer</td>
185
+ <td>Number of vertical seams to add/remove (positive = add, negative = remove)</td>
186
+ </tr>
187
+ <tr>
188
+ <td>hs</td>
189
+ <td>integer</td>
190
+ <td>Number of horizontal seams to add/remove (positive = add, negative = remove)</td>
191
+ </tr>
192
+ <tr>
193
+ <td>mode</td>
194
+ <td>string</td>
195
+ <td>Either "resize" (change dimensions) or "remove" (remove objects)</td>
196
+ </tr>
197
+ </table>
198
+ <h4>Response</h4>
199
+ <pre><code>{
200
+ "result": "data:image/png;base64,<base64-encoded-result>"
201
+ }</code></pre>
202
+ </div>
203
+ </section>
204
+
205
+ <section>
206
+ <h2>JavaScript Example</h2>
207
+ <p>Here's how to use the API in a JavaScript application:</p>
208
+
209
+ <h3>Inpainting Example</h3>
210
+ <pre><code>async function removeObject(imageBase64, maskBase64) {
211
+ const response = await fetch('{{ api_url }}/api/inpaint', {
212
+ method: 'POST',
213
+ headers: {
214
+ 'Content-Type': 'application/json',
215
+ },
216
+ body: JSON.stringify({
217
+ image: imageBase64,
218
+ mask: maskBase64,
219
+ }),
220
+ });
221
+
222
+ const data = await response.json();
223
+ return data.result; // base64 encoded result image
224
+ }</code></pre>
225
+
226
+ <h3>Seam Carving Example</h3>
227
+ <pre><code>async function resizeImage(imageBase64, maskBase64) {
228
+ const response = await fetch('{{ api_url }}/api/seam-carve', {
229
+ method: 'POST',
230
+ headers: {
231
+ 'Content-Type': 'application/json',
232
+ },
233
+ body: JSON.stringify({
234
+ image: imageBase64,
235
+ mask: maskBase64,
236
+ vs: -50, // Remove 50 vertical seams (make narrower)
237
+ hs: 30, // Add 30 horizontal seams (make taller)
238
+ mode: 'resize', // or 'remove' to remove objects
239
+ }),
240
+ });
241
+
242
+ const data = await response.json();
243
+ return data.result;
244
+ }</code></pre>
245
+ </section>
246
+
247
+ <section>
248
+ <h2>Setting Up a Drawing Canvas</h2>
249
+ <p>To integrate with our API, you'll need a way for users to draw masks over images. Here's a simple HTML5 Canvas example:</p>
250
+
251
+ <pre><code>function setupCanvas(imageUrl) {
252
+ // Create canvas elements
253
+ const imageCanvas = document.createElement('canvas');
254
+ const maskCanvas = document.createElement('canvas');
255
+ const container = document.getElementById('canvas-container');
256
+ container.appendChild(imageCanvas);
257
+ container.appendChild(maskCanvas);
258
+
259
+ // Load image
260
+ const img = new Image();
261
+ img.onload = function() {
262
+ // Set canvas dimensions
263
+ imageCanvas.width = maskCanvas.width = img.width;
264
+ imageCanvas.height = maskCanvas.height = img.height;
265
+
266
+ // Draw image on image canvas
267
+ const imgCtx = imageCanvas.getContext('2d');
268
+ imgCtx.drawImage(img, 0, 0);
269
+
270
+ // Setup mask canvas for drawing
271
+ const maskCtx = maskCanvas.getContext('2d');
272
+ maskCtx.fillStyle = 'rgba(255, 0, 0, 0.5)';
273
+
274
+ // Drawing state
275
+ let isDrawing = false;
276
+
277
+ // Mouse/touch event handlers
278
+ maskCanvas.addEventListener('mousedown', startDrawing);
279
+ maskCanvas.addEventListener('mousemove', draw);
280
+ maskCanvas.addEventListener('mouseup', stopDrawing);
281
+ maskCanvas.addEventListener('mouseleave', stopDrawing);
282
+
283
+ function startDrawing(e) {
284
+ isDrawing = true;
285
+ draw(e);
286
+ }
287
+
288
+ function draw(e) {
289
+ if (!isDrawing) return;
290
+ const rect = maskCanvas.getBoundingClientRect();
291
+ const x = e.clientX - rect.left;
292
+ const y = e.clientY - rect.top;
293
+ maskCtx.beginPath();
294
+ maskCtx.arc(x, y, 15, 0, Math.PI * 2);
295
+ maskCtx.fill();
296
+ }
297
+
298
+ function stopDrawing() {
299
+ isDrawing = false;
300
+ }
301
+ };
302
+ img.src = imageUrl;
303
+ }</code></pre>
304
+ </section>
305
+
306
+ <section class="demo-section">
307
+ <h2>Try It Out</h2>
308
+ <p>Visit our interactive demo frontend:</p>
309
+ <a href="https://walidadebayo-magic-eraser.netlify.app" class="btn" target="_blank">Open Demo Frontend</a>
310
+ <p><small>Demo frontend connects to this API for processing images.</small></p>
311
+ </section>
312
+
313
+ <footer>
314
+ <p><small>© 2023 Magic Eraser API. MIT License.</small></p>
315
+ </footer>
316
+ </body>
317
+ </html>