Harsh7817 commited on
Commit
4fcc94b
·
0 Parent(s):

Initial AI service commit

Browse files
Files changed (7) hide show
  1. .gitignore +44 -0
  2. Dockerfile +17 -0
  3. app.py +102 -0
  4. docker-compose.dev.yml +85 -0
  5. model_loader.py +32 -0
  6. requirements.txt +12 -0
  7. tasks.py +186 -0
.gitignore ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+
24
+ # Virtual Environments
25
+ venv/
26
+ env/
27
+ ENV/
28
+ .env
29
+ .venv
30
+
31
+ # Environment Variables
32
+ .env
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Large Files / Models
39
+ *.pth
40
+ *.pt
41
+ *.onnx
42
+ models/
43
+ data/
44
+ hf_cache/
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+ COPY requirements.txt /app/requirements.txt
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ build-essential git ffmpeg libgl1 \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY . /app
12
+
13
+ ENV PYTHONUNBUFFERED=1
14
+ ENV PYTHONDONTWRITEBYTECODE=1
15
+
16
+ # For GPU: use an nvidia/cuda base image and install the correct torch wheel with CUDA.
17
+ # When running with nvidia runtime, pass --gpus=all to docker run or set deploy settings in compose.
app.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, File, HTTPException
2
+ from fastapi.responses import FileResponse, JSONResponse
3
+ from uuid import uuid4
4
+ from pathlib import Path
5
+ import shutil
6
+ import os
7
+ import json
8
+ import redis
9
+ from celery import Celery
10
+ from dotenv import load_dotenv
11
+
12
+ load_dotenv()
13
+
14
+ # Directories (mounted by docker-compose)
15
+ UPLOAD_DIR = Path(os. environ. get("UPLOAD_DIR", "/data/uploads"))
16
+ RESULT_DIR = Path(os.environ. get("RESULT_DIR", "/data/results"))
17
+ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
18
+ RESULT_DIR.mkdir(parents=True, exist_ok=True)
19
+
20
+ # Redis for job status
21
+ REDIS_HOST = os. environ.get("REDIS_HOST", "redis")
22
+ REDIS_PORT = int(os.environ.get("REDIS_PORT", 6379))
23
+ CELERY_BROKER_URL = os.environ.get("CELERY_BROKER_URL", "redis://redis:6379/0")
24
+
25
+ # Lazy connections
26
+ _rdb = None
27
+ _celery_client = None
28
+
29
+ def get_redis():
30
+ global _rdb
31
+ if _rdb is None:
32
+ _rdb = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0, decode_responses=True)
33
+ return _rdb
34
+
35
+ def get_celery():
36
+ global _celery_client
37
+ if _celery_client is None:
38
+ _celery_client = Celery(broker=CELERY_BROKER_URL)
39
+ return _celery_client
40
+
41
+ app = FastAPI(title="Depth->STL processing service (API)")
42
+
43
+
44
+ def set_status(job_id: str, state: str, detail: str = "", result: str = ""):
45
+ payload = {"state": state, "detail": detail, "result": result}
46
+ get_redis().set(job_id, json.dumps(payload))
47
+
48
+
49
+ @app.post("/upload/")
50
+ async def upload_image(file: UploadFile = File(... )):
51
+ # Basic validation
52
+ if not file.content_type. startswith("image/"):
53
+ raise HTTPException(status_code=400, detail="File must be an image")
54
+
55
+ job_id = str(uuid4())
56
+ safe_name = Path(file.filename).name
57
+ fname = f"{job_id}_{safe_name}"
58
+ save_path = UPLOAD_DIR / fname
59
+
60
+ # Save uploaded file to mounted volume
61
+ try:
62
+ with save_path.open("wb") as buffer:
63
+ shutil.copyfileobj(file.file, buffer)
64
+ except Exception as e:
65
+ raise HTTPException(status_code=500, detail=f"Failed to save upload: {e}")
66
+
67
+ # Mark queued and enqueue Celery task
68
+ set_status(job_id, "QUEUED", "Job received and queued")
69
+ try:
70
+ async_result = get_celery().send_task(
71
+ "tasks.process_image_task",
72
+ args=[str(save_path), str(RESULT_DIR), job_id],
73
+ kwargs={},
74
+ queue=os.environ.get("CELERY_QUEUE", None),
75
+ )
76
+ except Exception as e:
77
+ set_status(job_id, "FAILURE", f"Failed to enqueue task: {e}")
78
+ raise HTTPException(status_code=500, detail=f"Failed to enqueue task: {e}")
79
+
80
+ return {"job_id": job_id, "celery_id": str(async_result.id)}
81
+
82
+
83
+ @app.get("/status/{job_id}")
84
+ def status(job_id: str):
85
+ raw = get_redis().get(job_id)
86
+ if not raw:
87
+ return JSONResponse({"state": "UNKNOWN", "detail": "No such job_id"}, status_code=404)
88
+ return JSONResponse(json.loads(raw))
89
+
90
+
91
+ @app.get("/download/{job_id}")
92
+ def download(job_id: str):
93
+ raw = get_redis().get(job_id)
94
+ if not raw:
95
+ raise HTTPException(status_code=404, detail="No such job")
96
+ info = json.loads(raw)
97
+ if info.get("state") != "SUCCESS":
98
+ raise HTTPException(status_code=404, detail="Result not ready")
99
+ stl_path = info.get("result")
100
+ if not stl_path or not Path(stl_path).exists():
101
+ raise HTTPException(status_code=404, detail="Result file missing")
102
+ return FileResponse(path=stl_path, filename=Path(stl_path).name, media_type="application/sla")
docker-compose.dev.yml ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ redis:
3
+ image: redis:7
4
+ ports:
5
+ - "6379:6379"
6
+ healthcheck:
7
+ test: ["CMD", "redis-cli", "ping"]
8
+ interval: 5s
9
+ timeout: 3s
10
+ retries: 5
11
+
12
+ python-api:
13
+ build:
14
+ context: ./python
15
+ command: ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
16
+ ports:
17
+ - "8000:8000"
18
+ environment:
19
+ - CELERY_BROKER_URL=redis://redis:6379/0
20
+ - CELERY_RESULT_BACKEND=redis://redis:6379/0
21
+ - REDIS_HOST=redis
22
+ - REDIS_PORT=6379
23
+ - UPLOAD_DIR=/data/uploads
24
+ - RESULT_DIR=/data/results
25
+ - DEPTH_CHECKPOINT=/models/depth-anything-Large-hf
26
+ - HF_HUB_OFFLINE=1
27
+ - TRANSFORMERS_OFFLINE=1
28
+ - HF_HUB_DISABLE_TELEMETRY=1
29
+ - OMP_NUM_THREADS=4
30
+ - MKL_NUM_THREADS=4
31
+ volumes:
32
+ - ./python:/app
33
+ - ./data:/data
34
+ - ./models:/models:ro
35
+ depends_on:
36
+ redis:
37
+ condition: service_healthy
38
+
39
+ python-worker:
40
+ build:
41
+ context: ./python
42
+ # GPU will be added by running docker run --gpus all manually (Option 1)
43
+ command: ["celery", "-A", "tasks.celery", "worker", "--loglevel=info", "--pool=solo"]
44
+ environment:
45
+ - CELERY_BROKER_URL=redis://redis:6379/0
46
+ - CELERY_RESULT_BACKEND=redis://redis:6379/0
47
+ - REDIS_HOST=redis
48
+ - REDIS_PORT=6379
49
+ - UPLOAD_DIR=/data/uploads
50
+ - RESULT_DIR=/data/results
51
+ - DEPTH_CHECKPOINT=/models/depth-anything-Large-hf
52
+ - USE_GPU=0
53
+ - POISSON_DEPTH=9
54
+ - OUTLIER_NEIGHBORS=15
55
+ - OUTLIER_STD_RATIO=1.0
56
+ - ORTHO_SCALE_FACTOR=255
57
+ - INFERENCE_RESIZE=0
58
+ - RESULT_PREFIX=
59
+ - HF_HUB_OFFLINE=1
60
+ - TRANSFORMERS_OFFLINE=1
61
+ - HF_HUB_DISABLE_TELEMETRY=1
62
+ - OMP_NUM_THREADS=4
63
+ - MKL_NUM_THREADS=4
64
+ volumes:
65
+ - ./python:/app
66
+ - ./data:/data:rw
67
+ - ./models:/models:ro
68
+ - ./hf_cache:/root/.cache/huggingface:rw
69
+ depends_on:
70
+ redis:
71
+ condition: service_healthy
72
+
73
+ node:
74
+ build:
75
+ context: ./node
76
+ command: ["node", "server.js"]
77
+ ports:
78
+ - "3000:3000"
79
+ environment:
80
+ PYTHON_URL: http://python-api:8000
81
+ MONGODB_URI: mongodb+srv://ironman88103102_db_user:[email protected]/teethnet?retryWrites=true&w=majority&appName=Cluster0
82
+ JWT_SECRET: 3fe4191be8414cac9a2185511b0045400be14cfb2a181cad3969a61594a2246d
83
+ depends_on:
84
+ python-api:
85
+ condition: service_started
model_loader.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ from transformers import AutoImageProcessor, AutoModelForDepthEstimation
4
+
5
+ # Default to the small model; overridden by DEPTH_CHECKPOINT env if set
6
+ _CHECKPOINT = os.environ.get("DEPTH_CHECKPOINT", "/models/depth-anything-small-hf")
7
+
8
+ _MODEL = None
9
+ _PROCESSOR = None
10
+ _DEVICE = None
11
+
12
+ def get_model_and_processor():
13
+ global _MODEL, _PROCESSOR, _DEVICE
14
+ if _MODEL is None or _PROCESSOR is None:
15
+ print("Loading model:", _CHECKPOINT, flush=True)
16
+ # Strongly limit CPU threads to avoid WSL2/Docker oversubscription
17
+ try:
18
+ torch.set_num_threads(max(1, (os.cpu_count() or 2) // 2))
19
+ except Exception:
20
+ pass
21
+
22
+ _PROCESSOR = AutoImageProcessor.from_pretrained(_CHECKPOINT)
23
+ _MODEL = AutoModelForDepthEstimation.from_pretrained(_CHECKPOINT)
24
+
25
+ if torch.cuda.is_available():
26
+ _DEVICE = torch.device("cuda")
27
+ else:
28
+ _DEVICE = torch.device("cpu")
29
+
30
+ _MODEL = _MODEL.to(_DEVICE)
31
+ _MODEL.eval()
32
+ return _MODEL, _PROCESSOR, _DEVICE
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python-multipart
2
+ fastapi
3
+ uvicorn[standard]
4
+ celery[redis]
5
+ redis
6
+ transformers
7
+ torch
8
+ opencv-python-headless
9
+ open3d==0.19.0
10
+ trimesh
11
+ numpy
12
+ python-dotenv
tasks.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from celery import Celery
3
+ from pathlib import Path
4
+ import traceback
5
+ import json
6
+ import redis
7
+ import time
8
+ import sys
9
+
10
+ import numpy as np
11
+ import cv2
12
+ import open3d as o3d
13
+ import torch
14
+ from PIL import Image
15
+ import trimesh
16
+ from transformers import AutoImageProcessor, AutoModelForDepthEstimation
17
+
18
+ # Celery / Redis config
19
+ CELERY_BROKER = os.environ.get("CELERY_BROKER_URL", "redis://redis:6379/0")
20
+ CELERY_BACKEND = os.environ.get("CELERY_RESULT_BACKEND", "redis://redis:6379/0")
21
+ REDIS_HOST = os.environ.get("REDIS_HOST", "redis")
22
+ REDIS_PORT = int(os.environ.get("REDIS_PORT", 6379))
23
+
24
+ # Pipeline settings (fixed to orthographic + Poisson depth=9 to match your notebook)
25
+ DEPTH_CHECKPOINT = os.environ.get("DEPTH_CHECKPOINT", "/models/depth-anything-Large-hf")
26
+ USE_GPU = int(os.environ.get("USE_GPU", "1"))
27
+ POISSON_DEPTH = int(os.environ.get("POISSON_DEPTH", "9"))
28
+ OUTLIER_NEIGHBORS = int(os.environ.get("OUTLIER_NEIGHBORS", "15"))
29
+ OUTLIER_STD_RATIO = float(os.environ.get("OUTLIER_STD_RATIO", "1.0"))
30
+ ORTHO_SCALE_FACTOR = float(os.environ.get("ORTHO_SCALE_FACTOR", "255")) # same as your function
31
+ INFERENCE_RESIZE = int(os.environ.get("INFERENCE_RESIZE", "0")) # 0 keeps original
32
+ RESULT_PREFIX = os.environ.get("RESULT_PREFIX", "")
33
+
34
+ try:
35
+ torch.set_num_threads(max(1, (os.cpu_count() or 2) // 2))
36
+ except Exception:
37
+ pass
38
+
39
+ celery = Celery("tasks", broker=CELERY_BROKER, backend=CELERY_BACKEND)
40
+ rdb = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0, decode_responses=True)
41
+
42
+ _model = None
43
+ _processor = None
44
+ _device = "cpu"
45
+
46
+ def log(msg):
47
+ print(msg, flush=True)
48
+ sys.stdout.flush()
49
+
50
+ def set_status(job_id: str, state: str, detail: str = "", result: str = ""):
51
+ payload = {"state": state, "detail": detail, "result": result}
52
+ rdb.set(job_id, json.dumps(payload))
53
+
54
+ def load_model():
55
+ global _model, _processor, _device
56
+ if _model is None:
57
+ log(f"Loading model: {DEPTH_CHECKPOINT}")
58
+ _processor = AutoImageProcessor.from_pretrained(DEPTH_CHECKPOINT)
59
+ _model = AutoModelForDepthEstimation.from_pretrained(DEPTH_CHECKPOINT)
60
+ if USE_GPU and torch.cuda.is_available():
61
+ _device = "cuda"
62
+ _model = _model.to("cuda")
63
+ else:
64
+ _device = "cpu"
65
+ _model.eval()
66
+ return _model, _processor, _device
67
+
68
+ def normalize_depth_uint8(depth_np: np.ndarray) -> np.ndarray:
69
+ m = np.max(depth_np)
70
+ if m <= 0:
71
+ return np.zeros_like(depth_np, dtype=np.uint8)
72
+ return (depth_np * 255.0 / m).astype("uint8")
73
+
74
+ def build_orthographic_point_cloud(depth_u8: np.ndarray, color_rgb: np.ndarray) -> o3d.geometry.PointCloud:
75
+ depth_map = depth_u8.astype(np.float32)
76
+ h, w = depth_map.shape
77
+ y, x = np.meshgrid(np.arange(h), np.arange(w), indexing='ij')
78
+ z = (depth_map / ORTHO_SCALE_FACTOR) * (h / 2.0)
79
+ points = np.stack((x, y, z), axis=-1).reshape(-1, 3)
80
+ mask = points[:, 2] != 0
81
+ points = points[mask]
82
+ pcd = o3d.geometry.PointCloud()
83
+ pcd.points = o3d.utility.Vector3dVector(points)
84
+ colors = color_rgb.reshape(-1, 3)[mask] / 255.0
85
+ pcd.colors = o3d.utility.Vector3dVector(colors)
86
+ return pcd
87
+
88
+ @celery.task(bind=True)
89
+ def process_image_task(self, image_path: str, result_dir: str, job_id: str):
90
+ start = time.time()
91
+ try:
92
+ set_status(job_id, "RUNNING", "Loading model")
93
+ model, processor, device = load_model()
94
+ log(f"[{job_id}] Model loaded on {device}")
95
+
96
+ img_bgr = cv2.imread(image_path)
97
+ if img_bgr is None:
98
+ raise RuntimeError("Failed to read image")
99
+ img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
100
+ orig_h, orig_w = img_rgb.shape[:2]
101
+
102
+ # Optional resize (not used in your notebook; keep 0 for fidelity)
103
+ if INFERENCE_RESIZE and INFERENCE_RESIZE > 0:
104
+ scale = INFERENCE_RESIZE / max(orig_h, orig_w)
105
+ new_w = int(orig_w * scale)
106
+ new_h = int(orig_h * scale)
107
+ img_proc = cv2.resize(img_rgb, (new_w, new_h), interpolation=cv2.INTER_AREA)
108
+ else:
109
+ img_proc = img_rgb
110
+
111
+ set_status(job_id, "RUNNING", "Running depth inference")
112
+ depth_inputs = processor(images=img_proc, return_tensors="pt").to(device)
113
+ with torch.no_grad():
114
+ outputs = model(**depth_inputs)
115
+ depth = outputs.predicted_depth.squeeze().detach().cpu().numpy()
116
+
117
+ # Match notebook: use depth resolution, resize color to depth size
118
+ dh, dw = depth.shape
119
+ color_resized = cv2.resize(img_proc, (dw, dh), interpolation=cv2.INTER_LINEAR)
120
+
121
+ depth_u8 = normalize_depth_uint8(depth)
122
+
123
+ set_status(job_id, "RUNNING", "Building orthographic point cloud")
124
+ pcd = build_orthographic_point_cloud(depth_u8, color_resized)
125
+
126
+ # Outlier removal (nb=15, std_ratio=1.0)
127
+ try:
128
+ cl, ind = pcd.remove_statistical_outlier(nb_neighbors=OUTLIER_NEIGHBORS,
129
+ std_ratio=OUTLIER_STD_RATIO)
130
+ pcd = pcd.select_by_index(ind)
131
+ except Exception as e:
132
+ log(f"[{job_id}] Outlier removal warning: {e}")
133
+
134
+ # Normals (your notebook: estimate_normals + orient_normals_to_align_with_direction)
135
+ if len(pcd.points) >= 10:
136
+ try:
137
+ pcd.estimate_normals()
138
+ pcd.orient_normals_to_align_with_direction()
139
+ except Exception as e:
140
+ log(f"[{job_id}] Normal estimation warning: {e}")
141
+
142
+ num_pts = np.asarray(pcd.points).shape[0]
143
+ log(f"[{job_id}] Point cloud size after cleanup: {num_pts}")
144
+ if num_pts == 0:
145
+ raise RuntimeError("Empty point cloud after cleanup")
146
+
147
+ set_status(job_id, "RUNNING", f"Poisson reconstruction depth={POISSON_DEPTH}")
148
+ mesh, densities = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(
149
+ pcd, depth=POISSON_DEPTH
150
+ )
151
+
152
+ # Compute normals
153
+ try:
154
+ mesh.compute_vertex_normals()
155
+ except Exception:
156
+ pass
157
+ mesh.compute_triangle_normals()
158
+
159
+ num_vertices = np.asarray(mesh.vertices).shape[0]
160
+ num_tris = np.asarray(mesh.triangles).shape[0]
161
+ log(f"[{job_id}] Mesh stats vertices={num_vertices} triangles={num_tris}")
162
+ if num_tris == 0:
163
+ raise RuntimeError("Poisson produced empty mesh")
164
+
165
+ Path(result_dir).mkdir(parents=True, exist_ok=True)
166
+ stl_path = Path(result_dir) / f"{RESULT_PREFIX}{job_id}.stl"
167
+
168
+ set_status(job_id, "RUNNING", "Exporting STL")
169
+ tm = trimesh.Trimesh(vertices=np.asarray(mesh.vertices),
170
+ faces=np.asarray(mesh.triangles),
171
+ process=True)
172
+ tm.export(str(stl_path), file_type="stl")
173
+
174
+ total = time.time() - start
175
+ set_status(job_id, "SUCCESS", f"Done in {total:.2f}s", str(stl_path))
176
+ log(f"[{job_id}] SUCCESS total={total:.2f}s STL={stl_path}")
177
+ return {
178
+ "status": "success",
179
+ "stl": str(stl_path),
180
+ "mesh_stats": {"vertices": int(num_vertices), "triangles": int(num_tris)}
181
+ }
182
+ except Exception as e:
183
+ traceback.print_exc()
184
+ set_status(job_id, "FAILURE", str(e))
185
+ log(f"[{job_id}] FAILURE: {e}")
186
+ raise