Langkah 3
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import HTMLResponse
from pathlib import Path
from datetime import datetime
import uuid
import cv2
import numpy as np
import base64
# ============================================================
# SMART INCUBATOR COMPUTER VISION SERVER
# CV-03 - OpenCV PREPROCESSING + DASHBOARD
#
# Compatible with OpenCV 5.0.0
# ============================================================
APP_NAME = "Smart Incubator Computer Vision Server"
VERSION = "CV-03"
BASE_DIR = Path(__file__).resolve().parent.parent
IMAGE_DIR = BASE_DIR / "storage" / "images"
IMAGE_DIR.mkdir(
parents=True,
exist_ok=True
)
# ============================================================
# FASTAPI
# ============================================================
app = FastAPI(
title=APP_NAME,
version=VERSION
)
# ============================================================
# HELPER
# ============================================================
def image_to_base64(image, extension=".jpg"):
success, encoded = cv2.imencode(
extension,
image
)
if not success:
raise ValueError(
f"Gagal melakukan encode gambar {extension}"
)
return base64.b64encode(
encoded.tobytes()
).decode("utf-8")
# ============================================================
# ROOT
# ============================================================
@app.get("/")
def root():
return {
"application": APP_NAME,
"version": VERSION,
"status": "running",
"module": "CV-03 OpenCV Preprocessing",
"opencv_version": cv2.__version__
}
# ============================================================
# HEALTH
# ============================================================
@app.get("/health")
def health():
return {
"status": "ok",
"opencv_version": cv2.__version__
}
# ============================================================
# UPLOAD IMAGE
# ============================================================
@app.post("/api/v1/vision/upload")
async def upload_image(
file: UploadFile = File(...)
):
allowed_types = [
"image/jpeg",
"image/jpg",
"image/png"
]
if file.content_type not in allowed_types:
raise HTTPException(
status_code=400,
detail="File harus JPG, JPEG atau PNG"
)
data = await file.read()
if not data:
raise HTTPException(
status_code=400,
detail="File kosong"
)
# --------------------------------------------------------
# Decode
# --------------------------------------------------------
image_array = np.frombuffer(
data,
dtype=np.uint8
)
image = cv2.imdecode(
image_array,
cv2.IMREAD_COLOR
)
if image is None:
raise HTTPException(
status_code=400,
detail="OpenCV tidak dapat membaca gambar"
)
# --------------------------------------------------------
# Resolution
# --------------------------------------------------------
height, width = image.shape[:2]
channels = (
image.shape[2]
if len(image.shape) == 3
else 1
)
# --------------------------------------------------------
# Folder tanggal
# --------------------------------------------------------
today = datetime.now().strftime(
"%Y-%m-%d"
)
save_dir = IMAGE_DIR / today
save_dir.mkdir(
parents=True,
exist_ok=True
)
# --------------------------------------------------------
# Filename
# --------------------------------------------------------
timestamp = datetime.now().strftime(
"%Y%m%d_%H%M%S"
)
unique_id = uuid.uuid4().hex[:6]
filename = (
f"{timestamp}_{unique_id}.jpg"
)
filepath = save_dir / filename
# --------------------------------------------------------
# Save
# --------------------------------------------------------
success = cv2.imwrite(
str(filepath),
image,
[
cv2.IMWRITE_JPEG_QUALITY,
95
]
)
if not success:
raise HTTPException(
status_code=500,
detail="Gagal menyimpan gambar"
)
return {
"status": "success",
"message": "Foto berhasil diterima",
"filename": filename,
"path": str(filepath),
"width": width,
"height": height,
"channels": channels,
"size_bytes": len(data),
"timestamp": datetime.now().isoformat()
}
# ============================================================
# FIND LATEST IMAGE
# ============================================================
def get_latest_image():
files = list(
IMAGE_DIR.rglob("*.jpg")
)
if not files:
return None
files.sort(
key=lambda x: x.stat().st_mtime,
reverse=True
)
return files[0]
# ============================================================
# BASIC IMAGE INFORMATION
# ============================================================
def get_basic_info(image):
height, width = image.shape[:2]
channels = (
image.shape[2]
if len(image.shape) == 3
else 1
)
gray = cv2.cvtColor(
image,
cv2.COLOR_BGR2GRAY
)
brightness = float(
np.mean(gray)
)
contrast = float(
np.std(gray)
)
return {
"width": width,
"height": height,
"channels": channels,
"brightness": round(
brightness,
2
),
"contrast": round(
contrast,
2
)
}
# ============================================================
# PREPROCESSING
# ============================================================
def preprocess_image(image):
# --------------------------------------------------------
# 1. GRAYSCALE
# --------------------------------------------------------
gray = cv2.cvtColor(
image,
cv2.COLOR_BGR2GRAY
)
# --------------------------------------------------------
# 2. GAUSSIAN BLUR
#
# Mengurangi noise kamera.
# --------------------------------------------------------
blurred = cv2.GaussianBlur(
gray,
(5, 5),
0
)
# --------------------------------------------------------
# 3. CLAHE
#
# Meningkatkan kontras lokal.
# --------------------------------------------------------
clahe = cv2.createCLAHE(
clipLimit=2.0,
tileGridSize=(8, 8)
)
enhanced = clahe.apply(
blurred
)
# --------------------------------------------------------
# 4. OTSU THRESHOLD
#
# Membuat gambar hitam-putih
# secara otomatis.
# --------------------------------------------------------
threshold_value, threshold = cv2.threshold(
enhanced,
0,
255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU
)
# --------------------------------------------------------
# 5. CANNY EDGE
#
# Mendeteksi garis / tepi objek.
# --------------------------------------------------------
edges = cv2.Canny(
enhanced,
50,
150
)
# --------------------------------------------------------
# 6. MORPHOLOGICAL CLEANUP
#
# Menutup lubang kecil dan noise.
# --------------------------------------------------------
kernel = cv2.getStructuringElement(
cv2.MORPH_ELLIPSE,
(5, 5)
)
morph = cv2.morphologyEx(
threshold,
cv2.MORPH_CLOSE,
kernel,
iterations=2
)
# --------------------------------------------------------
# 7. CONTOUR
# --------------------------------------------------------
contours, hierarchy = cv2.findContours(
morph,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE
)
# --------------------------------------------------------
# Buat gambar contour
# --------------------------------------------------------
contour_image = image.copy()
contour_count = 0
image_area = (
image.shape[0]
* image.shape[1]
)
min_area = image_area * 0.002
max_area = image_area * 0.95
valid_contours = []
for contour in contours:
area = cv2.contourArea(
contour
)
if (
area >= min_area
and area <= max_area
):
valid_contours.append(
contour
)
contour_count = len(
valid_contours
)
# --------------------------------------------------------
# Draw contour
# --------------------------------------------------------
cv2.drawContours(
contour_image,
valid_contours,
-1,
(0, 255, 0),
2
)
# --------------------------------------------------------
# Label contour
# --------------------------------------------------------
for index, contour in enumerate(
valid_contours,
start=1
):
x, y, w, h = cv2.boundingRect(
contour
)
cv2.putText(
contour_image,
str(index),
(x, y - 5),
cv2.FONT_HERSHEY_SIMPLEX,
0.6,
(0, 0, 255),
2
)
return {
"gray": gray,
"blurred": blurred,
"enhanced": enhanced,
"threshold": threshold,
"edges": edges,
"morph": morph,
"contour": contour_image,
"threshold_value": round(
float(threshold_value),
2
),
"contour_count": contour_count
}
# ============================================================
# ANALYZE + PREPROCESS LATEST IMAGE
# ============================================================
def analyze_latest_image():
filepath = get_latest_image()
if filepath is None:
raise FileNotFoundError(
"Belum ada foto di storage/images"
)
# --------------------------------------------------------
# Read
# --------------------------------------------------------
image = cv2.imread(
str(filepath),
cv2.IMREAD_COLOR
)
if image is None:
raise ValueError(
"OpenCV gagal membaca foto"
)
# --------------------------------------------------------
# Basic information
# --------------------------------------------------------
info = get_basic_info(
image
)
# --------------------------------------------------------
# Preprocessing
# --------------------------------------------------------
processed = preprocess_image(
image
)
return {
"filename": filepath.name,
"path": str(filepath),
"info": info,
"processed": processed
}
# ============================================================
# JSON ANALYSIS
# ============================================================
@app.get(
"/api/v1/vision/preprocess/latest"
)
def preprocess_latest():
try:
result = analyze_latest_image()
except FileNotFoundError as e:
raise HTTPException(
status_code=404,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=str(e)
)
processed = result[
"processed"
]
return {
"status": "success",
"filename": result[
"filename"
],
"resolution": result[
"info"
],
"preprocessing": {
"gaussian_blur": "5x5",
"clahe_clip_limit": 2.0,
"clahe_grid": "8x8",
"otsu_threshold": processed[
"threshold_value"
],
"canny_low": 50,
"canny_high": 150,
"morphology_kernel": "5x5",
"contour_count": processed[
"contour_count"
]
}
}
# ============================================================
# DASHBOARD
# ============================================================
@app.get(
"/vision",
response_class=HTMLResponse
)
def vision_dashboard():
try:
result = analyze_latest_image()
except FileNotFoundError:
return HTMLResponse(
content="""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
Smart Incubator CV-03
</title>
</head>
<body>
<h1>
Smart Incubator
</h1>
<h2>
CV-03 OpenCV Preprocessing
</h2>
<p>
Belum ada foto di:
</p>
<pre>
storage/images
</pre>
</body>
</html>
"""
)
except Exception as e:
return HTMLResponse(
content=f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
CV-03 Error
</title>
</head>
<body>
<h1>
OpenCV Error
</h1>
<pre>
{e}
</pre>
</body>
</html>
""",
status_code=500
)
# --------------------------------------------------------
# DATA
# --------------------------------------------------------
filepath = get_latest_image()
image = cv2.imread(
str(filepath),
cv2.IMREAD_COLOR
)
processed = result[
"processed"
]
info = result[
"info"
]
# --------------------------------------------------------
# Convert images to Base64
# --------------------------------------------------------
original_b64 = image_to_base64(
image,
".jpg"
)
gray_b64 = image_to_base64(
processed["gray"],
".jpg"
)
blurred_b64 = image_to_base64(
processed["blurred"],
".jpg"
)
enhanced_b64 = image_to_base64(
processed["enhanced"],
".jpg"
)
threshold_b64 = image_to_base64(
processed["threshold"],
".jpg"
)
edges_b64 = image_to_base64(
processed["edges"],
".jpg"
)
morph_b64 = image_to_base64(
processed["morph"],
".jpg"
)
contour_b64 = image_to_base64(
processed["contour"],
".jpg"
)
# --------------------------------------------------------
# HTML
# --------------------------------------------------------
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta
http-equiv="refresh"
content="5"
>
<title>
Smart Incubator CV-03
</title>
<style>
body {{
font-family:
Arial,
sans-serif;
background:
#f2f2f2;
margin:
20px;
}}
.container {{
max-width:
1200px;
margin:
auto;
}}
.header {{
background:
white;
padding:
20px;
border-radius:
10px;
box-shadow:
0 2px 8px
rgba(0,0,0,0.15);
}}
.grid {{
display:
grid;
grid-template-columns:
repeat(2, 1fr);
gap:
20px;
margin-top:
20px;
}}
.card {{
background:
white;
padding:
15px;
border-radius:
10px;
box-shadow:
0 2px 8px
rgba(0,0,0,0.15);
}}
.card img {{
width:
100%;
max-height:
350px;
object-fit:
contain;
background:
#111;
}}
.card h2 {{
font-size:
18px;
}}
.info {{
background:
white;
margin-top:
20px;
padding:
20px;
border-radius:
10px;
box-shadow:
0 2px 8px
rgba(0,0,0,0.15);
}}
table {{
width:
100%;
border-collapse:
collapse;
}}
td {{
padding:
10px;
border-bottom:
1px solid #ddd;
}}
td:first-child {{
font-weight:
bold;
width:
45%;
}}
.value {{
font-size:
18px;
}}
.badge {{
display:
inline-block;
padding:
6px 12px;
border-radius:
6px;
background:
#eeeeee;
font-weight:
bold;
}}
@media(max-width:800px) {{
.grid {{
grid-template-columns:
1fr;
}}
}}
</style>
</head>
<body>
<div class="container">
<!-- ======================================================
HEADER
======================================================= -->
<div class="header">
<h1>
Smart Incubator
</h1>
<h2>
CV-03 — OpenCV Preprocessing
</h2>
<p>
File:
<b>
{result["filename"]}
</b>
</p>
<p>
OpenCV:
<b>
{cv2.__version__}
</b>
</p>
</div>
<!-- ======================================================
BASIC INFORMATION
======================================================= -->
<div class="info">
<h2>
Informasi Foto
</h2>
<table>
<tr>
<td>
Resolusi
</td>
<td class="value">
{info["width"]}
×
{info["height"]}
pixels
</td>
</tr>
<tr>
<td>
Brightness
</td>
<td class="value">
{info["brightness"]}
/
255
</td>
</tr>
<tr>
<td>
Contrast
</td>
<td class="value">
{info["contrast"]}
</td>
</tr>
<tr>
<td>
Otsu Threshold
</td>
<td class="value">
{processed["threshold_value"]}
</td>
</tr>
<tr>
<td>
Valid Contours
</td>
<td class="value">
<span class="badge">
{processed["contour_count"]}
</span>
</td>
</tr>
</table>
</div>
<!-- ======================================================
IMAGE GRID
======================================================= -->
<div class="grid">
<!-- ORIGINAL -->
<div class="card">
<h2>
1. Original
</h2>
<img
src="data:image/jpeg;base64,{original_b64}"
>
<p>
Foto asli dari kamera.
</p>
</div>
<!-- GRAYSCALE -->
<div class="card">
<h2>
2. Grayscale
</h2>
<img
src="data:image/jpeg;base64,{gray_b64}"
>
<p>
Foto diubah menjadi
grayscale 8-bit.
</p>
</div>
<!-- BLUR -->
<div class="card">
<h2>
3. Gaussian Blur
</h2>
<img
src="data:image/jpeg;base64,{blurred_b64}"
>
<p>
Kernel:
<b>
5 × 5
</b>
</p>
</div>
<!-- CLAHE -->
<div class="card">
<h2>
4. CLAHE Enhancement
</h2>
<img
src="data:image/jpeg;base64,{enhanced_b64}"
>
<p>
Local contrast enhancement.
</p>
</div>
<!-- THRESHOLD -->
<div class="card">
<h2>
5. Otsu Threshold
</h2>
<img
src="data:image/jpeg;base64,{threshold_b64}"
>
<p>
Threshold otomatis:
<b>
{processed["threshold_value"]}
</b>
</p>
</div>
<!-- CANNY -->
<div class="card">
<h2>
6. Canny Edge
</h2>
<img
src="data:image/jpeg;base64,{edges_b64}"
>
<p>
Canny:
<b>
50 — 150
</b>
</p>
</div>
<!-- MORPH -->
<div class="card">
<h2>
7. Morphological Cleanup
</h2>
<img
src="data:image/jpeg;base64,{morph_b64}"
>
<p>
Kernel:
<b>
5 × 5
</b>
</p>
</div>
<!-- CONTOUR -->
<div class="card">
<h2>
8. Contour Detection
</h2>
<img
src="data:image/jpeg;base64,{contour_b64}"
>
<p>
Valid contours:
<b>
{processed["contour_count"]}
</b>
</p>
</div>
</div>
<!-- ======================================================
PIPELINE
======================================================= -->
<div class="info">
<h2>
CV-03 Processing Pipeline
</h2>
<p>
<b>
Original
</b>
→
<b>
Grayscale
</b>
→
<b>
Gaussian Blur
</b>
→
<b>
CLAHE
</b>
→
<b>
Otsu Threshold
</b>
→
<b>
Morphology
</b>
→
<b>
Contour
</b>
</p>
<p>
Canny Edge digunakan sebagai
jalur tambahan untuk melihat
tepi objek.
</p>
</div>
<!-- ======================================================
SYSTEM
======================================================= -->
<div class="info">
<h2>
System Status
</h2>
<p>
Server:
<b>
RUNNING
</b>
</p>
<p>
OpenCV:
<b>
{cv2.__version__}
</b>
</p>
<p>
Mode:
<b>
CV-03 PREPROCESSING
</b>
</p>
<p>
Dashboard refresh:
<b>
5 detik
</b>
</p>
</div>
</div>
</body>
</html>
"""
return HTMLResponse(
content=html
)
# ============================================================
# START SERVER
# ============================================================
if __name__ == "__main__":
import uvicorn
print()
print("=" * 65)
print(
"SMART INCUBATOR COMPUTER VISION SERVER"
)
print(
"CV-03 - OpenCV PREPROCESSING"
)
print("=" * 65)
print()
print(
"OpenCV:",
cv2.__version__
)
print()
print(
"Dashboard:"
)
print(
"http://127.0.0.1:8000/vision"
)
print()
print(
"Preprocessing API:"
)
print(
"http://127.0.0.1:8000/api/v1/vision/preprocess/latest"
)
print()
print("=" * 65)
print()
uvicorn.run(
app,
host="0.0.0.0",
port=8000
)
Post a Comment for "Langkah 3"