ynyg commited on
Commit
f9b81bc
·
1 Parent(s): 2e5c4ba

feat: 初始化项目结构和依赖配置

Browse files
Files changed (8) hide show
  1. .dockerignore +32 -0
  2. .gitignore +48 -0
  3. .python-version +1 -0
  4. Dockerfile +54 -0
  5. app.py +171 -0
  6. main.py +6 -0
  7. pyproject.toml +14 -0
  8. uv.lock +0 -0
.dockerignore ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 忽略虛擬環境和 Python 緩存
2
+ .venv/
3
+ __pycache__/
4
+ *.pyc
5
+ *.pyo
6
+ *.pyd
7
+
8
+ # 忽略深度學習模型目錄 (根據你的目錄名修改,例如 models/ 或 weights/)
9
+ models/
10
+ weights/
11
+ checkpoints/
12
+ .onnx
13
+ *.pth
14
+ *.bin
15
+ *.h5
16
+ *.safetensors
17
+
18
+ # 忽略數據集
19
+ data/
20
+ datasets/
21
+
22
+ # 忽略版本控制和編輯器配置
23
+ .git/
24
+ .idea/
25
+ .vscode/
26
+ .gitignore
27
+ .gitattributes
28
+
29
+ # 忽略日誌和臨時文件
30
+ logs/
31
+ *.log
32
+ tmp/
.gitignore ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /.venv/
2
+
3
+ # Dependency directories
4
+ node_modules/
5
+ jspm_packages/
6
+
7
+ # Build outputs
8
+ dist/
9
+ build/
10
+ out/
11
+
12
+ # Environment variables (Sensitive info)
13
+ .env
14
+ .env.local
15
+ .env.development.local
16
+ .env.test.local
17
+ .env.production.local
18
+
19
+ # Debug logs
20
+ npm-debug.log*
21
+ yarn-debug.log*
22
+ yarn-error.log*
23
+ logs
24
+ *.log
25
+
26
+ # Editor specific folders (Optional but recommended for JetBrains/VSCode)
27
+ .idea/
28
+ .vscode/
29
+ *.swp
30
+ *.bak
31
+
32
+ # OS generated files
33
+ .DS_Store
34
+ Thumbs.db
35
+
36
+ # 忽略深度學習模型目錄 (根據你的目錄名修改,例如 models/ 或 weights/)
37
+ models/
38
+ weights/
39
+ checkpoints/
40
+ .onnx
41
+ *.pth
42
+ *.bin
43
+ *.h5
44
+ *.safetensors
45
+
46
+ # 忽略數據集
47
+ data/
48
+ datasets/
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.14
Dockerfile ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 階段 1:構建環境
2
+ FROM ghcr.io/astral-sh/uv:python3.14-bookworm-slim AS builder
3
+
4
+ # 設置 uv 緩存和編譯環境變量
5
+ ENV UV_COMPILE_BYTECODE=1 \
6
+ UV_LINK_MODE=copy
7
+
8
+ WORKDIR /app
9
+
10
+ # 利用 Docker 層緩存安裝依賴
11
+ # 這裡使用 --mount 綁定 uv.lock 和 pyproject.toml,避免額外的 COPY 動作
12
+ RUN --mount=type=cache,target=/root/.cache/uv \
13
+ --mount=type=bind,source=uv.lock,target=uv.lock \
14
+ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
15
+ uv sync --frozen --no-install-project --no-dev
16
+
17
+ # 階段 2:運行環境
18
+ FROM python:3.14-slim-bookworm
19
+
20
+ # 設置 Python 環境變量
21
+ # PYTHONUNBUFFERED=1: 確保日誌直接輸出而不被緩衝
22
+ # PYTHONDONTWRITEBYTECODE=1: 不在容器內生成 .pyc 文件
23
+ ENV PYTHONUNBUFFERED=1 \
24
+ PYTHONDONTWRITEBYTECODE=1 \
25
+ PATH="/app/.venv/bin:$PATH"
26
+
27
+ WORKDIR /app
28
+
29
+ # 創建非 root 用戶並設置權限
30
+ RUN groupadd -r appuser && useradd -r -g appuser -u 1000 -m appuser && \
31
+ mkdir -p /app/models /app/.cache && \
32
+ chown -R appuser:appuser /app
33
+
34
+ USER appuser
35
+
36
+ # 從構建階段複製虛擬環境
37
+ COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv
38
+
39
+ # 下載模型
40
+ RUN python -c "from huggingface_hub import snapshot_download; \
41
+ snapshot_download(repo_id='ynyg/InkErase', \
42
+ local_dir='/app/models/InkErase', \
43
+ ignore_patterns=['*.ckpt', '*.pth', '*.git*'])"
44
+
45
+ # 複製應用代碼 (建議先複製代碼再啟動)
46
+ # 注意:如果項目很大,建議在 .dockerignore 中排除 .venv, .git 等
47
+ COPY --chown=appuser:appuser . .
48
+
49
+ # 暴露 FastAPI 默認端口或 Hugging Face Spaces 要求的端口
50
+ EXPOSE 7860
51
+
52
+ # 使用虛擬環境中的 uvicorn 啟動
53
+ # 增加 --proxy-headers 處理反向代理(如 Hugging Face 或 Nginx)
54
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--proxy-headers"]
app.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import json
3
+ from contextlib import asynccontextmanager
4
+ from pathlib import Path
5
+
6
+ import albumentations as A
7
+ import cv2
8
+ import numpy as np
9
+ import torch
10
+ from PIL import Image
11
+ from anyio.to_thread import run_sync
12
+ from fastapi import FastAPI, Request, UploadFile, File
13
+ from fastapi.responses import Response
14
+ from segmentation_models_pytorch import UnetPlusPlus
15
+
16
+ # 模型路徑
17
+ MODEL_PATH = "models/InkErase"
18
+ # 設備
19
+ device = "cuda" if torch.cuda.is_available() else "cpu"
20
+ # 分块大小
21
+ TRAIN_SIZE = 512
22
+
23
+
24
+ def load_model() -> UnetPlusPlus:
25
+ """加載模型"""
26
+ # 模型路径
27
+ path = Path(MODEL_PATH)
28
+ # 读取配置文件
29
+ cfg = json.loads((path / "config.json").read_text(encoding="utf-8"))
30
+ # 加載模型
31
+ return UnetPlusPlus(
32
+ encoder_name=cfg.get("encoder_name", "resnet50"),
33
+ encoder_weights=None,
34
+ in_channels=int(cfg.get("in_channels", 3)),
35
+ classes=int(cfg.get("classes", 3)),
36
+ decoder_attention_type=cfg.get("decoder_attention_type"),
37
+ activation=cfg.get("activation", "sigmoid"),
38
+ )
39
+
40
+
41
+ def get_preprocessing() -> A.Compose:
42
+ """获取Albumentations 預處理 pipeline"""
43
+ return A.Compose([
44
+ A.Normalize(mean=(0, 0, 0), std=(1, 1, 1), max_pixel_value=255.0),
45
+ A.ToTensorV2()
46
+ ])
47
+
48
+
49
+ @asynccontextmanager
50
+ async def lifespan(instance: FastAPI):
51
+ """
52
+ FastAPI 應用程序的生命周期管理器。
53
+ :param instance: FastAPI 應用程序實例
54
+ """
55
+ # 加載模型
56
+ instance.state.model = load_model()
57
+ # 初始化預處理函數
58
+ instance.state.preprocess_fn = get_preprocessing()
59
+ yield
60
+
61
+
62
+ app = FastAPI(lifespan=lifespan)
63
+
64
+
65
+ @app.post("/predict")
66
+ async def predict(request: Request, file: UploadFile = File(...)):
67
+ """
68
+ 笔迹擦除
69
+ :param request: 请求对象
70
+ :param file: 待处理的图片
71
+ :return: 預測結果,包括文本、預測類別和置信度
72
+ """
73
+ # 1. 使用 OpenCV 直接從內存讀取圖片
74
+ content = await file.read()
75
+ # 將 bytes 轉換為 numpy array
76
+ nparr = np.frombuffer(content, np.uint8)
77
+ # 解碼圖片 (默認 BGR)
78
+ original_image = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
79
+ # 转换为 RGB
80
+ original_image = cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)
81
+
82
+ # 获取图片尺寸
83
+ orig_h, orig_w = original_image.shape[:2]
84
+ # 获取模型和处理流
85
+ model = request.app.state.model
86
+ preprocess_fn = request.app.state.preprocess_fn
87
+
88
+ def _inference_logic():
89
+ with torch.no_grad():
90
+ # ==============================
91
+ # 情況 A: 圖片大於 512,進行切塊處理
92
+ # ==============================
93
+ if orig_w > TRAIN_SIZE or orig_h > TRAIN_SIZE:
94
+ # 1. 計算新的寬高(補齊為 512 的倍數)
95
+ new_w = (orig_w // TRAIN_SIZE + (1 if orig_w % TRAIN_SIZE != 0 else 0)) * TRAIN_SIZE
96
+ new_h = (orig_h // TRAIN_SIZE + (1 if orig_h % TRAIN_SIZE != 0 else 0)) * TRAIN_SIZE
97
+
98
+ # 2. Padding 原圖
99
+ padded_img = Image.new("RGB", (new_w, new_h), (0, 0, 0))
100
+ padded_img.paste(original_image, (0, 0))
101
+
102
+ result_mask = Image.new("L", (new_w, new_h))
103
+
104
+ # 3. 循環切割
105
+ for y in range(0, new_h, TRAIN_SIZE):
106
+ for x in range(0, new_w, TRAIN_SIZE):
107
+ box = (x, y, x + TRAIN_SIZE, y + TRAIN_SIZE)
108
+ patch = padded_img.crop(box)
109
+
110
+ # --- 修改部分: Albumentations 處理 ---
111
+ patch_np = np.array(patch) # 轉換為 numpy
112
+ transformed = preprocess_fn(image=patch_np)
113
+ input_tensor = transformed["image"].unsqueeze(0).to(device) # [1, C, H, W]
114
+ # -----------------------------------
115
+
116
+ output = model(input_tensor)
117
+
118
+ pred_mask = (output > 0.5).float().squeeze().cpu().numpy()
119
+ pred_mask = (pred_mask * 255).astype(np.uint8)
120
+
121
+ patch_mask_img = Image.fromarray(pred_mask)
122
+ result_mask.paste(patch_mask_img, (x, y))
123
+
124
+ final_image = result_mask.crop((0, 0, orig_w, orig_h))
125
+
126
+ # ==============================
127
+ # 情況 B: 圖片小於等於 512
128
+ # ==============================
129
+ else:
130
+ pad_w = TRAIN_SIZE
131
+ pad_h = TRAIN_SIZE
132
+
133
+ padded_img = Image.new("RGB", (pad_w, pad_h), (0, 0, 0))
134
+ padded_img.paste(original_image, (0, 0))
135
+
136
+ # --- 修改部分: Albumentations 處理 ---
137
+ patch_np = np.array(padded_img) # 轉換為 numpy
138
+ transformed = preprocess_fn(image=patch_np)
139
+ input_tensor = transformed["image"].unsqueeze(0).to(device)
140
+ # -----------------------------------
141
+
142
+ output = model(input_tensor)
143
+
144
+ pred_mask = (output > 0.5).float().squeeze().cpu().numpy()
145
+ pred_mask = (pred_mask * 255).astype(np.uint8)
146
+
147
+ final_image = Image.fromarray(pred_mask).crop((0, 0, orig_w, orig_h))
148
+
149
+ return final_image
150
+
151
+ # 執行推理
152
+ result_image = await run_sync(_inference_logic)
153
+
154
+ # 返回圖片流
155
+ img_byte_arr = io.BytesIO()
156
+ result_image.save(img_byte_arr, format='PNG')
157
+ return Response(content=img_byte_arr.getvalue(), media_type="image/png")
158
+
159
+
160
+ @app.get("/")
161
+ def greet_json():
162
+ """
163
+ 返回一個 JSON 格式的歡迎訊息。
164
+ """
165
+ return {"Hello": "World!"}
166
+
167
+
168
+ if __name__ == '__main__':
169
+ import uvicorn
170
+
171
+ uvicorn.run("app:app", host="0.0.0.0", port=8000)
main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ def main():
2
+ print("Hello from inkerase!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
pyproject.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "inkerase"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "albumentations>=2.0.8",
9
+ "fastapi[all]>=0.128.0",
10
+ "safetensors>=0.7.0",
11
+ "segmentation-models-pytorch>=0.5.0",
12
+ "torch>=2.10.0",
13
+ "torchvision>=0.25.0",
14
+ ]
uv.lock ADDED
The diff for this file is too large to render. See raw diff