changed project structure, merged docker compose files

This commit is contained in:
2026-05-06 17:09:28 +02:00
parent 86b3cd38f4
commit 5c6b4c4b9d
52 changed files with 20 additions and 20 deletions
+17
View File
@@ -0,0 +1,17 @@
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
bash \
&& rm -rf /var/lib/apt/lists/*
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
RUN chmod +x start_server.sh
ENTRYPOINT ["bash", "./start_server.sh"]
+153
View File
@@ -0,0 +1,153 @@
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
from tqdm import tqdm
class SeparableConvolution(nn.Module):
def __init__(self, c_in, c_out, kernel_size):
super().__init__()
self.depthwise = nn.Conv2d(c_in, c_in, kernel_size, groups=c_in, padding=kernel_size//2)
self.bn1 = nn.BatchNorm2d(c_in)
self.pointwise = nn.Conv2d(c_in, c_out, kernel_size=1)
self.bn2 = nn.BatchNorm2d(c_out)
def forward(self, x):
x = self.depthwise(x)
x = self.bn1(x)
x = F.relu(x)
x = self.pointwise(x)
x = self.bn2(x)
x = F.relu(x)
return x
class SkipBlock(nn.Module):
def __init__(self, c_in, c_out, kernel_size=3):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(c_in, c_out, kernel_size, padding=kernel_size//2),
nn.BatchNorm2d(c_out),
nn.ReLU(inplace=True),
nn.Conv2d(c_out, c_out, kernel_size, padding=kernel_size//2),
nn.BatchNorm2d(c_out),
nn.ReLU(inplace=True),
nn.Conv2d(c_out, c_out, kernel_size, padding=kernel_size//2),
nn.BatchNorm2d(c_out),
nn.ReLU(inplace=True)
)
self.conv_skip = nn.Sequential(
nn.Conv2d(c_in, c_out, 1),
nn.BatchNorm2d(c_out),
nn.ReLU(inplace=True)
)
def forward(self, x):
return(F.relu(self.conv_skip(x) + self.conv(x), inplace=True))
class Bird_CNN(nn.Module):
def __init__(self, c_in, c_hidden, c_out):
super().__init__()
self.model = nn.Sequential(
nn.Conv2d(c_in, c_hidden, kernel_size=3, padding=1),
nn.BatchNorm2d(c_hidden),
nn.ReLU(inplace=True),
SkipBlock(c_in=c_hidden, c_out=c_hidden),
SkipBlock(c_in=c_hidden, c_out=c_hidden),
SkipBlock(c_in=c_hidden, c_out=c_hidden),
SkipBlock(c_in=c_hidden, c_out=c_hidden),
SkipBlock(c_in=c_hidden, c_out=c_hidden*2),
SkipBlock(c_in=c_hidden*2, c_out=c_hidden*2),
SkipBlock(c_in=c_hidden*2, c_out=c_hidden*2),
SkipBlock(c_in=c_hidden*2, c_out=c_hidden*2),
nn.Conv2d(c_hidden*2, c_hidden*4, kernel_size=3, padding=1),
nn.ReLU(inplace=True),
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(),
nn.Linear(c_hidden*4, c_out),
nn.Dropout(0.3)
)
def forward(self, x):
return self.model(x)
def trainCNN(model, optimizer, loss_module, train_data_loader, validation_data_loader, device, num_epochs, SAVE_PATH, save=False):
best_val = 0
for epoch in range(num_epochs):
############
# Training #
############
model.train()
true_preds, count = 0, 0
for data_inputs, classes in tqdm(train_data_loader, desc=f"Train Epoch {epoch+1}", leave=False):
data_inputs = data_inputs.to(device)
classes = classes.to(device)
preds = model(data_inputs)
loss = loss_module(preds, classes)
optimizer.zero_grad()
loss.backward()
optimizer.step()
true_preds += (preds.argmax(dim=1) == classes).sum().item()
count += data_inputs.size(0)
train_acc = true_preds / count
torch.cuda.empty_cache()
##############
# Validation #
##############
model.eval()
true_preds, count = 0, 0
for data_inputs, classes in tqdm(validation_data_loader, desc=f"Validate Epoch {epoch+1}", leave=False):
with torch.no_grad():
data_inputs = data_inputs.to(device)
classes = classes.to(device)
preds = model(data_inputs)
loss = loss_module(preds, classes)
true_preds += (preds.argmax(dim=1) == classes).sum().item()
count += data_inputs.size(0)
val_acc = true_preds / count
if(save and best_val < val_acc):
best_val = val_acc
save_dir = os.path.join(SAVE_PATH, "bird_cnn")
os.makedirs(save_dir, exist_ok=True)
save_path = os.path.join(save_dir, f"bird_cnn{epoch+1}")
torch.save(model.state_dict(), save_path)
print(f"epoch: {epoch+1} | train accuracy: {int(train_acc * 1000) / 10}% | validation accuracy: {int(val_acc * 1000) / 10}%")
torch.cuda.empty_cache()
def sample(model, img, device, SAVE_PATH, model_name="bird_cnn", folder="bird_cnn"):
with torch.no_grad():
full_path = os.path.join(SAVE_PATH, folder, model_name)
state_dict = torch.load(full_path, weights_only=False)
model.load_state_dict(state_dict)
model.eval()
img = img.to(device)
pred = model(img)
probs = F.softmax(pred, dim=1)
return(torch.max(probs, dim=1))
Binary file not shown.
+51
View File
@@ -0,0 +1,51 @@
import torch
import torch.nn as nn
import torchvision
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split
import matplotlib.pyplot as plt
from bird_cnn import Bird_CNN, sample, trainCNN
from enum import Enum
from PIL import Image
SAVE_PATH = "./saved_models"
IMAGE_SIZE = 64
class bird_species(Enum):
Common_Kingfisher = 0
CommonMyna = 1
House_Crow = 2
Indian_Peacock = 3
Indian_Pitta = 4
Ruddy_Shelduck = 5
Sarus_Crane = 6
transform = transforms.Compose([
transforms.Resize(IMAGE_SIZE),
transforms.CenterCrop(IMAGE_SIZE),
transforms.ToTensor()
])
dataset = datasets.ImageFolder("data/train", transform=transform)
train_size = int(0.8 * len(dataset))
val_size = len(dataset) - train_size
train_dataset, val_dataset = random_split(dataset, [train_size, val_size])
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
device = torch.device("cpu") if not torch.cuda.is_available() else torch.device("cuda:0")
print("Using device", device)
model = Bird_CNN(c_in=3, c_hidden=32, c_out=7)
model.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
loss_module = nn.CrossEntropyLoss()
trainCNN(model, optimizer, loss_module, train_loader, val_loader, device, 50, SAVE_PATH=SAVE_PATH, save=True)
+6
View File
@@ -0,0 +1,6 @@
fastapi==0.136.1
networkx==3.6.1
numpy==2.3.4
tqdm==4.67.3
uvicorn==0.46.0
python-multipart==0.0.27
+85
View File
@@ -0,0 +1,85 @@
from collections import defaultdict
from enum import Enum
import os
import numpy as np
import matplotlib.pyplot as plt
import cv2
import torch
from torchvision import transforms
import torch.nn.functional as F
from bird_cnn import Bird_CNN
from PIL import Image
BUILD_PATH = "./build_models"
IMAGE_SIZE = 64
class bird_species(Enum):
Common_Kingfisher = 0
CommonMyna = 1
House_Crow = 2
Indian_Peacock = 3
Indian_Pitta = 4
Ruddy_Shelduck = 5
Sarus_Crane = 6
transform = transforms.Compose([
transforms.Resize(IMAGE_SIZE),
transforms.CenterCrop(IMAGE_SIZE),
transforms.ToTensor()
])
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = Bird_CNN(c_in=3, c_hidden=16, c_out=7)
full_path = os.path.join(BUILD_PATH, "bird_cnn")
model.load_state_dict(torch.load(full_path, map_location=torch.device(device)))
model.to(device)
model.eval()
img = cv2.imread("./testimages/two_crows.jpg")
if img is None:
raise ValueError("Image not found or path is wrong")
ss = cv2.ximgproc.segmentation.createSelectiveSearchSegmentation()
ss.setBaseImage(img)
ss.switchToSelectiveSearchFast()
rects = ss.process()
# convert to array for easy sorting
rects = np.array(rects)
# compute area
areas = rects[:, 2] * rects[:, 3]
# sort by area (descending)
idx = np.argsort(-areas)
# take top 10
top10 = rects[idx[:200]]
class_conf_sum = defaultdict(float)
for (x, y, w, h) in top10:
#cv2.rectangle(img_copy, (x, y), (x + w, y + h), (0, 255, 0), 1)
crop = img[y:y+h, x:x+w]
crop_pil = Image.fromarray(cv2.cvtColor(crop, cv2.COLOR_BGR2RGB))
image = transform(crop_pil).unsqueeze(0).to(device)
with torch.no_grad():
pred = model(image)
probs = F.softmax(pred, dim=1)
confidence, cls = torch.max(probs, dim=1)
if confidence.item() < 0.7:
continue
class_conf_sum[cls.item()] += confidence.item()
for i in range(6):
print(str(i) + ": " + str(class_conf_sum[i]))
+75
View File
@@ -0,0 +1,75 @@
from enum import Enum
import os
import torch
from torchvision import transforms
from fastapi import FastAPI, File, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from PIL import Image
import io
import torch.nn.functional as F
from bird_cnn import Bird_CNN
import threading
BUILD_PATH = "./build_models"
IMAGE_SIZE = 64
sem = threading.Semaphore(1) #adjust to performance
class bird_species(Enum):
Common_Kingfisher = 0
CommonMyna = 1
House_Crow = 2
Indian_Peacock = 3
Indian_Pitta = 4
Ruddy_Shelduck = 5
Sarus_Crane = 6
transform = transforms.Compose([
transforms.Resize(IMAGE_SIZE),
transforms.CenterCrop(IMAGE_SIZE),
transforms.ToTensor()
])
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = Bird_CNN(c_in=3, c_hidden=16, c_out=7)
full_path = os.path.join(BUILD_PATH, "bird_cnn")
model.load_state_dict(torch.load(full_path, map_location=torch.device(device)))
model.to(device)
model.eval()
app = FastAPI()
origins = [
"https://marvinkrausser.com",
"https://api.marvinkrausser.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.post("/predict")
async def predict(file: UploadFile = File(...)):
with sem:
image_bytes = await file.read()
image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
image = transform(image).unsqueeze(0).to(device)
with torch.no_grad():
pred = model(image)
probs = F.softmax(pred, dim=1)
confidence, cls = torch.max(probs, dim=1)
return {
"class": bird_species(cls.item()).name,
"confidence": confidence.item()
}
+1
View File
@@ -0,0 +1 @@
python -m uvicorn server:app --reload
+1
View File
@@ -0,0 +1 @@
uvicorn server:app --host 0.0.0.0 --port 8000 --reload
+61
View File
@@ -0,0 +1,61 @@
import torch
import torch.nn as nn
import torchvision
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split
import matplotlib.pyplot as plt
from bird_cnn import Bird_CNN, sample, trainCNN
from enum import Enum
from PIL import Image
SAVE_PATH = "./saved_models"
IMAGE_SIZE = 64
class bird_species(Enum):
Common_Kingfisher = 0
CommonMyna = 1
House_Crow = 2
Indian_Peacock = 3
Indian_Pitta = 4
Ruddy_Shelduck = 5
Sarus_Crane = 6
transform = transforms.Compose([
transforms.Resize(IMAGE_SIZE),
transforms.CenterCrop(IMAGE_SIZE),
transforms.ToTensor()
])
dataset = datasets.ImageFolder("data/train", transform=transform)
train_size = int(0.8 * len(dataset))
val_size = len(dataset) - train_size
train_dataset, val_dataset = random_split(dataset, [train_size, val_size])
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
device = torch.device("cpu") if not torch.cuda.is_available() else torch.device("cuda:0")
print("Using device", device)
model = Bird_CNN(c_in=3, c_hidden=16, c_out=7)
model.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
loss_module = nn.CrossEntropyLoss()
trainCNN(model, optimizer, loss_module, train_loader, val_loader, device, 50, SAVE_PATH=SAVE_PATH, save=True)
exit()
image_path = "test1.jpg"
image = Image.open(image_path).convert("RGB")
image = transform(image)
image = image.unsqueeze(0)
confidence, pred = sample(model=model, img=image, device=device, SAVE_PATH=SAVE_PATH)
print(f"Species: {bird_species(pred.item()).name} | Confidence: {int(confidence.item()*100)/100}")
+41
View File
@@ -0,0 +1,41 @@
import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split
from bird_cnn import Bird_CNN, sample, trainCNN
SAVE_PATH = "./saved_models"
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5),
(0.5, 0.5, 0.5))
])
train_dataset = datasets.CIFAR10(
root="./data/cifar10",
train=True,
download=True,
transform=transform
)
val_dataset = datasets.CIFAR10(
root="./data/cifar10",
train=False,
download=True,
transform=transform
)
train_loader = DataLoader(train_dataset, batch_size=4, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=4, shuffle=False)
device = torch.device("cpu") if not torch.cuda.is_available() else torch.device("cuda:0")
print("Using device", device)
model = Bird_CNN(c_in=3, c_hidden=16, c_out=10)
model.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
loss_module = nn.CrossEntropyLoss()
trainCNN(model, optimizer, loss_module, train_loader, val_loader, device, 50, SAVE_PATH=SAVE_PATH, save=False)
+41
View File
@@ -0,0 +1,41 @@
import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader, random_split
from bird_cnn import Bird_CNN, sample, trainCNN
SAVE_PATH = "./saved_models"
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5),
(0.5))
])
train_dataset = datasets.MNIST(
root="./data/mnist",
train=True,
download=True,
transform=transform
)
val_dataset = datasets.MNIST(
root="./data/mnist",
train=False,
download=True,
transform=transform
)
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False)
device = torch.device("cpu") if not torch.cuda.is_available() else torch.device("cuda:0")
print("Using device", device)
model = Bird_CNN(c_in=1, c_hidden=4, c_out=10)
model.to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4)
loss_module = nn.CrossEntropyLoss()
trainCNN(model, optimizer, loss_module, train_loader, val_loader, device, 50, SAVE_PATH=SAVE_PATH, save=False)
+22
View File
@@ -0,0 +1,22 @@
from PIL import Image
import os
folder = "data/train"
widths = []
heights = []
for root, _, files in os.walk(folder):
for file in files:
if file.endswith((".jpg", ".png", ".jpeg")):
path = os.path.join(root, file)
img = Image.open(path)
w, h = img.size
widths.append(w)
heights.append(h)
avg_w = sum(widths) / len(widths)
avg_h = sum(heights) / len(heights)
print("Average width:", avg_w)
print("Average height:", avg_h)