Skip to content

Commit ecc5df1

Browse files
Harshith ReddyHarshith Reddy
authored andcommitted
Add backend code (no weights), root README with links and contact
1 parent 0ce9f77 commit ecc5df1

File tree

15 files changed

+606
-1
lines changed

15 files changed

+606
-1
lines changed

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,15 @@
1-
# Polyp-Frontend
1+
# Polyp Detection
2+
3+
AI-powered polyp segmentation from colonoscopy images using **DilatedSegNet** (RUPNet): encoder-decoder with ResNet50, dilated convolution pooling, and two model variants (Kvasir-SEG and BKAI-IGH).
4+
5+
**Live app:** [Frontend](https://harshithreddy01.github.io/Polyp-Frontend/)
6+
**API:** [Hugging Face Space](https://huggingface.co/spaces/HarshithReddy01/Polyp_Detection)
7+
8+
**Metrics (reported in training):** Dice coefficient **0.90**, mIoU **0.83**, ~33.68 FPS on GPU.
9+
10+
---
11+
12+
- **Frontend:** React (this repo root) — upload image, choose model, view mask and overlay.
13+
- **Backend:** FastAPI + PyTorch in `/backend` (no weights in repo; see [backend README](backend/README.md) for local run or use the HF link above).
14+
15+
**Contact:** [Harshith Reddy Nalla](https://harshithreddy01.github.io/My-Web/)

backend/Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
FROM python:3.10-slim
2+
3+
RUN useradd -m -u 1000 user
4+
USER user
5+
ENV PATH="/home/user/.local/bin:$PATH"
6+
7+
WORKDIR /app
8+
9+
COPY --chown=user requirements.txt requirements.txt
10+
RUN pip install --no-cache-dir --user -r requirements.txt
11+
12+
COPY --chown=user . /app
13+
14+
EXPOSE 7860
15+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]

backend/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Backend (FastAPI + DilatedSegNet)
2+
3+
This folder contains the API and model code only. **Weight files (`.pth`) are not included.**
4+
5+
- **Use the live API:** [Hugging Face Space](https://huggingface.co/spaces/HarshithReddy01/Polyp_Detection) (no setup).
6+
- **Run locally:** Place `checkpoint-Kvasir-Seg.pth` and/or `checkpoint-BKAI-IGH.pth` in this folder. Download links: [Kvasir-SEG](https://drive.google.com/file/d/1diYckKDMqDWSDD6O5Jm6InCxWEkU0GJC/view?usp=sharing), [BKAI-IGH](https://drive.google.com/file/d/1ojGaQThD56mRhGQaVoJVpAw0oVwSzX8N/view?usp=sharing).
7+
8+
`pip install -r requirements.txt` then `uvicorn main:app --host 0.0.0.0 --port 7860`.

backend/api/__init__.py

Whitespace-only changes.

backend/api/routes.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import torch
2+
from fastapi import APIRouter, File, UploadFile, HTTPException, Query
3+
4+
from core.config import IMAGE_SIZE, CHECKPOINT_MAP
5+
from services import model_service
6+
from utils.image_utils import (
7+
decode_image_from_bytes,
8+
preprocess,
9+
tensor_to_mask_logits,
10+
mask_logits_to_uint8,
11+
mask_to_png_base64,
12+
)
13+
14+
router = APIRouter()
15+
16+
VALID_MODELS = list(CHECKPOINT_MAP.keys())
17+
18+
19+
@router.post("/predict")
20+
async def predict(
21+
file: UploadFile = File(...),
22+
model: str = Query("Kvasir-Seg"),
23+
):
24+
if model not in VALID_MODELS:
25+
raise HTTPException(status_code=400, detail=f"Invalid model. Choose from: {VALID_MODELS}")
26+
if not file.content_type or not file.content_type.startswith("image/"):
27+
raise HTTPException(status_code=400, detail="Expected an image file")
28+
try:
29+
data = await file.read()
30+
except Exception as e:
31+
raise HTTPException(status_code=400, detail=f"Failed to read file: {str(e)}")
32+
if not data:
33+
raise HTTPException(status_code=400, detail="Empty file")
34+
try:
35+
image = decode_image_from_bytes(data)
36+
except ValueError as e:
37+
raise HTTPException(status_code=400, detail=str(e))
38+
tensor = preprocess(image)
39+
logits = model_service.predict(tensor, model)
40+
probs = torch.sigmoid(logits)
41+
pred = tensor_to_mask_logits(probs)
42+
mask = mask_logits_to_uint8(pred, threshold=0.5)
43+
mask_b64 = mask_to_png_base64(mask)
44+
return {"mask": mask_b64, "size": list(IMAGE_SIZE), "model": model}

backend/core/__init__.py

Whitespace-only changes.

backend/core/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import os
2+
from pathlib import Path
3+
4+
import torch as _torch
5+
6+
BASE_DIR = Path(__file__).resolve().parent.parent
7+
MODEL_DIR = BASE_DIR
8+
9+
CHECKPOINT_MAP = {
10+
"Kvasir-Seg": MODEL_DIR / "checkpoint-Kvasir-Seg.pth",
11+
"BKAI-IGH": MODEL_DIR / "checkpoint-BKAI-IGH.pth",
12+
}
13+
14+
IMAGE_SIZE = (256, 256)
15+
DEVICE = "cuda" if _torch.cuda.is_available() else "cpu"

backend/main.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from fastapi import FastAPI
2+
from fastapi.middleware.cors import CORSMiddleware
3+
4+
from api.routes import router
5+
6+
app = FastAPI()
7+
8+
app.add_middleware(
9+
CORSMiddleware,
10+
allow_origins=["*"],
11+
allow_credentials=True,
12+
allow_methods=["*"],
13+
allow_headers=["*"],
14+
)
15+
16+
app.include_router(router)
17+
18+
19+
@app.get("/")
20+
def root():
21+
return {"status": "ok", "model": "RUPNet"}

backend/model.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import torch
2+
import torch.nn as nn
3+
from resnet import resnet50
4+
import numpy as np
5+
import cv2
6+
7+
8+
def save_feats_mean(x, size=(256, 256)):
9+
b, c, h, w = x.shape
10+
with torch.no_grad():
11+
x = x.detach().cpu().numpy()
12+
x = np.transpose(x[0], (1, 2, 0))
13+
x = np.mean(x, axis=-1)
14+
x = x/np.max(x)
15+
x = x * 255.0
16+
x = x.astype(np.uint8)
17+
if h != size[1]:
18+
x = cv2.resize(x, size)
19+
x = cv2.applyColorMap(x, cv2.COLORMAP_JET)
20+
x = np.array(x, dtype=np.uint8)
21+
return x
22+
23+
24+
def get_mean_attention_map(x):
25+
x = torch.mean(x, axis=1)
26+
x = torch.unsqueeze(x, 1)
27+
x = x / torch.max(x)
28+
return x
29+
30+
31+
class ResidualBlock(nn.Module):
32+
def __init__(self, in_c, out_c):
33+
super().__init__()
34+
self.relu = nn.ReLU()
35+
self.conv = nn.Sequential(
36+
nn.Conv2d(in_c, out_c, kernel_size=3, padding=1),
37+
nn.BatchNorm2d(out_c),
38+
nn.ReLU(),
39+
nn.Conv2d(out_c, out_c, kernel_size=3, padding=1),
40+
nn.BatchNorm2d(out_c)
41+
)
42+
self.shortcut = nn.Sequential(
43+
nn.Conv2d(in_c, out_c, kernel_size=1, padding=0),
44+
nn.BatchNorm2d(out_c)
45+
)
46+
47+
def forward(self, inputs):
48+
x1 = self.conv(inputs)
49+
x2 = self.shortcut(inputs)
50+
x = self.relu(x1 + x2)
51+
return x
52+
53+
54+
class DilatedConv(nn.Module):
55+
def __init__(self, in_c, out_c):
56+
super().__init__()
57+
self.c1 = nn.Sequential(
58+
nn.Conv2d(in_c, out_c, kernel_size=3, padding=1, dilation=1),
59+
nn.BatchNorm2d(out_c),
60+
nn.ReLU()
61+
)
62+
self.c2 = nn.Sequential(
63+
nn.Conv2d(in_c, out_c, kernel_size=3, padding=3, dilation=3),
64+
nn.BatchNorm2d(out_c),
65+
nn.ReLU()
66+
)
67+
self.c3 = nn.Sequential(
68+
nn.Conv2d(in_c, out_c, kernel_size=3, padding=6, dilation=6),
69+
nn.BatchNorm2d(out_c),
70+
nn.ReLU()
71+
)
72+
self.c4 = nn.Sequential(
73+
nn.Conv2d(in_c, out_c, kernel_size=3, padding=9, dilation=9),
74+
nn.BatchNorm2d(out_c),
75+
nn.ReLU()
76+
)
77+
self.c5 = nn.Sequential(
78+
nn.Conv2d(out_c*4, out_c, kernel_size=1, padding=0),
79+
nn.BatchNorm2d(out_c),
80+
nn.ReLU()
81+
)
82+
83+
def forward(self, inputs):
84+
x1 = self.c1(inputs)
85+
x2 = self.c2(inputs)
86+
x3 = self.c3(inputs)
87+
x4 = self.c4(inputs)
88+
x = torch.cat([x1, x2, x3, x4], axis=1)
89+
x = self.c5(x)
90+
return x
91+
92+
93+
class ChannelAttention(nn.Module):
94+
def __init__(self, in_planes, ratio=16):
95+
super(ChannelAttention, self).__init__()
96+
self.avg_pool = nn.AdaptiveAvgPool2d(1)
97+
self.max_pool = nn.AdaptiveMaxPool2d(1)
98+
self.fc1 = nn.Conv2d(in_planes, in_planes // 16, 1, bias=False)
99+
self.relu1 = nn.ReLU()
100+
self.fc2 = nn.Conv2d(in_planes // 16, in_planes, 1, bias=False)
101+
self.sigmoid = nn.Sigmoid()
102+
103+
def forward(self, x):
104+
x0 = x
105+
avg_out = self.fc2(self.relu1(self.fc1(self.avg_pool(x))))
106+
max_out = self.fc2(self.relu1(self.fc1(self.max_pool(x))))
107+
out = avg_out + max_out
108+
return x0 * self.sigmoid(out)
109+
110+
111+
class SpatialAttention(nn.Module):
112+
def __init__(self, kernel_size=7):
113+
super(SpatialAttention, self).__init__()
114+
assert kernel_size in (3, 7), 'kernel size must be 3 or 7'
115+
padding = 3 if kernel_size == 7 else 1
116+
self.conv1 = nn.Conv2d(2, 1, kernel_size, padding=padding, bias=False)
117+
self.sigmoid = nn.Sigmoid()
118+
119+
def forward(self, x):
120+
x0 = x
121+
avg_out = torch.mean(x, dim=1, keepdim=True)
122+
max_out, _ = torch.max(x, dim=1, keepdim=True)
123+
x = torch.cat([avg_out, max_out], dim=1)
124+
x = self.conv1(x)
125+
return x0 * self.sigmoid(x)
126+
127+
128+
class DecoderBlock(nn.Module):
129+
def __init__(self, in_c, out_c):
130+
super().__init__()
131+
self.up = nn.Upsample(scale_factor=2, mode="bilinear", align_corners=True)
132+
self.r1 = ResidualBlock(in_c[0]+in_c[1], out_c)
133+
self.r2 = ResidualBlock(out_c, out_c)
134+
self.ca = ChannelAttention(out_c)
135+
self.sa = SpatialAttention()
136+
137+
def forward(self, x, s):
138+
x = self.up(x)
139+
x = torch.cat([x, s], axis=1)
140+
x = self.r1(x)
141+
x = self.r2(x)
142+
x = self.ca(x)
143+
x = self.sa(x)
144+
return x
145+
146+
147+
class RUPNet(nn.Module):
148+
def __init__(self):
149+
super().__init__()
150+
backbone = resnet50(pretrained=False)
151+
self.layer0 = nn.Sequential(backbone.conv1, backbone.bn1, backbone.relu)
152+
self.layer1 = nn.Sequential(backbone.maxpool, backbone.layer1)
153+
self.layer2 = backbone.layer2
154+
self.layer3 = backbone.layer3
155+
self.r1 = nn.Sequential(DilatedConv(64, 64), nn.MaxPool2d((8, 8)))
156+
self.r2 = nn.Sequential(DilatedConv(256, 64), nn.MaxPool2d((4, 4)))
157+
self.r3 = nn.Sequential(DilatedConv(512, 64), nn.MaxPool2d((2, 2)))
158+
self.r4 = DilatedConv(1024, 64)
159+
self.d1 = DecoderBlock([256, 512], 256)
160+
self.d2 = DecoderBlock([256, 256], 128)
161+
self.d3 = DecoderBlock([128, 64], 64)
162+
self.d4 = DecoderBlock([64, 3], 32)
163+
self.y = nn.Conv2d(32, 1, kernel_size=1, padding=0)
164+
165+
def forward(self, x, heatmap=None):
166+
s0 = x
167+
s1 = self.layer0(s0)
168+
s2 = self.layer1(s1)
169+
s3 = self.layer2(s2)
170+
s4 = self.layer3(s3)
171+
r1 = self.r1(s1)
172+
r2 = self.r2(s2)
173+
r3 = self.r3(s3)
174+
r4 = self.r4(s4)
175+
rx = torch.cat([r1, r2, r3, r4], axis=1)
176+
d1 = self.d1(rx, s3)
177+
d2 = self.d2(d1, s2)
178+
d3 = self.d3(d2, s1)
179+
d4 = self.d4(d3, s0)
180+
y = self.y(d4)
181+
if heatmap is not None:
182+
hmap = save_feats_mean(d4)
183+
return hmap, y
184+
else:
185+
return y

backend/requirements.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
fastapi==0.109.2
2+
uvicorn[standard]==0.27.1
3+
python-multipart==0.0.9
4+
torch>=1.9.0
5+
torchvision>=0.10.0
6+
opencv-python-headless>=4.5.0
7+
numpy>=1.21.0

0 commit comments

Comments
 (0)