Skip to content

Commit

Permalink
Merge pull request #3 from anras5/feature/webapp
Browse files Browse the repository at this point in the history
Feature/webapp
  • Loading branch information
anras5 authored May 5, 2023
2 parents b0daa35 + c6cc48c commit c0e5e5e
Show file tree
Hide file tree
Showing 9 changed files with 425 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea/
temp/
unet_checkpoint.pth
146 changes: 145 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
@@ -1 +1,145 @@
# LOOK INTO notebooks/main.ipynb
import uuid
import os
from typing import Tuple

import cv2
import numpy as np

from flask import Flask, render_template, redirect, url_for, flash
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired, FileAllowed
from wtforms import SubmitField, BooleanField
from werkzeug.utils import secure_filename

from notebooks.classifiers.classic import ClassicClassifier
from notebooks.classifiers.unet import UNetClassifier
from notebooks.utils.confusion_matrix import ConfusionMatrix

app = Flask(__name__)
app.config.from_mapping(
SECRET_KEY='dev',
)


class InputForm(FlaskForm):
input_image = FileField("Prześlij plik",
validators=[FileRequired(), FileAllowed(['jpg', 'png'])])
input_map = FileField("Prześlij mapę ekspercką (opcjonalne)",
validators=[FileAllowed(['jpg', 'png'])])
submit = SubmitField()


def predict_classic(image_path: str, user_path: str, true_path: str = '') -> Tuple[str, ConfusionMatrix]:
cc = ClassicClassifier([(5, 5), (12, 12), (20, 20)], 200)
image = cv2.imread(image_path)
pred_image = cc.predict(image)

back = '\\'
pred_path = f"{image_path.split(back)[-1].split('.')[0]}_pred_classic.png"
pred_path = os.path.join(user_path, pred_path)
cv2.imwrite(pred_path, pred_image)

if true_path:
true_image = cv2.imread(true_path, cv2.IMREAD_GRAYSCALE)
cm = ConfusionMatrix(true_image.flatten(), pred_image.flatten())
return pred_path, cm
else:
return pred_path, None


def predict_unet(image_path: str, user_path: str, true_path: str = '') -> Tuple[str, ConfusionMatrix]:

def mask_parse(mask):
mask = np.expand_dims(mask, axis=-1) # (512, 512, 1)
mask = np.concatenate([mask, mask, mask], axis=-1) # (512, 512, 3)
return mask

uc = UNetClassifier('models/unet_checkpoint.pth')
image = cv2.imread(image_path, cv2.IMREAD_COLOR)
y_pred = uc.predict(image)

pred_image = mask_parse(y_pred)
back = '\\'
pred_path = f"{image_path.split(back)[-1].split('.')[0]}_pred_unet.png"
pred_path = os.path.join(user_path, pred_path)
cv2.imwrite(pred_path, pred_image * 255)

if true_path:
true_image = cv2.imread(true_path, cv2.IMREAD_GRAYSCALE)
answer = cv2.resize(true_image, dsize=(512, 512), interpolation=cv2.INTER_CUBIC)
ans = answer / 255.0
ans = ans[np.newaxis, ...]
y_true = ans > 0.5
y_true = y_true.astype(np.uint8)
y_true = y_true.reshape(-1)

cm = ConfusionMatrix(y_true, y_pred)

return pred_path, cm
else:
return pred_path, None


@app.route("/", methods=['GET', 'POST'])
def home():
form = InputForm()
if form.validate_on_submit():
# ------------------------------------------------------------------------------------------------------------ #
# HANDLE UPLOADED FILES

# get user's unique id
uid = str(uuid.uuid4())
user_path = os.path.join('static/temp', uid)
os.makedirs(user_path)

# get files from form
f = form.input_image.data
f_filename = secure_filename(f.filename)
image_path = os.path.join(user_path, f_filename)
f.save(image_path)

