Ann-Grabetski commited on
Commit
909fab2
·
1 Parent(s): f78216c
.env.example DELETED
@@ -1,3 +0,0 @@
1
- MODEL_DIR=models
2
- MODEL_FILE=model.pt
3
- GRADIO_WATCH_DIRS="planparser,src"
 
 
 
 
README.md CHANGED
@@ -0,0 +1 @@
 
 
1
+ [Lind for data downloading](https://universe.roboflow.com/research-g8szb/floorplan-details-fork)
mvp/api.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # planparser/api.py
2
+ import os
3
+ import io
4
+ from glob import glob
5
+
6
+ from ultralytics import YOLO
7
+ from PIL import Image
8
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
9
+ from pydantic import BaseModel
10
+ from dotenv import load_dotenv
11
+
12
+ load_dotenv()
13
+
14
+ MODEL_DIR = os.getenv("MODEL_DIR")
15
+ if not MODEL_DIR:
16
+ raise RuntimeError("MODEL_DIR is not set")
17
+ MODEL_DIR = os.path.abspath(os.path.expanduser(MODEL_DIR))
18
+
19
+ app = FastAPI(title="Model API")
20
+ _models: dict[str, YOLO] = {}
21
+
22
+
23
+ def _pick_weights(path: str) -> str:
24
+ if os.path.isfile(path):
25
+ return path
26
+ if not os.path.isdir(path):
27
+ raise HTTPException(status_code=400, detail=f"model not found: {path}")
28
+
29
+ for name in ["model.pt", "best.pt"]:
30
+ cand = os.path.join(path, name)
31
+ if os.path.isfile(cand):
32
+ return cand
33
+
34
+ pts = sorted(glob(os.path.join(path, "*.pt")))
35
+ if pts:
36
+ return pts[0]
37
+
38
+ raise HTTPException(status_code=400, detail=f"no .pt weights in: {path}")
39
+
40
+
41
+ def _resolve_weights_path(model_name: str) -> str:
42
+ if not model_name:
43
+ raise HTTPException(status_code=400, detail="model_name is required")
44
+
45
+ p = os.path.abspath(os.path.join(MODEL_DIR, model_name))
46
+ if os.path.commonpath([MODEL_DIR, p]) != MODEL_DIR:
47
+ raise HTTPException(status_code=400, detail="model_name must be inside MODEL_DIR")
48
+
49
+ return _pick_weights(p)
50
+
51
+
52
+ def load_model(weights_path: str) -> YOLO:
53
+ weights_path = os.path.abspath(weights_path)
54
+ m = _models.get(weights_path)
55
+ if m is None:
56
+ m = YOLO(weights_path)
57
+ _models[weights_path] = m
58
+ return m
59
+
60
+
61
+ class Detection(BaseModel):
62
+ class_id: int
63
+ class_name: str
64
+ confidence: float
65
+ xyxy: list[float]
66
+
67
+
68
+ class PredictResponse(BaseModel):
69
+ detections: list[Detection]
70
+
71
+
72
+ @app.get("/health")
73
+ def health():
74
+ return {"ok": True}
75
+
76
+
77
+ @app.post("/predict", response_model=PredictResponse)
78
+ async def predict(
79
+ file: UploadFile = File(...),
80
+ model_name: str = Form(...),
81
+ ):
82
+ weights_path = _resolve_weights_path(model_name)
83
+
84
+ raw = await file.read()
85
+ img = Image.open(io.BytesIO(raw)).convert("RGB")
86
+
87
+ r0 = load_model(weights_path)(img)[0]
88
+ boxes = getattr(r0, "boxes", None)
89
+ if boxes is None:
90
+ raise HTTPException(status_code=500, detail="model output has no boxes (not a detection model?)")
91
+
92
+ names = getattr(r0, "names", {}) or {}
93
+ dets: list[Detection] = []
94
+ for cls_t, conf_t, xyxy_t in zip(boxes.cls, boxes.conf, boxes.xyxy):
95
+ cls_id = int(cls_t.item())
96
+ conf = float(conf_t.item())
97
+ xyxy = [float(x) for x in xyxy_t.tolist()]
98
+ cls_name = names.get(cls_id, str(cls_id)) if isinstance(names, dict) else str(cls_id)
99
+ dets.append(Detection(class_id=cls_id, class_name=cls_name, confidence=conf, xyxy=xyxy))
100
+
101
+ return PredictResponse(detections=dets)
102
+ # uv run uvicorn planparser.api:app --host 0.0.0.0 --port 8000
mvp/app.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # planparser/app.py
2
+ import os
3
+ import io
4
+ import time
5
+
6
+ import gradio as gr
7
+ import requests
8
+ from PIL import Image, ImageDraw
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ API_URL = os.getenv("API_URL")
14
+ if not API_URL:
15
+ raise RuntimeError("API_URL is not set")
16
+
17
+ EXAMPLE_1 = os.getenv("EXAMPLE_1")
18
+ EXAMPLE_2 = os.getenv("EXAMPLE_2")
19
+
20
+ MODEL_MAP = {
21
+ "yolo11n": os.getenv("MODEL_1"),
22
+ "yolo11s": os.getenv("MODEL_2"),
23
+ "yolo11l": os.getenv("MODEL_3"),
24
+ }
25
+
26
+ MODEL_CHOICES = list(MODEL_MAP.keys())
27
+ DEFAULT_MODEL = MODEL_CHOICES[0]
28
+
29
+
30
+ def _draw_detections(img: Image.Image, dets: list[dict]) -> Image.Image:
31
+ out = img.copy().convert("RGB")
32
+ d = ImageDraw.Draw(out)
33
+
34
+ for det in dets:
35
+ x1, y1, x2, y2 = det["xyxy"]
36
+ d.rectangle([x1, y1, x2, y2], width=2)
37
+ txt = f'{det["class_name"]} {det["confidence"]:.2f}'
38
+ d.text((x1, max(0, y1 - 12)), txt)
39
+
40
+ return out
41
+
42
+
43
+ def predict(model_label: str, img: Image.Image):
44
+ if img is None or not model_label:
45
+ return None, [], ""
46
+
47
+ buf = io.BytesIO()
48
+ img.convert("RGB").save(buf, format="JPEG")
49
+ buf.seek(0)
50
+
51
+ t0 = time.perf_counter()
52
+ r = requests.post(
53
+ f"{API_URL}/predict",
54
+ files={"file": ("image.jpg", buf, "image/jpeg")},
55
+ data={"model_name": MODEL_MAP[model_label]},
56
+ timeout=60,
57
+ )
58
+ dt = time.perf_counter() - t0
59
+
60
+ if r.status_code != 200:
61
+ return None, {"error": r.text}, f"{dt:.3f} s"
62
+
63
+ data = r.json()
64
+ dets = data.get("detections", []) or []
65
+ vis = _draw_detections(img, dets)
66
+
67
+ return vis, dets, f"{dt:.3f} s"
68
+
69
+
70
+ with gr.Blocks(title="YOLO detection demo") as demo:
71
+ gr.Markdown("# YOLO detection demo")
72
+
73
+ model_dd = gr.Dropdown(
74
+ choices=MODEL_CHOICES,
75
+ value=DEFAULT_MODEL,
76
+ label="Model",
77
+ )
78
+
79
+ with gr.Row():
80
+ with gr.Column(scale=1):
81
+ img_in = gr.Image(
82
+ type="pil",
83
+ label="Image",
84
+ sources=["upload"],
85
+ height=200,
86
+ )
87
+
88
+ ex = [p for p in [EXAMPLE_1, EXAMPLE_2] if p]
89
+ if ex:
90
+ gr.Examples(examples=ex, inputs=img_in)
91
+
92
+ btn = gr.Button("Submit")
93
+
94
+ with gr.Column(scale=2):
95
+ out_img = gr.Image(type="pil", label="Result", height=600)
96
+ out_json = gr.JSON(label="Detections")
97
+ out_time = gr.Textbox(label="Processing time")
98
+
99
+ btn.click(predict, inputs=[model_dd, img_in], outputs=[out_img, out_json, out_time])
100
+
101
+ if __name__ == "__main__":
102
+ demo.launch(server_name="0.0.0.0", server_port=7860)
103
+ # uv run gradio planparser/app.py
mvp/utils/images.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from PIL import Image
3
+
4
+ def to_white_bg(in_path: str | Path, out_path: str | Path) -> None:
5
+ in_path = Path(in_path)
6
+ out_path = Path(out_path)
7
+
8
+ im = Image.open(in_path)
9
+ if im.mode in ("RGBA", "LA") or ("transparency" in im.info):
10
+ im = im.convert("RGBA")
11
+ white = Image.new("RGBA", im.size, (255, 255, 255, 255))
12
+ out = Image.alpha_composite(white, im).convert("RGB")
13
+ else:
14
+ out = im.convert("RGB")
15
+
16
+ out_path.parent.mkdir(parents=True, exist_ok=True)
17
+ out.save(out_path)
notebooks/choose_model.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
planparser/api.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # planparser/api.py
2
+ import os
3
+ import io
4
+ from glob import glob
5
+
6
+ from ultralytics import YOLO
7
+ from PIL import Image
8
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
9
+ from pydantic import BaseModel
10
+ from dotenv import load_dotenv
11
+
12
+ load_dotenv()
13
+
14
+ MODEL_DIR = os.getenv("MODEL_DIR")
15
+ if not MODEL_DIR:
16
+ raise RuntimeError("MODEL_DIR is not set")
17
+ MODEL_DIR = os.path.abspath(os.path.expanduser(MODEL_DIR))
18
+
19
+ app = FastAPI(title="Model API")
20
+ _models: dict[str, YOLO] = {}
21
+
22
+
23
+ def _pick_weights(path: str) -> str:
24
+ if os.path.isfile(path):
25
+ return path
26
+ if not os.path.isdir(path):
27
+ raise HTTPException(status_code=400, detail=f"model not found: {path}")
28
+
29
+ for name in ["model.pt", "best.pt"]:
30
+ cand = os.path.join(path, name)
31
+ if os.path.isfile(cand):
32
+ return cand
33
+
34
+ pts = sorted(glob(os.path.join(path, "*.pt")))
35
+ if pts:
36
+ return pts[0]
37
+
38
+ raise HTTPException(status_code=400, detail=f"no .pt weights in: {path}")
39
+
40
+
41
+ def _resolve_weights_path(model_name: str) -> str:
42
+ if not model_name:
43
+ raise HTTPException(status_code=400, detail="model_name is required")
44
+
45
+ p = os.path.abspath(os.path.join(MODEL_DIR, model_name))
46
+ if os.path.commonpath([MODEL_DIR, p]) != MODEL_DIR:
47
+ raise HTTPException(status_code=400, detail="model_name must be inside MODEL_DIR")
48
+
49
+ return _pick_weights(p)
50
+
51
+
52
+ def load_model(weights_path: str) -> YOLO:
53
+ weights_path = os.path.abspath(weights_path)
54
+ m = _models.get(weights_path)
55
+ if m is None:
56
+ m = YOLO(weights_path)
57
+ _models[weights_path] = m
58
+ return m
59
+
60
+
61
+ class Detection(BaseModel):
62
+ class_id: int
63
+ class_name: str
64
+ confidence: float
65
+ xyxy: list[float]
66
+
67
+
68
+ class PredictResponse(BaseModel):
69
+ detections: list[Detection]
70
+
71
+
72
+ @app.get("/health")
73
+ def health():
74
+ return {"ok": True}
75
+
76
+
77
+ @app.post("/predict", response_model=PredictResponse)
78
+ async def predict(
79
+ file: UploadFile = File(...),
80
+ model_name: str = Form(...),
81
+ ):
82
+ weights_path = _resolve_weights_path(model_name)
83
+
84
+ raw = await file.read()
85
+ img = Image.open(io.BytesIO(raw)).convert("RGB")
86
+
87
+ r0 = load_model(weights_path)(img)[0]
88
+ boxes = getattr(r0, "boxes", None)
89
+ if boxes is None:
90
+ raise HTTPException(status_code=500, detail="model output has no boxes (not a detection model?)")
91
+
92
+ names = getattr(r0, "names", {}) or {}
93
+ dets: list[Detection] = []
94
+ for cls_t, conf_t, xyxy_t in zip(boxes.cls, boxes.conf, boxes.xyxy):
95
+ cls_id = int(cls_t.item())
96
+ conf = float(conf_t.item())
97
+ xyxy = [float(x) for x in xyxy_t.tolist()]
98
+ cls_name = names.get(cls_id, str(cls_id)) if isinstance(names, dict) else str(cls_id)
99
+ dets.append(Detection(class_id=cls_id, class_name=cls_name, confidence=conf, xyxy=xyxy))
100
+
101
+ return PredictResponse(detections=dets)
102
+ # uv run uvicorn planparser.api:app --host 0.0.0.0 --port 8000
planparser/app.py CHANGED
@@ -1,42 +1,103 @@
 
1
  import os
 
 
 
2
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- MODEL_DIR = os.getenv("MODEL_DIR", "models")
5
- MODEL_FILE = os.getenv("MODEL_FILE", "model.pt")
6
 
7
- _model = None
 
 
8
 
 
9
 
10
- def get_model_path() -> str:
11
- path = os.path.join(MODEL_DIR, MODEL_FILE)
12
- if not os.path.exists(path):
13
- raise FileNotFoundError(
14
- f"Model not found: {path}. Put the file there or set MODEL_DIR and MODEL_FILE."
15
- )
16
- return path
17
 
 
 
18
 
19
- def load_model():
20
- global _model
21
- if _model is not None:
22
- return _model
23
- import torch
24
 
25
- _model = torch.jit.load(get_model_path(), map_location="cpu").eval()
26
- return _model
 
 
 
 
 
 
27
 
 
 
 
28
 
29
- def predict(text: str) -> str:
30
- _ = load_model()
31
- return f"Loaded local model: {os.path.join(MODEL_DIR, MODEL_FILE)}. Input: {text}"
32
 
 
 
 
 
33
 
34
- demo = gr.Interface(
35
- fn=predict,
36
- inputs=gr.Textbox(label="Input"),
37
- outputs=gr.Textbox(label="Output"),
38
- title="Minimal demo",
39
- )
40
 
41
  if __name__ == "__main__":
42
  demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
1
+ # planparser/app.py
2
  import os
3
+ import io
4
+ import time
5
+
6
  import gradio as gr
7
+ import requests
8
+ from PIL import Image, ImageDraw
9
+ from dotenv import load_dotenv
10
+
11
+ load_dotenv()
12
+
13
+ API_URL = os.getenv("API_URL")
14
+ if not API_URL:
15
+ raise RuntimeError("API_URL is not set")
16
+
17
+ EXAMPLE_1 = os.getenv("EXAMPLE_1")
18
+ EXAMPLE_2 = os.getenv("EXAMPLE_2")
19
+
20
+ MODEL_MAP = {
21
+ "yolo11n": os.getenv("MODEL_1"),
22
+ "yolo11s": os.getenv("MODEL_2"),
23
+ "yolo11l": os.getenv("MODEL_3"),
24
+ }
25
+
26
+ MODEL_CHOICES = list(MODEL_MAP.keys())
27
+ DEFAULT_MODEL = MODEL_CHOICES[0]
28
+
29
+
30
+ def _draw_detections(img: Image.Image, dets: list[dict]) -> Image.Image:
31
+ out = img.copy().convert("RGB")
32
+ d = ImageDraw.Draw(out)
33
+
34
+ for det in dets:
35
+ x1, y1, x2, y2 = det["xyxy"]
36
+ d.rectangle([x1, y1, x2, y2], width=2)
37
+ txt = f'{det["class_name"]} {det["confidence"]:.2f}'
38
+ d.text((x1, max(0, y1 - 12)), txt)
39
+
40
+ return out
41
+
42
+
43
+ def predict(model_label: str, img: Image.Image):
44
+ if img is None or not model_label:
45
+ return None, [], ""
46
+
47
+ buf = io.BytesIO()
48
+ img.convert("RGB").save(buf, format="JPEG")
49
+ buf.seek(0)
50
+
51
+ t0 = time.perf_counter()
52
+ r = requests.post(
53
+ f"{API_URL}/predict",
54
+ files={"file": ("image.jpg", buf, "image/jpeg")},
55
+ data={"model_name": MODEL_MAP[model_label]},
56
+ timeout=60,
57
+ )
58
+ dt = time.perf_counter() - t0
59
 
60
+ if r.status_code != 200:
61
+ return None, {"error": r.text}, f"{dt:.3f} s"
62
 
63
+ data = r.json()
64
+ dets = data.get("detections", []) or []
65
+ vis = _draw_detections(img, dets)
66
 
67
+ return vis, dets, f"{dt:.3f} s"
68
 
 
 
 
 
 
 
 
69
 
70
+ with gr.Blocks(title="YOLO detection demo") as demo:
71
+ gr.Markdown("# YOLO detection demo")
72
 
73
+ model_dd = gr.Dropdown(
74
+ choices=MODEL_CHOICES,
75
+ value=DEFAULT_MODEL,
76
+ label="Model",
77
+ )
78
 
79
+ with gr.Row():
80
+ with gr.Column(scale=1):
81
+ img_in = gr.Image(
82
+ type="pil",
83
+ label="Image",
84
+ sources=["upload"],
85
+ height=200,
86
+ )
87
 
88
+ ex = [p for p in [EXAMPLE_1, EXAMPLE_2] if p]
89
+ if ex:
90
+ gr.Examples(examples=ex, inputs=img_in)
91
 
92
+ btn = gr.Button("Submit")
 
 
93
 
94
+ with gr.Column(scale=2):
95
+ out_img = gr.Image(type="pil", label="Result", height=600)
96
+ out_json = gr.JSON(label="Detections")
97
+ out_time = gr.Textbox(label="Processing time")
98
 
99
+ btn.click(predict, inputs=[model_dd, img_in], outputs=[out_img, out_json, out_time])
 
 
 
 
 
100
 
101
  if __name__ == "__main__":
102
  demo.launch(server_name="0.0.0.0", server_port=7860)
103
+ # uv run gradio planparser/app.py
pyproject.toml CHANGED
@@ -6,6 +6,14 @@ readme = "README.md"
6
  requires-python = ">=3.12"
7
  dependencies = [
8
  "gradio>=6.2.0",
 
9
  "torch>=2.9.1",
10
  "torchvision>=0.24.1",
 
 
 
 
 
 
 
11
  ]
 
6
  requires-python = ">=3.12"
7
  dependencies = [
8
  "gradio>=6.2.0",
9
+ "requests>=2.32.5",
10
  "torch>=2.9.1",
11
  "torchvision>=0.24.1",
12
+ "ultralytics>=8.3.243",
13
+ ]
14
+
15
+ [dependency-groups]
16
+ dev = [
17
+ "jupyterlab>=4.5.1",
18
+ "python-dotenv>=1.2.1",
19
  ]
src/data/data.yaml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ train: ../train/images
2
+ val: ../valid/images
3
+ test: ../test/images
4
+
5
+ nc: 15
6
+ names: ['bathtub', 'bed', 'bed2', 'chair', 'door', 'door2', 'shower', 'sink', 'sofa1', 'sofa2', 'sofa3', 'stove', 'table', 'toilet', 'vanity']
7
+
8
+ roboflow:
9
+ workspace: research-g8szb
10
+ project: floorplan-details-fork
11
+ version: 1
12
+ license: CC BY 4.0
13
+ url: https://universe.roboflow.com/research-g8szb/floorplan-details-fork/dataset/1
src/examples/example1.jpg ADDED

Git LFS Details

  • SHA256: c7a3c9aeebb5b7c877169624c871338b8fe6f4ab0ce80305e898b227046213a2
  • Pointer size: 131 Bytes
  • Size of remote file: 137 kB
src/examples/example2.jpg ADDED

Git LFS Details

  • SHA256: a9bbb8957f07633302de7088f21ed974eb569934f00af0895ac32d7c0b3e37ab
  • Pointer size: 130 Bytes
  • Size of remote file: 21.9 kB