Nekochu commited on
Commit
b4daad9
·
verified ·
1 Parent(s): 84c6a93
Files changed (3) hide show
  1. README.md +121 -12
  2. app.py +323 -0
  3. requirements.txt +7 -0
README.md CHANGED
@@ -1,12 +1,121 @@
1
- ---
2
- title: Lama Cleaner
3
- emoji: 🌖
4
- colorFrom: purple
5
- colorTo: pink
6
- sdk: gradio
7
- sdk_version: 6.3.0
8
- app_file: app.py
9
- pinned: false
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Lama-Cleaner
3
+ emoji: 🧹
4
+ colorFrom: green
5
+ colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 6.3.0
8
+ app_file: app.py
9
+ pinned: false
10
+ license: apache-2.0
11
+ tags:
12
+ - image-inpainting
13
+ - object-removal
14
+ - lama
15
+ - mcp-server
16
+ short_description: Remove unwanted objects from images with LaMa
17
+ ---
18
+
19
+ # Lama-Cleaner: Image Inpainting
20
+
21
+ Remove unwanted objects from your images using LaMa (Large Mask Inpainting).
22
+
23
+ ## Features
24
+
25
+ - **Object Removal** - Remove any unwanted object, person, or defect from images
26
+ - **LaMa Model** - Uses state-of-the-art LaMa inpainting model
27
+ - **CPU Inference** - Runs on HuggingFace Spaces free tier
28
+ - **CLI Support** - Command-line interface for batch processing
29
+
30
+ ## Usage
31
+
32
+ 1. Upload an image
33
+ 2. Draw over the area you want to remove (white brush = mask)
34
+ 3. Click "Remove Object"
35
+
36
+ ## Tips
37
+
38
+ - Draw the mask slightly larger than the object for best results
39
+ - LaMa works best for small to medium sized areas
40
+ - For complex backgrounds, you may need to adjust the mask
41
+
42
+ ---
43
+
44
+ ## API
45
+
46
+ ### Python Client
47
+
48
+ ```python
49
+ from gradio_client import Client, handle_file
50
+
51
+ client = Client("Luminia/lama-cleaner")
52
+
53
+ # Note: ImageEditor data format
54
+ result = client.predict(
55
+ editor_data={
56
+ "background": handle_file("image.png"),
57
+ "layers": [handle_file("mask.png")],
58
+ "composite": None
59
+ },
60
+ api_name="/inpaint"
61
+ )
62
+ print(result) # (output_image, status)
63
+ ```
64
+
65
+ ### REST API (curl)
66
+
67
+ ```bash
68
+ # Step 1: Submit job
69
+ curl -X POST "https://luminia-lama-cleaner.hf.space/gradio_api/call/inpaint" \
70
+ -H "Content-Type: application/json" \
71
+ -d '{"data": [{"background": "...", "layers": [...]}]}'
72
+
73
+ # Step 2: Get result (SSE stream)
74
+ curl "https://luminia-lama-cleaner.hf.space/gradio_api/call/inpaint/{event_id}"
75
+ ```
76
+
77
+ ### MCP (Model Context Protocol)
78
+
79
+ This Space supports MCP for AI assistants (Claude Desktop, Cursor, VS Code).
80
+
81
+ 1. Click **MCP** badge → **Add to MCP tools**
82
+ 2. The `inpaint` tool becomes available
83
+
84
+ **Tool schema:**
85
+ ```json
86
+ {
87
+ "name": "inpaint",
88
+ "parameters": {
89
+ "editor_data": {"type": "object", "description": "ImageEditor data with background and mask layers"}
90
+ },
91
+ "returns": ["image", "string"]
92
+ }
93
+ ```
94
+
95
+ **MCP Config:**
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "lama-cleaner": {"url": "https://luminia-lama-cleaner.hf.space/gradio_api/mcp/"}
100
+ }
101
+ }
102
+ ```
103
+
104
+ ---
105
+
106
+ ## CLI Usage
107
+
108
+ ```bash
109
+ # Inpaint with external mask
110
+ python app.py inpaint -i image.png -m mask.png -o output.png
111
+ ```
112
+
113
+ **Mask format:** White (255) = area to inpaint, Black (0) = keep
114
+
115
+ ---
116
+
117
+ ## Credits
118
+
119
+ Based on [LaMa](https://github.com/advimman/lama) by SAIC-Moscow and [lama-cleaner](https://github.com/Sanster/lama-cleaner) by Sanster.
120
+
121
+ Paper: [Resolution-robust Large Mask Inpainting with Fourier Convolutions](https://arxiv.org/abs/2109.07161)
app.py ADDED
@@ -0,0 +1,323 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Lama-Cleaner: Image Inpainting with LaMa
3
+ CPU inference for HuggingFace Spaces free tier
4
+ Based on https://github.com/Sanster/lama-cleaner
5
+ """
6
+ import argparse
7
+ import gc
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import cv2
13
+ import numpy as np
14
+ import torch
15
+ from huggingface_hub import hf_hub_download
16
+ from PIL import Image
17
+
18
+ # Force CPU
19
+ os.environ["CUDA_VISIBLE_DEVICES"] = ""
20
+ DEVICE = torch.device("cpu")
21
+
22
+ # Model info
23
+ HF_REPO = "fashn-ai/LaMa"
24
+ MODEL_FILE = "big-lama.pt"
25
+ CACHE_DIR = Path("models")
26
+ CACHE_DIR.mkdir(exist_ok=True)
27
+
28
+ # Global model (lazy loaded)
29
+ MODEL = None
30
+
31
+
32
+ def download_model():
33
+ """Download LaMa model from HuggingFace Hub"""
34
+ model_path = CACHE_DIR / MODEL_FILE
35
+ if not model_path.exists():
36
+ print(f"Downloading {MODEL_FILE}...")
37
+ hf_hub_download(
38
+ repo_id=HF_REPO,
39
+ filename=MODEL_FILE,
40
+ local_dir=CACHE_DIR,
41
+ )
42
+ return model_path
43
+
44
+
45
+ def load_model():
46
+ """Load model (lazy loading to save memory)"""
47
+ global MODEL
48
+ if MODEL is not None:
49
+ return MODEL
50
+
51
+ print("Loading LaMa model...")
52
+ model_path = download_model()
53
+ MODEL = torch.jit.load(str(model_path), map_location=DEVICE)
54
+ MODEL.eval()
55
+ gc.collect()
56
+ print("Model loaded!")
57
+ return MODEL
58
+
59
+
60
+ def norm_img(np_img):
61
+ """Normalize image: HWC -> CHW, uint8 -> float32 [0,1]
62
+ Matches original lama_cleaner/helper.py norm_img()
63
+ """
64
+ if len(np_img.shape) == 2:
65
+ np_img = np_img[:, :, np.newaxis]
66
+ np_img = np.transpose(np_img, (2, 0, 1)) # HWC -> CHW
67
+ np_img = np_img.astype("float32") / 255
68
+ return np_img
69
+
70
+
71
+ def ceil_modulo(x, mod):
72
+ """Ceil to nearest multiple of mod"""
73
+ if x % mod == 0:
74
+ return x
75
+ return (x // mod + 1) * mod
76
+
77
+
78
+ def pad_img_to_modulo(img, mod=8):
79
+ """Pad image to be divisible by mod
80
+ Matches original lama_cleaner/helper.py pad_img_to_modulo()
81
+ """
82
+ if len(img.shape) == 2:
83
+ img = img[:, :, np.newaxis]
84
+ height, width = img.shape[:2]
85
+ out_height = ceil_modulo(height, mod)
86
+ out_width = ceil_modulo(width, mod)
87
+ return np.pad(
88
+ img,
89
+ ((0, out_height - height), (0, out_width - width), (0, 0)),
90
+ mode="symmetric",
91
+ )
92
+
93
+
94
+ def inpaint(image: np.ndarray, mask: np.ndarray) -> np.ndarray:
95
+ """
96
+ Inpaint image using LaMa model.
97
+ Matches original lama_cleaner/model/lama.py forward()
98
+
99
+ Args:
100
+ image: RGB image [H, W, 3] uint8
101
+ mask: Binary mask [H, W] uint8, 255 = area to inpaint, 0 = keep
102
+
103
+ Returns:
104
+ Inpainted RGB image [H, W, 3] uint8
105
+ """
106
+ model = load_model()
107
+
108
+ orig_h, orig_w = image.shape[:2]
109
+
110
+ # Ensure image is RGB (3 channels)
111
+ if len(image.shape) == 3 and image.shape[2] == 4:
112
+ image = image[:, :, :3]
113
+
114
+ # Pad to mod 8
115
+ pad_image = pad_img_to_modulo(image, mod=8)
116
+ pad_mask = pad_img_to_modulo(mask, mod=8)
117
+
118
+ # Normalize: HWC -> CHW, [0,255] -> [0,1]
119
+ image_norm = norm_img(pad_image)
120
+ mask_norm = norm_img(pad_mask)
121
+
122
+ # Binary mask
123
+ mask_norm = (mask_norm > 0) * 1
124
+
125
+ # Convert to tensor and add batch dimension
126
+ image_tensor = torch.from_numpy(image_norm).unsqueeze(0).to(DEVICE)
127
+ mask_tensor = torch.from_numpy(mask_norm).unsqueeze(0).to(DEVICE)
128
+
129
+ # Inference
130
+ with torch.no_grad():
131
+ inpainted = model(image_tensor, mask_tensor)
132
+
133
+ # Convert back to numpy: [1,C,H,W] -> [H,W,C]
134
+ result = inpainted[0].permute(1, 2, 0).cpu().numpy()
135
+ result = np.clip(result * 255, 0, 255).astype(np.uint8)
136
+
137
+ # Crop to original size
138
+ result = result[:orig_h, :orig_w]
139
+
140
+ # Result is RGB, convert to BGR for blending
141
+ result_bgr = cv2.cvtColor(result, cv2.COLOR_RGB2BGR)
142
+
143
+ # Blend: only replace masked area (like original _pad_forward)
144
+ mask_blend = mask[:, :, np.newaxis].astype(np.float32) / 255.0
145
+ image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
146
+ blended = result_bgr * mask_blend + image_bgr * (1 - mask_blend)
147
+ blended = blended.astype(np.uint8)
148
+
149
+ # Convert back to RGB for output
150
+ result_rgb = cv2.cvtColor(blended, cv2.COLOR_BGR2RGB)
151
+
152
+ gc.collect()
153
+ return result_rgb
154
+
155
+
156
+ def process_image(editor_data, progress=None):
157
+ """Process image from Gradio ImageEditor"""
158
+ if editor_data is None:
159
+ return None, "Please upload an image and draw a mask"
160
+
161
+ # Extract image and mask from editor data
162
+ if isinstance(editor_data, dict):
163
+ background = editor_data.get("background")
164
+ layers = editor_data.get("layers", [])
165
+ composite = editor_data.get("composite")
166
+
167
+ if background is None:
168
+ return None, "Please upload an image"
169
+
170
+ # Handle background - could be numpy array or file path
171
+ if isinstance(background, str):
172
+ # File path
173
+ background = np.array(Image.open(background).convert("RGB"))
174
+ elif isinstance(background, np.ndarray):
175
+ # Ensure RGB
176
+ if len(background.shape) == 3 and background.shape[2] == 4:
177
+ background = cv2.cvtColor(background, cv2.COLOR_RGBA2RGB)
178
+ else:
179
+ return None, "Invalid image format"
180
+
181
+ # Get mask from layers
182
+ mask = None
183
+ if layers and len(layers) > 0:
184
+ mask_layer = layers[0]
185
+ if isinstance(mask_layer, str):
186
+ # File path
187
+ mask_img = Image.open(mask_layer)
188
+ if mask_img.mode == "RGBA":
189
+ mask = np.array(mask_img)[:, :, 3] # Use alpha as mask
190
+ else:
191
+ mask = np.array(mask_img.convert("L"))
192
+ elif isinstance(mask_layer, np.ndarray):
193
+ if len(mask_layer.shape) == 3:
194
+ if mask_layer.shape[2] == 4:
195
+ mask = mask_layer[:, :, 3] # Use alpha as mask
196
+ else:
197
+ mask = cv2.cvtColor(mask_layer, cv2.COLOR_RGB2GRAY)
198
+ else:
199
+ mask = mask_layer
200
+
201
+ if mask is None:
202
+ return None, "Please draw a mask on the image"
203
+
204
+ image = background
205
+ else:
206
+ return None, "Invalid input format"
207
+
208
+ # Binarize mask (like original: cv2.threshold)
209
+ _, mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)
210
+
211
+ # Check if mask has any content
212
+ if mask.max() == 0:
213
+ return None, "Please draw a mask on the area you want to remove"
214
+
215
+ # Inpaint
216
+ result = inpaint(image, mask)
217
+
218
+ return result, "Inpainting complete!"
219
+
220
+
221
+ def cli_inpaint(image_path: str, mask_path: str, output_path: str):
222
+ """CLI mode for inpainting"""
223
+ # Load image (RGB)
224
+ image = cv2.imread(image_path)
225
+ if image is None:
226
+ print(f"Error: Could not load image from {image_path}")
227
+ sys.exit(1)
228
+ image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
229
+
230
+ # Load mask (grayscale)
231
+ mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)
232
+ if mask is None:
233
+ print(f"Error: Could not load mask from {mask_path}")
234
+ sys.exit(1)
235
+
236
+ # Binarize mask
237
+ _, mask = cv2.threshold(mask, 127, 255, cv2.THRESH_BINARY)
238
+
239
+ print(f"Input image: {image.shape}")
240
+ print(f"Mask: {mask.shape}")
241
+
242
+ # Inpaint
243
+ result = inpaint(image, mask)
244
+
245
+ # Save result (convert to BGR for cv2.imwrite)
246
+ result_bgr = cv2.cvtColor(result, cv2.COLOR_RGB2BGR)
247
+ cv2.imwrite(output_path, result_bgr)
248
+ print(f"Result saved to {output_path}")
249
+
250
+
251
+ def main():
252
+ parser = argparse.ArgumentParser(description="Lama-Cleaner: Image Inpainting")
253
+ subparsers = parser.add_subparsers(dest="command")
254
+
255
+ # Inpaint command
256
+ inpaint_parser = subparsers.add_parser("inpaint", help="Inpaint an image")
257
+ inpaint_parser.add_argument("-i", "--image", required=True, help="Input image path")
258
+ inpaint_parser.add_argument("-m", "--mask", required=True, help="Mask image path (white = area to inpaint)")
259
+ inpaint_parser.add_argument("-o", "--output", required=True, help="Output image path")
260
+
261
+ args = parser.parse_args()
262
+
263
+ if args.command == "inpaint":
264
+ cli_inpaint(args.image, args.mask, args.output)
265
+ else:
266
+ # No command = launch Gradio UI
267
+ launch_gradio()
268
+
269
+
270
+ def launch_gradio():
271
+ """Launch Gradio UI"""
272
+ import gradio as gr
273
+
274
+ description = """
275
+ # Lama-Cleaner: Image Inpainting
276
+
277
+ Remove unwanted objects from your images using LaMa (Large Mask Inpainting).
278
+
279
+ **How to use:**
280
+ 1. Upload an image
281
+ 2. Draw over the area you want to remove (use the brush tool)
282
+ 3. Click "Remove Object"
283
+ """
284
+
285
+ with gr.Blocks(title="Lama-Cleaner") as demo:
286
+ gr.Markdown(description)
287
+
288
+ with gr.Row():
289
+ with gr.Column():
290
+ image_editor = gr.ImageEditor(
291
+ label="Draw mask on area to remove",
292
+ type="numpy",
293
+ brush=gr.Brush(colors=["#FFFFFF"], default_size=30),
294
+ eraser=gr.Eraser(default_size=30),
295
+ )
296
+ process_btn = gr.Button("Remove Object", variant="primary", size="lg")
297
+
298
+ with gr.Column():
299
+ output_image = gr.Image(label="Result")
300
+ status = gr.Textbox(label="Status", interactive=False)
301
+
302
+ process_btn.click(
303
+ fn=process_image,
304
+ inputs=[image_editor],
305
+ outputs=[output_image, status],
306
+ api_name="inpaint",
307
+ )
308
+
309
+ gr.Markdown("""
310
+ ## Tips
311
+ - Draw a white mask over the area you want to remove
312
+ - For best results, extend the mask slightly beyond the object
313
+ - LaMa works best for small to medium sized areas
314
+ """)
315
+
316
+ demo.queue().launch()
317
+
318
+
319
+ if __name__ == "__main__":
320
+ if len(sys.argv) > 1:
321
+ main()
322
+ else:
323
+ launch_gradio()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ --extra-index-url https://download.pytorch.org/whl/cpu
2
+ torch
3
+ gradio>=6.0.0
4
+ numpy
5
+ opencv-python-headless
6
+ pillow
7
+ huggingface_hub