f_m = form.input_map.data
if f_m is not None:
f_m_filename = secure_filename(f_m.filename)
map_path = os.path.join(user_path, f_m_filename)
f_m.save(map_path)

classic_path, cm_classic = predict_classic(image_path, user_path, map_path)
unet_path, cm_unet = predict_unet(image_path, user_path, map_path)
else:
classic_path, cm_classic = predict_classic(image_path, user_path)
unet_path, cm_unet = predict_unet(image_path, user_path)

# ------------------------------------------------------------------------------------------------------------ #
# RESIZE UPLOADED FILES TO 512x512

img = cv2.imread(image_path, cv2.IMREAD_COLOR)
img = cv2.resize(img, dsize=(512, 512), interpolation=cv2.INTER_CUBIC)
cv2.imwrite(image_path, img)

if f_m is not None:
ans = cv2.imread(map_path, cv2.IMREAD_GRAYSCALE)
ans = cv2.resize(ans, dsize=(512, 512), interpolation=cv2.INTER_CUBIC)
cv2.imwrite(map_path, ans)

pred = cv2.imread(classic_path, cv2.IMREAD_GRAYSCALE)
pred = cv2.resize(pred, dsize=(512, 512), interpolation=cv2.INTER_CUBIC)
cv2.imwrite(classic_path, pred)

if f_m is not None:
return render_template('result.html',
image_path=image_path, map_path=map_path,
classic_path=classic_path, unet_path=unet_path,
cm_classic=cm_classic, cm_unet=cm_unet)
else:
return render_template('result.html',
image_path=image_path,
classic_path=classic_path, unet_path=unet_path,
cm_classic=cm_classic, cm_unet=cm_unet)

print(form.errors)
return render_template('index.html', form=form)


if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port='5000')
35 changes: 35 additions & 0 deletions notebooks/classifiers/unet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import torch
import cv2
import numpy as np
from notebooks.unet.unet import UNet


def mask_parse(mask):
mask = np.expand_dims(mask, axis=-1) # (512, 512, 1)
mask = np.concatenate([mask, mask, mask], axis=-1) # (512, 512, 3)
return mask


class UNetClassifier:

def __init__(self, model_path):
self.model = UNet(3)
self.model.load_state_dict(torch.load(model_path, map_location='cuda'))
self.model.eval()

def predict(self, image: np.ndarray) -> np.ndarray:
image = cv2.resize(image, dsize=(512, 512), interpolation=cv2.INTER_CUBIC)
img = image / 255.0
img = np.transpose(img, (2, 0, 1))
img = img[np.newaxis, ...]
img = torch.from_numpy(img.astype(np.float32))

with torch.no_grad():
pred_y = self.model(img)

pred_y = pred_y[0].cpu().numpy()
pred_y = np.squeeze(pred_y, axis=0)
pred_y = pred_y > 0.5
pred_y = np.array(pred_y, dtype=np.uint8)

return pred_y
8 changes: 4 additions & 4 deletions notebooks/utils/confusion_matrix.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ def __init__(self, true, pred):
columns=['Predicted Negative', 'Predicted Positive'])

def __str__(self):
return f"{'Accuracy:'.ljust(13, ' ')}{self.accuracy}\n" \
f"{'Recall:'.ljust(13, ' ')}{self.recall}\n" \
f"{'Precision'.ljust(13, ' ')}{self.precision}\n" \
f"{'Specificity:'.ljust(13, ' ')}{self.specificity}\n"
return f"{'Accuracy:'.ljust(13, ' ')}{self.accuracy:.3f}\n" \
f"{'Recall:'.ljust(13, ' ')}{self.recall:.3f}\n" \
f"{'Precision'.ljust(13, ' ')}{self.precision:.3f}\n" \
f"{'Specificity:'.ljust(13, ' ')}{self.specificity:.3f}\n"

