Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
7fe54f35d7 | ||
|
bcf0d04de6 | ||
c1f7cd5d92 | |||
7ac72b91cd | |||
|
030d5e432e | ||
|
80706e2c16 | ||
290772dfa2 | |||
e886e8af7e | |||
5f7e15dbea | |||
|
557e16dfd6 | ||
|
e0046ef98a | ||
|
9f3bbb0d87 | ||
a00c20f3b1 | |||
|
294fb339ed | ||
|
695592b7b6 | ||
|
663ef6d80d |
8
.idea/.gitignore
vendored
@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="MarkdownSettingsMigration">
|
|
||||||
<option name="stateVersion" value="1" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,18 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: { browser: true, es2020: true },
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
],
|
|
||||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['react-refresh'],
|
|
||||||
rules: {
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
|
24
Frontend/CatApp/.gitignore
vendored
@ -1,24 +0,0 @@
|
|||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
@ -1,13 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>CatApp</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
4219
Frontend/CatApp/package-lock.json
generated
@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "catapp",
|
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"build": "tsc && vite build",
|
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
|
||||||
"preview": "vite preview"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/react": "^18.2.43",
|
|
||||||
"@types/react-dom": "^18.2.17",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
|
||||||
"@typescript-eslint/parser": "^6.14.0",
|
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
|
||||||
"autoprefixer": "^10.4.16",
|
|
||||||
"eslint": "^8.55.0",
|
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
|
||||||
"eslint-plugin-react-refresh": "^0.4.5",
|
|
||||||
"postcss": "^8.4.33",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"typescript": "^5.2.2",
|
|
||||||
"vite": "^5.0.8"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
import Detector from "./components/Detector";
|
|
||||||
import Nav from "./components/Nav";
|
|
||||||
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Nav/>
|
|
||||||
|
|
||||||
<Detector/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
import React, { ChangeEvent, useState } from 'react';
|
|
||||||
|
|
||||||
const Detector = () => {
|
|
||||||
const [file, setFile] = useState<File | undefined>();
|
|
||||||
const [preview, setPreview] = useState<string>('https://mly5gz70zztl.i.optimole.com/cb:w3s1.35324/w:auto/h:auto/q:mauto/f:best/https://www.lifewithcatman.com/wp-content/uploads/2019/03/maine-coon-cat-photography-robert-sijka-64-57ad8f2c0277c__880.jpg');
|
|
||||||
const [results, setResults] = useState<any[]>([]);
|
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (e.target.files) {
|
|
||||||
const selectedFile = e.target.files[0];
|
|
||||||
setFile(selectedFile);
|
|
||||||
|
|
||||||
// Generate preview for the selected file
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onloadend = () => {
|
|
||||||
setPreview(reader.result as string);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(selectedFile);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
try {
|
|
||||||
if (file) {
|
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('image', file);
|
|
||||||
|
|
||||||
const response = await fetch('http://127.0.0.1:5000/api/v1/detect-cat', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const resultFromServer = await response.json();
|
|
||||||
|
|
||||||
// Update results list with the new result
|
|
||||||
setResults((prevResults) => [...prevResults, { file, ...resultFromServer }]);
|
|
||||||
} else {
|
|
||||||
console.error('Błąd serwera:', response.statusText);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Brak pliku do wysłania.');
|
|
||||||
alert('Choose a file before sending')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Błąd:', error);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-center flex-wrap">
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="w-5/6 sm:w-[420px] my-6 m-auto border-[1px] border-black p-4 rounded-2xl flex flex-wrap justify-center items-center">
|
|
||||||
{preview && (
|
|
||||||
<div className="flex justify-center ">
|
|
||||||
<img className='rounded-full border-orange-500 border-2' src={preview} alt="Podgląd" style={{ maxWidth: '100%', maxHeight: '200px' }} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<input className='' type="file" onChange={handleFileChange} />
|
|
||||||
<button
|
|
||||||
className='px-4 p-1 lg:mx-2 border-[1px] text-orange-500 border-orange-500 rounded-lg shadow-lg hover:scale-125 hover:text-white hover:bg-orange-500 duration-500 font-bold mt-4 sm:mt-0'
|
|
||||||
onClick={handleSubmit}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
IS IT?
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{loading && <div className='w-full text-center text-blue-200 uppercase font-semibold'> Sending file...</div>}
|
|
||||||
{results.length > 0 && (
|
|
||||||
<div>
|
|
||||||
{results.map((result, index) => (
|
|
||||||
<div className='border-[1px] border-gray-500 m-4 rounded-lg p-4' key={index}>
|
|
||||||
<h3 className='text-center font-DM text-orange-500 font-semibold'><span>{index + 1}. </span>CAT RESULT</h3>
|
|
||||||
<table>
|
|
||||||
<thead className='font-DM'>
|
|
||||||
<tr>
|
|
||||||
<th>Image:</th>
|
|
||||||
<th>Results:</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{result.results && Object.keys(result.results).map((filename) => (
|
|
||||||
<tr key={filename}>
|
|
||||||
<td>
|
|
||||||
<img className='rounded-xl m-2 border-orange-500 border-[1px]' src={URL.createObjectURL(result.file)} alt="Podgląd" style={{ maxWidth: '100%', maxHeight: '100px' }} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<p>
|
|
||||||
<span>Result:</span><span className='text-orange-500 font-DM'> {result.results[filename].isCat ? 'This is a cat' : 'Not a cat'}</span>
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
{result.results[filename].predictions &&
|
|
||||||
Object.keys(result.results[filename].predictions).map((key) => (
|
|
||||||
<li key={key}>
|
|
||||||
{result.results[filename].predictions[key].score.toFixed(2) * 100}<span>% - </span>
|
|
||||||
{result.results[filename].predictions[key].label}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Detector;
|
|
@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
const Nav = () => {
|
|
||||||
return (
|
|
||||||
<div className='w-full h-32 px-8 md:px-20 shadow-2xl flex items-center justify-center'>
|
|
||||||
<img className="h-32 w-32 p-4" src="https://github.com/UniSzef/test/blob/main/logo.png?raw=true"></img>
|
|
||||||
<div className="md:text-4xl text-2xl font-DM font-bold">Be sure that your <span className="text-orange-400">cat</span> is real!</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Nav
|
|
@ -1,20 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
const Waves = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<svg className="absolute -z-10" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
|
|
||||||
<path fill="#edb593" fill-opacity="1" d="M0,288L48,250.7C96,213,192,139,288,117.3C384,96,480,128,576,149.3C672,171,768,181,864,165.3C960,149,1056,107,1152,112C1248,117,1344,171,1392,197.3L1440,224L1440,0L1392,0C1344,0,1248,0,1152,0C1056,0,960,0,864,0C768,0,672,0,576,0C480,0,384,0,288,0C192,0,96,0,48,0L0,0Z"></path>
|
|
||||||
</svg>
|
|
||||||
<svg className="absolute -z-10 translate-y-[500px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
|
|
||||||
<path fill="#edb593" fill-opacity="1" d="M0,192L40,202.7C80,213,160,235,240,229.3C320,224,400,192,480,160C560,128,640,96,720,101.3C800,107,880,149,960,186.7C1040,224,1120,256,1200,261.3C1280,267,1360,245,1400,234.7L1440,224L1440,320L1400,320C1360,320,1280,320,1200,320C1120,320,1040,320,960,320C880,320,800,320,720,320C640,320,560,320,480,320C400,320,320,320,240,320C160,320,80,320,40,320L0,320Z"></path>
|
|
||||||
</svg>
|
|
||||||
<div className="h-[400px] w-full -z-10 absolute bg-waves translate-y-[800px]"></div>
|
|
||||||
<svg className="absolute -z-10 translate-y-[1200px]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
|
|
||||||
<path fill="#edb593" fill-opacity="1" d="M0,288L26.7,261.3C53.3,235,107,181,160,181.3C213.3,181,267,235,320,240C373.3,245,427,203,480,170.7C533.3,139,587,117,640,117.3C693.3,117,747,139,800,154.7C853.3,171,907,181,960,202.7C1013.3,224,1067,256,1120,256C1173.3,256,1227,224,1280,224C1333.3,224,1387,256,1413,272L1440,288L1440,0L1413.3,0C1386.7,0,1333,0,1280,0C1226.7,0,1173,0,1120,0C1066.7,0,1013,0,960,0C906.7,0,853,0,800,0C746.7,0,693,0,640,0C586.7,0,533,0,480,0C426.7,0,373,0,320,0C266.7,0,213,0,160,0C106.7,0,53,0,27,0L0,0Z"></path>
|
|
||||||
</svg>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Waves
|
|
Before Width: | Height: | Size: 66 KiB |
@ -1,5 +0,0 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display&display=swap');
|
|
||||||
|
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
@ -1,10 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom/client'
|
|
||||||
import App from './App.tsx'
|
|
||||||
import './index.css'
|
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>,
|
|
||||||
)
|
|
1
Frontend/CatApp/src/vite-env.d.ts
vendored
@ -1 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
@ -1,18 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
fontFamily: {
|
|
||||||
DM: ['DM Serif Display']
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
waves: '#edb593',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
||||||
"module": "ESNext",
|
|
||||||
"skipLibCheck": true,
|
|
||||||
|
|
||||||
/* Bundler mode */
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
|
|
||||||
/* Linting */
|
|
||||||
"strict": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"noFallthroughCasesInSwitch": true
|
|
||||||
},
|
|
||||||
"include": ["src"],
|
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"composite": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"module": "ESNext",
|
|
||||||
"moduleResolution": "bundler",
|
|
||||||
"allowSyntheticDefaultImports": true
|
|
||||||
},
|
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { defineConfig } from 'vite'
|
|
||||||
import react from '@vitejs/plugin-react'
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
})
|
|
11
README.md
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
Cat detection
|
||||||
|
|
||||||
|
Quick guide:
|
||||||
|
1. install docker
|
||||||
|
2. build image with `./scripts/build_image.bat`
|
||||||
|
3. run container with `./scripts/start.bat`
|
||||||
|
|
||||||
|
Quick tests guide:
|
||||||
|
1. run unit tests `./scripts/run_tests.bat`
|
||||||
|
|
||||||
|
Have fun!
|
@ -1,48 +1,58 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import torch
|
import numpy as np
|
||||||
import torch.nn.functional as F
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from torchvision.models.resnet import resnet50, ResNet50_Weights
|
from keras.src.applications.resnet import preprocess_input, decode_predictions
|
||||||
from torchvision.transforms import transforms
|
from keras.applications.resnet import ResNet50
|
||||||
|
|
||||||
model = resnet50(weights=ResNet50_Weights.DEFAULT)
|
|
||||||
|
|
||||||
model.eval()
|
|
||||||
|
|
||||||
# Define the image transformations
|
|
||||||
preprocess = transforms.Compose([
|
|
||||||
transforms.Resize(256),
|
|
||||||
transforms.CenterCrop(224),
|
|
||||||
transforms.ToTensor(),
|
|
||||||
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
|
|
||||||
])
|
|
||||||
|
|
||||||
|
|
||||||
def is_cat(image):
|
"""
|
||||||
|
Recognition file.
|
||||||
|
Model is ResNet50. Pretrained model to image recognition.
|
||||||
|
If model recognize cat then returns response with first ten CAT predictions.
|
||||||
|
If first prediction is not a cat then returns False.
|
||||||
|
If prediction is not a cat (is not within list_of_labels) then skips this prediction.
|
||||||
|
Format of response:
|
||||||
|
{
|
||||||
|
'label': {label}
|
||||||
|
'score': {score}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
model = ResNet50(weights='imagenet')
|
||||||
|
|
||||||
|
|
||||||
|
# PRIVATE Preprocess image method
|
||||||
|
def _preprocess_image(image):
|
||||||
try:
|
try:
|
||||||
img = Image.open(BytesIO(image.read()))
|
img = Image.open(BytesIO(image.read()))
|
||||||
|
img = img.resize((224, 224))
|
||||||
# Preprocess the image
|
img_array = np.array(img)
|
||||||
img_t = preprocess(img)
|
img_array = np.expand_dims(img_array, axis=0)
|
||||||
batch_t = torch.unsqueeze(img_t, 0)
|
img_array = preprocess_input(img_array)
|
||||||
|
return img_array
|
||||||
# Make the prediction
|
|
||||||
out = model(batch_t)
|
|
||||||
|
|
||||||
# Apply softmax to get probabilities
|
|
||||||
probabilities = F.softmax(out, dim=1)
|
|
||||||
|
|
||||||
# Get the maximum predicted class and its probability
|
|
||||||
max_prob, max_class = torch.max(probabilities, dim=1)
|
|
||||||
max_prob = max_prob.item()
|
|
||||||
max_class = max_class.item()
|
|
||||||
|
|
||||||
# Check if the maximum predicted class is within the range 281-285
|
|
||||||
if 281 <= max_class <= 285:
|
|
||||||
return max_class, max_prob
|
|
||||||
else:
|
|
||||||
return max_class, None
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Error while processing the image:", e)
|
print(f"Error preprocessing image: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# Generate response
|
||||||
|
def _generate_response(decoded_predictions, list_of_labels):
|
||||||
|
results = {}
|
||||||
|
for i, (imagenet_id, label, score) in enumerate(decoded_predictions):
|
||||||
|
if i == 0 and label not in list_of_labels:
|
||||||
|
return None
|
||||||
|
if score < 0.01:
|
||||||
|
break
|
||||||
|
if label in list_of_labels:
|
||||||
|
results[len(results) + 1] = {"label": label, "score": round(float(score), 2)}
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# Cat detection
|
||||||
|
def detect_cat(image_file, list_of_labels):
|
||||||
|
img_array = _preprocess_image(image_file)
|
||||||
|
prediction = model.predict(img_array)
|
||||||
|
decoded_predictions = decode_predictions(prediction, top=10)[0]
|
||||||
|
return _generate_response(decoded_predictions, list_of_labels)
|
||||||
|
6
docker/.dockerignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
*.md
|
||||||
|
/venv
|
||||||
|
.git
|
||||||
|
__pycache__
|
||||||
|
.pytest_cache
|
||||||
|
/tests
|
11
docker/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.11
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["python", "main.py"]
|
10
docker/docker-compose.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
version: '3.3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
cat-detection:
|
||||||
|
image: cat-detection
|
||||||
|
build:
|
||||||
|
context: ../
|
||||||
|
dockerfile: ./docker/Dockerfile
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
9
docs.md
@ -1,9 +0,0 @@
|
|||||||
# Api
|
|
||||||
|
|
||||||
Port -> 5000
|
|
||||||
|
|
||||||
endpoint -> /detect-cat
|
|
||||||
|
|
||||||
Key -> 'Image'
|
|
||||||
|
|
||||||
Value -> {UPLOADED_FILE}
|
|
53
docs/docs.md
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Api
|
||||||
|
|
||||||
|
Port -> 5000
|
||||||
|
|
||||||
|
endpoint -> api/v1/detect-cat
|
||||||
|
|
||||||
|
Key -> 'Image'
|
||||||
|
|
||||||
|
Value -> {UPLOADED_FILE}
|
||||||
|
|
||||||
|
Flask Rest API application to cat recognition.
|
||||||
|
If request is valid then send response with results of recognition.
|
||||||
|
If key named 'Image' in body does not occur then returns 400 (BAD REQUEST).
|
||||||
|
Otherwise, returns 200 with results of recognition.
|
||||||
|
Format of response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lang": "{users_lang}",
|
||||||
|
"results": {
|
||||||
|
"{filename}": {
|
||||||
|
"isCat": "{is_cat}",
|
||||||
|
"results": {
|
||||||
|
"1": "{result}",
|
||||||
|
"2": "{result}",
|
||||||
|
"3": "{result}",
|
||||||
|
"4": "{result}",
|
||||||
|
"5": "{result}",
|
||||||
|
"6": "{result}",
|
||||||
|
"7": "{result}",
|
||||||
|
"8": "{result}",
|
||||||
|
"9": "{result}",
|
||||||
|
"10": "{result}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
"{error_message}",
|
||||||
|
"{error_message}"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Format of result:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"label": "{label}",
|
||||||
|
"score": "{score}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
```json
|
||||||
|
|
||||||
|
```
|
30
language_label_mapper.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from jproperties import Properties
|
||||||
|
|
||||||
|
"""
|
||||||
|
Translator method.
|
||||||
|
If everything fine then returns translated labels.
|
||||||
|
Else throws an Exception and returns untranslated labels.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def translate(to_translate, lang):
|
||||||
|
try:
|
||||||
|
config = Properties()
|
||||||
|
script_directory = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
resources_path = os.path.join(script_directory, "./resources")
|
||||||
|
|
||||||
|
# Load properties file for given lang
|
||||||
|
with open(os.path.join(resources_path, f"./{lang}.properties"), 'rb') as config_file:
|
||||||
|
config.load(config_file, encoding='UTF-8')
|
||||||
|
|
||||||
|
# Translate labels for given to_translate dictionary
|
||||||
|
for index, label_info in to_translate.items():
|
||||||
|
label = label_info.get("label")
|
||||||
|
to_translate[index]["label"] = config.get(label).data
|
||||||
|
return to_translate, []
|
||||||
|
except Exception as e:
|
||||||
|
error_message = f"Error translating labels: {e}"
|
||||||
|
print(error_message)
|
||||||
|
return to_translate, error_message
|
116
main.py
@ -1,38 +1,108 @@
|
|||||||
from flask import Flask, request, jsonify, session
|
from flask import Flask, request, Response, json
|
||||||
|
from cat_detection import detect_cat
|
||||||
|
from language_label_mapper import translate
|
||||||
|
from validator import validate
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
"""
|
||||||
|
Flask Rest API application to cat recognition.
|
||||||
|
If request is valid then send response with results of recognition.
|
||||||
|
If key named 'Image' in body does not occurred then returns 400 (BAD REQUEST).
|
||||||
|
Otherwise returns 200 with results of recognition.
|
||||||
|
Format of response:
|
||||||
|
{
|
||||||
|
"lang": {users_lang},
|
||||||
|
"results": {
|
||||||
|
{filename}: {
|
||||||
|
"isCat": {is_cat},
|
||||||
|
"results": {
|
||||||
|
"1": {result}
|
||||||
|
"2": {result}
|
||||||
|
"3": {result}
|
||||||
|
...
|
||||||
|
"10" {result}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
},
|
||||||
|
errors[
|
||||||
|
{error_message},
|
||||||
|
{error_message},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
To see result format -> cat_detection.py
|
||||||
|
"""
|
||||||
|
|
||||||
from cat_detection import is_cat
|
|
||||||
|
|
||||||
# Define flask app
|
# Define flask app
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = 'secret_key'
|
app.secret_key = 'secret_key'
|
||||||
|
CORS(app)
|
||||||
|
|
||||||
|
# Available cats
|
||||||
|
list_of_labels = [
|
||||||
|
'lynx',
|
||||||
|
'lion',
|
||||||
|
'tiger',
|
||||||
|
'cheetah',
|
||||||
|
'leopard',
|
||||||
|
'jaguar',
|
||||||
|
'tabby',
|
||||||
|
'Egyptian_cat',
|
||||||
|
'cougar',
|
||||||
|
'Persian_cat',
|
||||||
|
'Siamese_cat',
|
||||||
|
'snow_leopard',
|
||||||
|
'tiger_cat'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Available languages
|
||||||
|
languages = {'pl', 'en'}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/detect-cat', methods=['POST'])
|
@app.route('/api/v1/detect-cat', methods=['POST'])
|
||||||
def upload_file():
|
def upload_file():
|
||||||
# 'Key' in body should be named as 'image'. Type should be 'File' and in 'Value' we should upload image from disc.
|
# Validate request
|
||||||
file = request.files['image']
|
error_messages = validate(request)
|
||||||
if file.filename == '':
|
|
||||||
return jsonify({'error': "File name is empty. Please name a file."}), 400
|
|
||||||
max_class, max_prob = is_cat(file)
|
|
||||||
|
|
||||||
# Save result in session
|
# If any errors occurred, return 400 (BAD REQUEST)
|
||||||
session['result'] = max_class, max_prob
|
if len(error_messages) > 0:
|
||||||
|
errors = json.dumps(
|
||||||
|
{
|
||||||
|
'errors': error_messages
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return Response(errors, status=400, mimetype='application/json')
|
||||||
|
|
||||||
# Tworzenie komunikatu na podstawie wyniku analizy zdjęcia
|
# Get files from request
|
||||||
translator = {
|
files = request.files.getlist('image')
|
||||||
281: "tabby cat",
|
|
||||||
282: "tiger cat",
|
# Get user's language (Value in header 'Accept-Language'). Default value is English
|
||||||
283: "persian cat",
|
lang = request.accept_languages.best_match(languages, default='en')
|
||||||
284: "siamese cat",
|
|
||||||
285: "egyptian cat"
|
# Define JSON structure for results
|
||||||
|
results = {
|
||||||
|
'lang': lang,
|
||||||
|
'results': {},
|
||||||
|
'errors': []
|
||||||
}
|
}
|
||||||
if max_prob is not None:
|
|
||||||
result = f"The image is recognized as '{translator[max_class]}' with a probability of {round(max_prob * 100, 2)}%"
|
|
||||||
else:
|
|
||||||
result = f"The image is not recognized as a class within the range 281-285 ({max_class})"
|
|
||||||
|
|
||||||
return jsonify({'result': result}), 200
|
# Generate results
|
||||||
|
for file in files:
|
||||||
|
predictions = detect_cat(file, list_of_labels)
|
||||||
|
if predictions is not None:
|
||||||
|
predictions, error_messages = translate(predictions, lang)
|
||||||
|
results['results'][file.filename] = {
|
||||||
|
'isCat': False if not predictions else True,
|
||||||
|
**({'predictions': predictions} if predictions is not None else {})
|
||||||
|
}
|
||||||
|
if len(error_messages) > 1:
|
||||||
|
results['errors'].append(error_messages)
|
||||||
|
|
||||||
|
# Send response with 200 (Success)
|
||||||
|
return Response(json.dumps(results), status=200, mimetype='application/json')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True)
|
app.run(host='0.0.0.0')
|
||||||
|
9
requirements.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
flask==3.0.0
|
||||||
|
numpy==1.26.3
|
||||||
|
pillow==10.2.0
|
||||||
|
keras==2.15.0
|
||||||
|
jproperties==2.1.1
|
||||||
|
tensorflow==2.15.0
|
||||||
|
werkzeug==3.0.1
|
||||||
|
pytest==7.4.4
|
||||||
|
flask-cors==4.0.0
|
14
resources/en.properties
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# EN
|
||||||
|
lynx=lynx
|
||||||
|
lion=lion
|
||||||
|
tiger=tiger
|
||||||
|
cheetah=cheetah
|
||||||
|
leopard=leopard
|
||||||
|
jaguar=jaguar
|
||||||
|
tabby=tabby
|
||||||
|
Egyptian_cat=Egyptian cat
|
||||||
|
cougar=cougar
|
||||||
|
Persian_cat=Persian cat
|
||||||
|
Siamese_cat=Siamese cat
|
||||||
|
snow_leopard=snow leopard
|
||||||
|
tiger_cat=tiger cat
|
14
resources/pl.properties
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# PL
|
||||||
|
lynx=ryś
|
||||||
|
lion=lew
|
||||||
|
tiger=tygrys
|
||||||
|
cheetah=gepard
|
||||||
|
leopard=lampart
|
||||||
|
jaguar=jaguar
|
||||||
|
tabby=kot pręgowany
|
||||||
|
Egyptian_cat=kot egipski
|
||||||
|
cougar=puma
|
||||||
|
Persian_cat=kot perski
|
||||||
|
Siamese_cat=kot syjamski
|
||||||
|
snow_leopard=lampart śnieżny
|
||||||
|
tiger_cat=kot tygrysi
|
24
scripts/build_image.bat
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
|
||||||
|
docker -v >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [31mDocker is not installed.[0m
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
docker info >nul 2>&1
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [31mDocker engine is not running.[0m
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
cd %~dp0
|
||||||
|
echo [32mBuilding docker image...[0m
|
||||||
|
docker-compose -f ../docker/docker-compose.yml build
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [31mBuilding docker image failed.[0m
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo [32mThe image was built successfully.[0m
|
27
scripts/build_image.sh
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export LC_ALL=C.UTF-8
|
||||||
|
export LANG=C.UTF-8
|
||||||
|
|
||||||
|
docker -v > /dev/null 2>&1
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "\033[31mDocker is not installed.\033[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker info > /dev/null 2>&1
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "\033[31mDocker engine is not running.\033[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd ../docker
|
||||||
|
echo -e "\033[32mBuilding docker image...\033[0m"
|
||||||
|
docker-compose build cat-detection
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "\033[31mBuilding docker image failed.\033[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "\033[32mThe image was built successfully.\033[0m"
|
12
scripts/run_tests.bat
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@echo off
|
||||||
|
echo [32mRunning unit tests.[0m
|
||||||
|
|
||||||
|
cd %~dp0
|
||||||
|
pytest ../tests
|
||||||
|
|
||||||
|
if %ERRORLEVEL% equ 0 (
|
||||||
|
echo [32mTests passed successfully.[0m
|
||||||
|
) else (
|
||||||
|
Tests failed.
|
||||||
|
echo [31mTests failed.[0m
|
||||||
|
)
|
9
scripts/start.bat
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
|
||||||
|
cd %~dp0
|
||||||
|
docker compose -f ../docker/docker-compose.yml up cat-detection -d
|
||||||
|
if %errorlevel% neq 0 (
|
||||||
|
echo [31mStarting docker container failed.[0m
|
||||||
|
exit /b 1
|
||||||
|
)
|
11
scripts/start.sh
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export LC_ALL=C.UTF-8
|
||||||
|
export LANG=C.UTF-8
|
||||||
|
|
||||||
|
docker-compose -f ../docker/docker-compose.yml up cat-detection -d
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "\033[31mStarting docker container failed.\033[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
9
test.py
@ -1,9 +0,0 @@
|
|||||||
import requests
|
|
||||||
|
|
||||||
url = 'http://127.0.0.1:5000/detect-cat'
|
|
||||||
|
|
||||||
files = {'image': (open('cat1.jpg', 'rb'))}
|
|
||||||
|
|
||||||
response = requests.post(url, files=files)
|
|
||||||
|
|
||||||
print(response.text)
|
|
0
tests/__init__.py
Normal file
BIN
tests/img/Egyptian_cat/Egyptian-Mau-Bronze-750.jpg
Normal file
After Width: | Height: | Size: 360 KiB |
BIN
tests/img/Persian_cat/PER_Fluffy_Fancy_Antony_(5468896948).jpg
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
tests/img/Siamese_cat/siamese-cat-cover.jpg
Normal file
After Width: | Height: | Size: 159 KiB |
BIN
tests/img/cheetah/TheCheethcat.jpg
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
tests/img/cougar/Cougar_at_Cougar_Mountain_Zoological_Park_2.jpg
Normal file
After Width: | Height: | Size: 216 KiB |
BIN
tests/img/jaguar/Junior-Jaguar-Belize-Zoo.jpg
Normal file
After Width: | Height: | Size: 326 KiB |
BIN
tests/img/leopard/African_leopard_male_(cropped).jpg
Normal file
After Width: | Height: | Size: 221 KiB |
BIN
tests/img/lion/Lion_waiting_in_Namibia.jpg
Normal file
After Width: | Height: | Size: 281 KiB |
BIN
tests/img/lynx/552661_poster.jpg
Normal file
After Width: | Height: | Size: 469 KiB |
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
BIN
tests/img/snow_lopard/Snow-Leopard.jpg
Normal file
After Width: | Height: | Size: 255 KiB |
BIN
tests/img/tabby/Cat_November_2010-1a.jpg
Normal file
After Width: | Height: | Size: 2.7 MiB |
After Width: | Height: | Size: 345 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
19
tests/test_1.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from werkzeug.datastructures import FileStorage
|
||||||
|
|
||||||
|
from main import app
|
||||||
|
|
||||||
|
|
||||||
|
def test_upload_file():
|
||||||
|
with app.test_client() as test_client:
|
||||||
|
script_directory = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
image_path = os.path.join(script_directory, "./img/tiger_cat/cat1.jpg")
|
||||||
|
image = FileStorage(
|
||||||
|
stream=open(image_path, "rb"),
|
||||||
|
filename="cat1.jpg",
|
||||||
|
content_type="image/jpeg",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = test_client.post('/api/v1/detect-cat', data={'image': image}, content_type='multipart/form-data')
|
||||||
|
assert response.status_code == 200
|
31
validator.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
"""
|
||||||
|
Validation method.
|
||||||
|
If everything fine then returns empty list.
|
||||||
|
Else returns list of error messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Allowed extensions
|
||||||
|
allowed_extensions = {'jpg', 'jpeg', 'png'}
|
||||||
|
|
||||||
|
|
||||||
|
def validate(request):
|
||||||
|
errors = []
|
||||||
|
try:
|
||||||
|
images = request.files.getlist('image')
|
||||||
|
|
||||||
|
# Case 1 - > request has no 'Image' Key in body
|
||||||
|
if images is None:
|
||||||
|
raise KeyError("'Image' key not found in request.")
|
||||||
|
|
||||||
|
# Case 2 - > if some of the images has no filename
|
||||||
|
if not images or all(img.filename == '' for img in images):
|
||||||
|
raise ValueError("Value of 'Image' key is empty.")
|
||||||
|
|
||||||
|
# Case 3 -> if some of the images has wrong extension
|
||||||
|
for img in images:
|
||||||
|
if not img.filename.lower().endswith(('.png', '.jpg', '.jpeg')):
|
||||||
|
raise ValueError(f"Given file '{img.filename}' has no allowed extension. "
|
||||||
|
f"Allowed extensions: {allowed_extensions}.")
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(e.args[0])
|
||||||
|
return errors
|