Waikul commited on
Commit
ff90953
·
verified ·
1 Parent(s): 61720bc

Upload 7 files

Browse files
Files changed (7) hide show
  1. Dockerfile +15 -0
  2. LICENSE +21 -0
  3. README.md +26 -12
  4. requirements.txt +8 -0
  5. server.py +121 -0
  6. static/style.css +8 -0
  7. templates/index.html +61 -0
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+ COPY requirements.txt ./requirements.txt
5
+ RUN pip install --no-cache-dir -r requirements.txt
6
+
7
+ COPY server.py ./server.py
8
+ COPY templates ./templates
9
+ COPY static ./static
10
+ COPY README.md ./README.md
11
+ COPY LICENSE ./LICENSE
12
+
13
+ EXPOSE 7860
14
+
15
+ CMD ["sh", "-c", "uvicorn server:app --host 0.0.0.0 --port ${PORT:-7860}"]
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Waikul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
README.md CHANGED
@@ -1,12 +1,26 @@
1
- ---
2
- title: RemoveWatermark
3
- emoji: 🚀
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: hell yeah
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Watermark Remover (Hugging Face Space)
2
+
3
+ A simple FastAPI + OpenCV inpainting service to remove watermarks.
4
+ Deployable for free on Hugging Face Spaces using Docker.
5
+
6
+ ## API
7
+ Endpoint: `POST /api/remove-watermark`
8
+
9
+ ### Fields (multipart/form-data)
10
+ - `image`: file (required)
11
+ - `mask`: file (optional)
12
+ - `engine`: `opencv` (default) or `lama`
13
+ - `method`: `telea` (default) or `ns`
14
+ - `radius`: int (default 3, OpenCV only)
15
+ - `auto_mask`: 1 or 0 (default 1)
16
+
17
+ ### Example
18
+ ```bash
19
+ curl -X POST https://<your-space>.hf.space/api/remove-watermark \
20
+ -F image=@your_image.jpg \
21
+ -F engine=opencv -F method=telea -F radius=3 -F auto_mask=1 \
22
+ --output result.png
23
+ ```
24
+
25
+ ## License
26
+ MIT License
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.112.2
2
+ uvicorn[standard]==0.30.6
3
+ python-multipart==0.0.9
4
+ Jinja2==3.1.4
5
+ opencv-python==4.10.0.84
6
+ numpy==1.26.4
7
+ Pillow==10.4.0
8
+ requests==2.32.3
server.py ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ from typing import Optional
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from PIL import Image
8
+ import requests
9
+ from fastapi import FastAPI, File, Form, UploadFile, HTTPException, Request
10
+ from fastapi.responses import StreamingResponse, HTMLResponse, JSONResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+ from fastapi.templating import Jinja2Templates
13
+
14
+ app = FastAPI(title="Watermark Remover API")
15
+
16
+ # Serve static + templates
17
+ app.mount("/static", StaticFiles(directory="static"), name="static")
18
+ templates = Jinja2Templates(directory="templates")
19
+
20
+ LAMA_URL = os.getenv("LAMA_URL", "http://localhost:5000") # optional
21
+
22
+ def read_image_to_cv2(file: UploadFile) -> np.ndarray:
23
+ data = file.file.read()
24
+ img_arr = np.frombuffer(data, np.uint8)
25
+ img = cv2.imdecode(img_arr, cv2.IMREAD_COLOR)
26
+ if img is None:
27
+ raise HTTPException(400, detail="Invalid image file")
28
+ return img
29
+
30
+ def pil_bytes_from_cv2(img: np.ndarray, fmt: str = "PNG") -> io.BytesIO:
31
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
32
+ pil_img = Image.fromarray(img_rgb)
33
+ buf = io.BytesIO()
34
+ pil_img.save(buf, format=fmt)
35
+ buf.seek(0)
36
+ return buf
37
+
38
+ def auto_text_mask(img: np.ndarray) -> np.ndarray:
39
+ """Simple heuristic mask for text/logo-like overlays."""
40
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
41
+ gray = cv2.equalizeHist(gray)
42
+ mser = cv2.MSER_create(_delta=5, _min_area=60, _max_area=10000)
43
+ regions, _ = mser.detectRegions(gray)
44
+ mask = np.zeros(gray.shape, dtype=np.uint8)
45
+ for p in regions:
46
+ hull = cv2.convexHull(p.reshape(-1, 1, 2))
47
+ cv2.drawContours(mask, [hull], -1, 255, thickness=-1)
48
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
49
+ mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel, iterations=1)
50
+ mask = cv2.dilate(mask, kernel, iterations=1)
51
+ return mask
52
+
53
+ def inpaint_opencv(img: np.ndarray, mask: np.ndarray, method: str = "telea", radius: int = 3) -> np.ndarray:
54
+ flag = cv2.INPAINT_TELEA if method.lower() == "telea" else cv2.INPAINT_NS
55
+ if mask.ndim == 3:
56
+ mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
57
+ _, mask_bin = cv2.threshold(mask, 1, 255, cv2.THRESH_BINARY)
58
+ result = cv2.inpaint(img, mask_bin, radius, flag)
59
+ return result
60
+
61
+ def call_lama_server(img: np.ndarray, mask: np.ndarray) -> np.ndarray:
62
+ if mask.ndim == 3:
63
+ mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
64
+ _, mask_bin = cv2.threshold(mask, 1, 255, cv2.THRESH_BINARY)
65
+
66
+ def to_png_bytes(arr: np.ndarray) -> bytes:
67
+ a = arr
68
+ if a.ndim == 2:
69
+ a = cv2.cvtColor(a, cv2.COLOR_GRAY2BGR)
70
+ ok, buf = cv2.imencode('.png', a)
71
+ if not ok:
72
+ raise HTTPException(500, detail="Encoding error")
73
+ return buf.tobytes()
74
+
75
+ files = {
76
+ 'image': ('image.png', to_png_bytes(img), 'image/png'),
77
+ 'mask': ('mask.png', to_png_bytes(mask_bin), 'image/png'),
78
+ }
79
+ data = {'method': 'lama'}
80
+ try:
81
+ resp = requests.post(f"{LAMA_URL}/inpaint", data=data, files=files, timeout=120)
82
+ resp.raise_for_status()
83
+ except Exception as e:
84
+ raise HTTPException(502, detail=f"LaMa server error: {e}")
85
+ nparr = np.frombuffer(resp.content, np.uint8)
86
+ out = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
87
+ if out is None:
88
+ raise HTTPException(502, detail="Invalid response from LaMa server")
89
+ return out
90
+
91
+ @app.get("/", response_class=HTMLResponse)
92
+ def index(request: Request):
93
+ return templates.TemplateResponse("index.html", {"request": request})
94
+
95
+ @app.get("/health")
96
+ def health():
97
+ return JSONResponse({"ok": True})
98
+
99
+ @app.post("/api/remove-watermark")
100
+ def remove_watermark(
101
+ image: UploadFile = File(...),
102
+ mask: Optional[UploadFile] = File(None),
103
+ engine: str = Form("opencv"), # "opencv" | "lama"
104
+ method: str = Form("telea"), # opencv: telea | ns
105
+ radius: int = Form(3),
106
+ auto_mask: int = Form(1), # 1=true, 0=false
107
+ ):
108
+ img = read_image_to_cv2(image)
109
+
110
+ if mask is not None:
111
+ mask_img = read_image_to_cv2(mask)
112
+ else:
113
+ mask_img = auto_text_mask(img) if auto_mask else np.zeros(img.shape[:2], dtype=np.uint8)
114
+
115
+ if engine == "lama":
116
+ out = call_lama_server(img, mask_img)
117
+ else:
118
+ out = inpaint_opencv(img, mask_img, method=method, radius=radius)
119
+
120
+ buf = pil_bytes_from_cv2(out, fmt="PNG")
121
+ return StreamingResponse(buf, media_type="image/png")
static/style.css ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ body { font-family: system-ui, Arial, sans-serif; padding: 24px; }
2
+ main { max-width: 840px; margin: auto; }
3
+ h1 { margin-bottom: 16px; }
4
+ form label { display: block; margin: 8px 0; }
5
+ .row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
6
+ button { padding: 8px 12px; }
7
+ .hidden { display: none; }
8
+ #output img { max-width: 100%; height: auto; margin-top: 12px; border: 1px solid #ddd; }
templates/index.html ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Watermark Remover</title>
7
+ <link rel="stylesheet" href="/static/style.css" />
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>Watermark Remover</h1>
12
+ <form id="form">
13
+ <label>Image <input type="file" name="image" accept="image/*" required></label>
14
+ <label>Mask (optional) <input type="file" name="mask" accept="image/*"></label>
15
+ <div class="row">
16
+ <label>Engine
17
+ <select name="engine">
18
+ <option value="opencv" selected>OpenCV (fast, free)</option>
19
+ <option value="lama">LaMa (needs separate server)</option>
20
+ </select>
21
+ </label>
22
+ <label>Method
23
+ <select name="method">
24
+ <option value="telea" selected>Telea</option>
25
+ <option value="ns">Navier-Stokes</option>
26
+ </select>
27
+ </label>
28
+ <label>Radius <input type="number" name="radius" value="3" min="1" max="50"></label>
29
+ <label>Auto Mask <input type="checkbox" name="auto_mask" checked></label>
30
+ </div>
31
+ <button type="submit">Remove</button>
32
+ </form>
33
+
34
+ <section id="output" class="hidden">
35
+ <h2>Result</h2>
36
+ <img id="result-img" alt="result" />
37
+ <a id="download" download="result.png">Download</a>
38
+ </section>
39
+ </main>
40
+
41
+ <script>
42
+ const form = document.getElementById('form');
43
+ const out = document.getElementById('output');
44
+ const img = document.getElementById('result-img');
45
+ const dl = document.getElementById('download');
46
+
47
+ form.addEventListener('submit', async (e) => {
48
+ e.preventDefault();
49
+ const data = new FormData(form);
50
+ data.set('auto_mask', form.auto_mask.checked ? 1 : 0);
51
+ const res = await fetch('/api/remove-watermark', { method: 'POST', body: data });
52
+ if (!res.ok) { alert('Failed: ' + res.status); return; }
53
+ const blob = await res.blob();
54
+ const url = URL.createObjectURL(blob);
55
+ img.src = url;
56
+ dl.href = url;
57
+ out.classList.remove('hidden');
58
+ });
59
+ </script>
60
+ </body>
61
+ </html>