def heatmap(self, annot=True, fmt=".0f", norm=LogNorm(), cmap='crest', cbar_kws=None):
if cbar_kws is None:
Expand Down
Binary file modified requirements.txt
Binary file not shown.
41 changes: 41 additions & 0 deletions static/css/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
h1 {
padding: 2%;
}

html, body {
height: 100%;
}

body {
display: flex;
flex-direction: column;
align-content: center;
text-align: center;
font-family: 'Montserrat', sans-serif;
}

.content {
flex: 1 0 auto;
}

.footer {
flex-shrink: 0;
}

.img-box img {
object-fit: contain;
}


/*SCROLL BAR*/
::-webkit-scrollbar {
display: none;
}

::-webkit-scrollbar-track {
display: none;
}

::-webkit-scrollbar-thumb {
display: none;
}
46 changes: 46 additions & 0 deletions templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{% extends "layout.html" %}

{% block content %}

<h1 class="mt-2">WYKRYWANIE NACZYŃ KRWIONOŚNYCH DNA OKA</h1>

<p>Prześlij swój obraz korzystając poniższego formularza:</p>
<div class="container form">
<form method="post" class="row g-3" enctype="multipart/form-data" id="photoForm">
{{ form.hidden_tag() }}
<div class="row my-3">
<div class="col-12 mx-auto">
{{ form.input_image.label(class_='form-label') }}
{{ form.input_image(class_='form-control') }}
</div>
</div>
<div class="row my-3">
<div class="col-12 mx-auto">
{{ form.input_map.label(class_='form-label') }} <br>
{{ form.input_map(class_='form-control') }}
</div>
</div>
<div class="row my-2">
<div class="col-12 mx-auto" id="submit-btn">
{{ form.submit(class_='btn btn-success') }}
</div>
</div>
</form>
<div class="col-12 d-none mt-3" id="spinner">
<div class="spinner-border text-warning" role="status"></div>
<p>Twoje żądanie jest przetwarzane. Nie zamykaj okna przeglądarki.</p>
</div>
</div>

{% endblock %}

{% block js %}

<script>
document.getElementById("photoForm").addEventListener("submit", (event) => {
document.getElementById("spinner").classList.remove("d-none");
document.getElementById("submit-btn").classList.add("d-none");
});

</script>
{% endblock %}
83 changes: 83 additions & 0 deletions templates/layout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Blood Vessels</title>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp" crossorigin="anonymous">

{#Google Fonts#}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet">

{#custom css#}
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">

{# Notie #}
<link rel="stylesheet" type="text/css" href="https://unpkg.com/notie/dist/notie.min.css">

{#Font awesome#}
<script src="https://kit.fontawesome.com/bd9dc0e408.js" crossorigin="anonymous"></script>
</head>
<body>
<div class="content mb-3">
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href={{ url_for('home') }}><i class="fa-solid fa-eye"></i></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="{{ url_for('home') }}">Strona główna</a>
</li>
</ul>
</div>
</div>
</nav>

{% block content %} {% endblock %}
</div>

<footer class="footer mt-auto py-3 bg-body-tertiary">
<div class="container">
<span class="text-body-secondary">Filip Marciniak, Szymon Pasternak</span><br>
<span class="text-body-secondary">2023</span>
</div>
</footer>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N"
crossorigin="anonymous"></script>

{# Notie #}
<script src="https://unpkg.com/notie"></script>

<script>
{# Notie notifications #}
{# flash messaging #}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
notie.alert({
type: '{{ category }}', // optional, default = 4, enum: [1, 2, 3, 4, 5, 'success', 'warning', 'error', 'info', 'neutral']
text: '{{ message }}',
stay: false, // optional, default = false
time: 4, // optional, default = 3, minimum = 1,
position: 'bottom' // optional, default = 'top', enum: ['top', 'bottom']
})
{% endfor %}
{% endif %}
{% endwith %}

</script>

{% block js %} {% endblock %}
</body>
</html>
Loading

0 comments on commit c0e5e5e

Please sign in to comment.