CV-02
Bagus. Kita lanjut CV-02 — OpenCV Image Analyzer.
Pada tahap ini alurnya:
Foto tersimpan
│
▼
Smart Incubator Server
│
├── OpenCV membaca foto
├── Resolusi
├── Brightness
├── Contrast
└── Histogram
│
▼
Browser menampilkan hasil
Kita buat agar tidak perlu PowerShell untuk melihat hasil. Cukup buka halaman:
http://127.0.0.1:8000/vision
---
1. Install OpenCV
Buka PowerShell/Anaconda Prompt baru.
conda activate vision
pip install opencv-python
Kemudian cek:
python -c "import cv2; print(cv2.__version__)"
Jika muncul misalnya:
4.12.0
berarti OpenCV sudah siap.
---
2. Struktur project CV-02
Kita ubah sedikit struktur:
C:\smart-incubator\
│
├── server\
│ └── main.py
│
└── storage\
└── images\
└── 2026-09-06\
└── foto.jpg
Untuk tahap ini kita tetap memakai main.py dari CV-01, tetapi kita tambahkan kemampuan OpenCV.
---
3. Ganti main.py
Buka:
C:\smart-incubator\server\main.py
Ganti isinya dengan kode berikut:
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse, HTMLResponse
from pathlib import Path
from datetime import datetime
import uuid
import cv2
import numpy as np
import base64
# ============================================================
# CONFIGURATION
# ============================================================
APP_NAME = "Smart Incubator Computer Vision Server"
VERSION = "CV-02"
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
)
# ============================================================
# ROOT
# ============================================================
@app.get("/")
def root():
return {
"application": APP_NAME,
"version": VERSION,
"status": "running",
"module": "CV-02 OpenCV Image Analyzer"
}
# ============================================================
# 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 berupa JPG, JPEG atau PNG"
)
data = await file.read()
if not data:
raise HTTPException(
status_code=400,
detail="File kosong"
)
# --------------------------------------------------------
# Decode image menggunakan OpenCV
# --------------------------------------------------------
image_array = np.frombuffer(data, np.uint8)
image = cv2.imdecode(
image_array,
cv2.IMREAD_COLOR
)
if image is None:
raise HTTPException(
status_code=400,
detail="File gambar tidak dapat dibaca OpenCV"
)
# --------------------------------------------------------
# Resolusi
# --------------------------------------------------------
height, width = image.shape[:2]
# --------------------------------------------------------
# Folder tanggal
# --------------------------------------------------------
today = datetime.now().strftime("%Y-%m-%d")
save_dir = IMAGE_DIR / today
save_dir.mkdir(
parents=True,
exist_ok=True
)
# --------------------------------------------------------
# Nama file
# --------------------------------------------------------
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
# --------------------------------------------------------
# Simpan sebagai JPEG
# --------------------------------------------------------
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,
"size_bytes": len(data),
"timestamp": datetime.now().isoformat()
}
# ============================================================
# GET 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]
# ============================================================
# OPENCV IMAGE ANALYSIS
# ============================================================
def analyze_image(filepath):
image = cv2.imread(
str(filepath),
cv2.IMREAD_COLOR
)
if image is None:
raise ValueError(
"OpenCV gagal membaca gambar"
)
# --------------------------------------------------------
# RESOLUTION
# --------------------------------------------------------
height, width = image.shape[:2]
channels = image.shape[2]
# --------------------------------------------------------
# GRAYSCALE
# --------------------------------------------------------
gray = cv2.cvtColor(
image,
cv2.COLOR_BGR2GRAY
)
# --------------------------------------------------------
# BRIGHTNESS
#
# Mean pixel intensity:
# 0 = hitam
# 255 = putih
# --------------------------------------------------------
brightness = float(
np.mean(gray)
)
# --------------------------------------------------------
# CONTRAST
#
# Standard deviation pixel intensity.
# Semakin besar -> semakin tinggi variasi kontras.
# --------------------------------------------------------
contrast = float(
np.std(gray)
)
# --------------------------------------------------------
# HISTOGRAM
# --------------------------------------------------------
histogram = cv2.calcHist(
[gray],
[0],
None,
[256],
[0, 256]
)
# Normalisasi histogram
histogram = cv2.normalize(
histogram,
histogram,
0,
220,
cv2.NORM_MINMAX
)
# --------------------------------------------------------
# BUAT GAMBAR HISTOGRAM
# --------------------------------------------------------
hist_width = 768
hist_height = 300
hist_image = np.ones(
(hist_height, hist_width, 3),
dtype=np.uint8
) * 255
# Grid horizontal
for y in range(
0,
hist_height,
50
):
cv2.line(
hist_image,
(0, y),
(hist_width, y),
(220, 220, 220),
1
)
# Histogram
for i in range(1, 256):
x1 = int(
(i - 1)
* hist_width
/ 256
)
x2 = int(
i
* hist_width
/ 256
)
y1 = hist_height - int(
histogram[i - 1][0]
)
y2 = hist_height - int(
histogram[i][0]
)
cv2.line(
hist_image,
(x1, y1),
(x2, y2),
(0, 0, 0),
2
)
# Label histogram
cv2.putText(
hist_image,
"0",
(5, hist_height - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 0),
1
)
cv2.putText(
hist_image,
"128",
(
hist_width // 2 - 15,
hist_height - 10
),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 0),
1
)
cv2.putText(
hist_image,
"255",
(
hist_width - 35,
hist_height - 10
),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 0),
1
)
# --------------------------------------------------------
# ENCODE HISTOGRAM -> BASE64
# --------------------------------------------------------
_, encoded_hist = cv2.imencode(
".png",
hist_image
)
histogram_base64 = base64.b64encode(
encoded_hist
).decode("utf-8")
# --------------------------------------------------------
# ENCODE ORIGINAL IMAGE
# --------------------------------------------------------
_, encoded_image = cv2.imencode(
".jpg",
image
)
image_base64 = base64.b64encode(
encoded_image
).decode("utf-8")
# --------------------------------------------------------
# FILE SIZE
# --------------------------------------------------------
file_size = filepath.stat().st_size
return {
"filename": filepath.name,
"path": str(filepath),
"resolution": {
"width": width,
"height": height,
"channels": channels
},
"brightness": round(
brightness,
2
),
"contrast": round(
contrast,
2
),
"file_size_bytes": file_size,
"histogram": histogram_base64,
"image": image_base64
}
# ============================================================
# ANALYZE LATEST IMAGE - JSON
# ============================================================
@app.get("/api/v1/vision/analyze/latest")
def analyze_latest():
latest = get_latest_image()
if latest is None:
raise HTTPException(
status_code=404,
detail="Belum ada foto"
)
try:
result = analyze_image(
latest
)
return {
"status": "success",
"data": result
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=str(e)
)
# ============================================================
# VISION DASHBOARD
# ============================================================
@app.get(
"/vision",
response_class=HTMLResponse
)
def vision_dashboard():
latest = get_latest_image()
if latest is None:
return """
<html>
<head>
<title>Smart Incubator CV-02</title>
</head>
<body>
<h1>Smart Incubator - CV-02</h1>
<h2>OpenCV Image Analyzer</h2>
<p>
Belum ada foto yang tersimpan.
</p>
</body>
</html>
"""
try:
result = analyze_image(
latest
)
except Exception as e:
return f"""
<html>
<body>
<h1>Error</h1>
<pre>{e}</pre>
</body>
</html>
"""
resolution = result["resolution"]
brightness = result["brightness"]
contrast = result["contrast"]
image_base64 = result["image"]
histogram_base64 = result["histogram"]
# --------------------------------------------------------
# HTML
# --------------------------------------------------------
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta
http-equiv="refresh"
content="5"
>
<title>
Smart Incubator CV-02
</title>
<style>
body {{
font-family: Arial, sans-serif;
background: #f2f2f2;
margin: 30px;
}}
h1 {{
margin-bottom: 5px;
}}
.container {{
max-width: 1000px;
margin: auto;
}}
.card {{
background: white;
padding: 20px;
margin-top: 20px;
border-radius: 10px;
box-shadow:
0 2px 8px
rgba(0,0,0,0.15);
}}
img.main {{
max-width: 100%;
max-height: 500px;
display: block;
margin: auto;
}}
img.hist {{
width: 100%;
max-width: 768px;
display: block;
margin: auto;
}}
table {{
border-collapse: collapse;
width: 100%;
}}
td {{
padding: 10px;
border-bottom:
1px solid #ddd;
}}
td:first-child {{
font-weight: bold;
width: 40%;
}}
.value {{
font-size: 20px;
}}
</style>
</head>
<body>
<div class="container">
<h1>
Smart Incubator
</h1>
<p>
Computer Vision CV-02 —
OpenCV Image Analyzer
</p>
<div class="card">
<h2>
Foto Terakhir
</h2>
<img
class="main"
src="data:image/jpeg;base64,{image_base64}"
>
<p>
<b>
File:
</b>
{result["filename"]}
</p>
</div>
<div class="card">
<h2>
Image Information
</h2>
<table>
<tr>
<td>
Resolution
</td>
<td class="value">
{resolution["width"]}
×
{resolution["height"]}
pixels
</td>
</tr>
<tr>
<td>
Channels
</td>
<td>
{resolution["channels"]}
</td>
</tr>
<tr>
<td>
Brightness
</td>
<td class="value">
{brightness}
/ 255
</td>
</tr>
<tr>
<td>
Contrast
</td>
<td class="value">
{contrast}
</td>
</tr>
<tr>
<td>
File Size
</td>
<td>
{result["file_size_bytes"]:,}
bytes
</td>
</tr>
</table>
</div>
<div class="card">
<h2>
Grayscale Histogram
</h2>
<img
class="hist"
src="data:image/png;base64,{histogram_base64}"
>
<p>
Kiri = pixel gelap
|
Kanan = pixel terang
</p>
</div>
<div class="card">
<h2>
CV-02 Status
</h2>
<p>
OpenCV:
<b>{cv2.__version__}</b>
</p>
<p>
Dashboard otomatis
refresh setiap 5 detik.
</p>
</div>
</div>
</body>
</html>
"""
return HTMLResponse(
content=html
)
# ============================================================
# START SERVER
# ============================================================
if __name__ == "__main__":
import uvicorn
uvicorn.run(
app,
host="0.0.0.0",
port=8000
)
---
4. Jalankan server
Buka Anaconda Prompt:
conda activate vision
Kemudian:
cd C:\smart-incubator\server
Jalankan:
python main.py
Harus muncul kira-kira:
Uvicorn running on http://0.0.0.0:8000
---
5. Tes dengan foto yang sudah ada
Karena CV-01 kita sudah menyimpan foto, kita tidak perlu upload ulang.
Pastikan ada file di:
C:\smart-incubator\storage\images\
Misalnya:
C:\smart-incubator\
└── storage\
└── images\
└── 2026-09-06\
└── 20260906_221530_a1b2c3.jpg
Kemudian buka Chrome/Edge:
http://127.0.0.1:8000/vision
---
6. Hasil yang akan kita dapat
Dashboard akan menampilkan kira-kira:
┌──────────────────────────────────────────┐
│ SMART INCUBATOR │
│ Computer Vision CV-02 │
├──────────────────────────────────────────┤
│ │
│ [ FOTO TELUR ] │
│ │
├──────────────────────────────────────────┤
│ Image Information │
│ │
│ Resolution 640 × 480 pixels │
│ Channels 3 │
│ Brightness 121.43 / 255 │
│ Contrast 48.72 │
│ File Size 85,432 bytes │
├──────────────────────────────────────────┤
│ Grayscale Histogram │
│ │
│ /\ │
│ / \____ │
│ ____/ \_____ │
│ │
│ 0 128 255 │
└──────────────────────────────────────────┘
Arti parameter
Brightness
Dihitung dari rata-rata intensitas pixel grayscale:
0 = sangat gelap
128 = sedang
255 = sangat terang
Contrast
Menggunakan standar deviasi intensitas pixel:
Contrast rendah → gambar cenderung rata
Contrast tinggi → perbedaan gelap/terang lebih kuat
Histogram
Menunjukkan distribusi pixel:
0 ─────────────────────────────── 255
gelap terang
Ini akan menjadi sangat penting nanti untuk Candling Vision, karena kita perlu mengetahui apakah foto telur terlalu gelap, terlalu terang, atau memiliki distribusi cahaya yang cocok untuk analisis embrio.
---
7. Ada satu endpoint JSON juga
Selain dashboard, kita sudah membuat:
http://127.0.0.1:8000/api/v1/vision/analyze/latest
Kalau dibuka, server akan memberikan data seperti:
{
"status": "success",
"data": {
"filename": "20260906_221530_a1b2c3.jpg",
"resolution": {
"width": 640,
"height": 480,
"channels": 3
},
"brightness": 121.43,
"contrast": 48.72,
"file_size_bytes": 85432
}
}
Ini sengaja kita buat karena nantinya AI/XiaoZhi tidak perlu membaca halaman HTML.
AI cukup memanggil:
GET /api/v1/vision/analyze/latest
dan mendapatkan data terstruktur.
---
8. Arsitektur kita sekarang
Kita sudah mulai membangun fondasi yang benar:
┌──────────────────────┐
│ ESP32-CAM │
│ OV5640 │
└──────────┬───────────┘
│
FOTO JPEG
│
Main.py yang kedua
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse, 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-02 - OpenCV Image Analyzer
# Compatible with OpenCV 5.0.0
# ============================================================
APP_NAME = "Smart Incubator Computer Vision Server"
VERSION = "CV-02"
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
)
# ============================================================
# ROOT
# ============================================================
@app.get("/")
def root():
return {
"application": APP_NAME,
"version": VERSION,
"status": "running",
"module": "CV-02 OpenCV Image Analyzer",
"opencv_version": cv2.__version__
}
# ============================================================
# HEALTH CHECK
# ============================================================
@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 berupa JPG, JPEG atau PNG"
)
data = await file.read()
if not data:
raise HTTPException(
status_code=400,
detail="File kosong"
)
# --------------------------------------------------------
# Decode image menggunakan OpenCV
# --------------------------------------------------------
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 file gambar"
)
# --------------------------------------------------------
# RESOLUTION
# --------------------------------------------------------
height, width = image.shape[:2]
channels = (
image.shape[2]
if len(image.shape) == 3
else 1
)
# --------------------------------------------------------
# CREATE DATE FOLDER
# --------------------------------------------------------
today = datetime.now().strftime(
"%Y-%m-%d"
)
save_dir = IMAGE_DIR / today
save_dir.mkdir(
parents=True,
exist_ok=True
)
# --------------------------------------------------------
# CREATE UNIQUE FILE NAME
# --------------------------------------------------------
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 AS JPEG
# --------------------------------------------------------
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]
# ============================================================
# ANALYZE IMAGE WITH OPENCV
# ============================================================
def analyze_image(filepath):
# --------------------------------------------------------
# READ IMAGE
# --------------------------------------------------------
image = cv2.imread(
str(filepath),
cv2.IMREAD_COLOR
)
if image is None:
raise ValueError(
"OpenCV gagal membaca gambar"
)
# --------------------------------------------------------
# RESOLUTION
# --------------------------------------------------------
height, width = image.shape[:2]
channels = (
image.shape[2]
if len(image.shape) == 3
else 1
)
# --------------------------------------------------------
# CONVERT TO GRAYSCALE
# --------------------------------------------------------
gray = cv2.cvtColor(
image,
cv2.COLOR_BGR2GRAY
)
# --------------------------------------------------------
# BRIGHTNESS
#
# Mean pixel intensity.
#
# 0 = hitam
# 255 = putih
# --------------------------------------------------------
brightness = float(
np.mean(gray)
)
# --------------------------------------------------------
# CONTRAST
#
# Standard deviation.
# Semakin tinggi = variasi terang/gelap
# semakin besar.
# --------------------------------------------------------
contrast = float(
np.std(gray)
)
# --------------------------------------------------------
# HISTOGRAM
# --------------------------------------------------------
histogram = cv2.calcHist(
[gray],
[0],
None,
[256],
[0, 256]
)
# --------------------------------------------------------
# PENTING:
# Ubah histogram menjadi ARRAY 1 DIMENSI.
#
# Ini menghindari error:
# "invalid index to scalar variable"
#
# dan aman untuk OpenCV 5.0.0
# --------------------------------------------------------
histogram = histogram.flatten()
# --------------------------------------------------------
# NORMALIZE HISTOGRAM
# --------------------------------------------------------
histogram = cv2.normalize(
histogram,
None,
0,
220,
cv2.NORM_MINMAX
)
histogram = histogram.flatten()
# --------------------------------------------------------
# CREATE HISTOGRAM IMAGE
# --------------------------------------------------------
hist_width = 768
hist_height = 300
hist_image = np.ones(
(
hist_height,
hist_width,
3
),
dtype=np.uint8
) * 255
# --------------------------------------------------------
# GRID
# --------------------------------------------------------
for y in range(
0,
hist_height,
50
):
cv2.line(
hist_image,
(0, y),
(hist_width, y),
(220, 220, 220),
1
)
# --------------------------------------------------------
# DRAW HISTOGRAM
# --------------------------------------------------------
for i in range(
1,
256
):
x1 = int(
(i - 1)
* hist_width
/ 256
)
x2 = int(
i
* hist_width
/ 256
)
y1 = hist_height - int(
histogram[i - 1]
)
y2 = hist_height - int(
histogram[i]
)
cv2.line(
hist_image,
(x1, y1),
(x2, y2),
(0, 0, 0),
2
)
# --------------------------------------------------------
# HISTOGRAM LABELS
# --------------------------------------------------------
cv2.putText(
hist_image,
"0",
(5, hist_height - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 0),
1
)
cv2.putText(
hist_image,
"128",
(
hist_width // 2 - 15,
hist_height - 10
),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 0),
1
)
cv2.putText(
hist_image,
"255",
(
hist_width - 35,
hist_height - 10
),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 0),
1
)
# --------------------------------------------------------
# ENCODE HISTOGRAM TO BASE64
# --------------------------------------------------------
histogram_ok, encoded_hist = cv2.imencode(
".png",
hist_image
)
if not histogram_ok:
raise ValueError(
"Gagal membuat gambar histogram"
)
histogram_base64 = base64.b64encode(
encoded_hist.tobytes()
).decode(
"utf-8"
)
# --------------------------------------------------------
# ENCODE ORIGINAL IMAGE TO BASE64
# --------------------------------------------------------
image_ok, encoded_image = cv2.imencode(
".jpg",
image
)
if not image_ok:
raise ValueError(
"Gagal membaca gambar untuk dashboard"
)
image_base64 = base64.b64encode(
encoded_image.tobytes()
).decode(
"utf-8"
)
# --------------------------------------------------------
# FILE SIZE
# --------------------------------------------------------
file_size = filepath.stat().st_size
# --------------------------------------------------------
# RETURN RESULT
# --------------------------------------------------------
return {
"filename": filepath.name,
"path": str(filepath),
"resolution": {
"width": width,
"height": height,
"channels": channels
},
"brightness": round(
brightness,
2
),
"contrast": round(
contrast,
2
),
"file_size_bytes": file_size,
"histogram": histogram_base64,
"image": image_base64
}
# ============================================================
# ANALYZE LATEST IMAGE - JSON
# ============================================================
@app.get(
"/api/v1/vision/analyze/latest"
)
def analyze_latest():
latest = get_latest_image()
if latest is None:
raise HTTPException(
status_code=404,
detail="Belum ada foto di storage/images"
)
try:
result = analyze_image(
latest
)
return {
"status": "success",
"data": result
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=str(e)
)
# ============================================================
# VISION DASHBOARD
# ============================================================
@app.get(
"/vision",
response_class=HTMLResponse
)
def vision_dashboard():
latest = get_latest_image()
# --------------------------------------------------------
# NO IMAGE
# --------------------------------------------------------
if latest is None:
return HTMLResponse(
content="""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
Smart Incubator CV-02
</title>
</head>
<body>
<h1>
Smart Incubator
</h1>
<h2>
CV-02 OpenCV Image Analyzer
</h2>
<p>
Belum ada foto yang tersimpan.
</p>
<p>
Silakan upload foto terlebih dahulu
ke endpoint:
</p>
<pre>
POST /api/v1/vision/upload
</pre>
</body>
</html>
"""
)
# --------------------------------------------------------
# ANALYZE IMAGE
# --------------------------------------------------------
try:
result = analyze_image(
latest
)
except Exception as e:
return HTMLResponse(
content=f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
CV-02 Error
</title>
</head>
<body>
<h1>
OpenCV Error
</h1>
<pre>
{e}
</pre>
</body>
</html>
""",
status_code=500
)
# --------------------------------------------------------
# DATA
# --------------------------------------------------------
resolution = result[
"resolution"
]
brightness = result[
"brightness"
]
contrast = result[
"contrast"
]
image_base64 = result[
"image"
]
histogram_base64 = result[
"histogram"
]
# --------------------------------------------------------
# BRIGHTNESS STATUS
# --------------------------------------------------------
if brightness < 60:
brightness_status = (
"TERLALU GELAP"
)
elif brightness < 100:
brightness_status = (
"GELAP"
)
elif brightness <= 180:
brightness_status = (
"NORMAL"
)
elif brightness <= 220:
brightness_status = (
"TERANG"
)
else:
brightness_status = (
"TERLALU TERANG"
)
# --------------------------------------------------------
# CONTRAST STATUS
# --------------------------------------------------------
if contrast < 25:
contrast_status = (
"RENDAH"
)
elif contrast < 60:
contrast_status = (
"NORMAL"
)
Tentu. Kita reset saja main.py supaya tidak perlu tambal-sulam. Berikut versi CV-02 lengkap, sudah disesuaikan agar aman dengan OpenCV 5.0.0.
Langkah: hentikan server dengan Ctrl+C, buka C:\smart-incubator\server\main.py, hapus seluruh isinya, lalu copy-paste kode berikut.
from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import JSONResponse, 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-02 - OpenCV Image Analyzer
# Compatible with OpenCV 5.0.0
# ============================================================
APP_NAME = "Smart Incubator Computer Vision Server"
VERSION = "CV-02"
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
)
# ============================================================
# ROOT
# ============================================================
@app.get("/")
def root():
return {
"application": APP_NAME,
"version": VERSION,
"status": "running",
"module": "CV-02 OpenCV Image Analyzer",
"opencv_version": cv2.__version__
}
# ============================================================
# HEALTH CHECK
# ============================================================
@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 berupa JPG, JPEG atau PNG"
)
data = await file.read()
if not data:
raise HTTPException(
status_code=400,
detail="File kosong"
)
# --------------------------------------------------------
# Decode image menggunakan OpenCV
# --------------------------------------------------------
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 file gambar"
)
# --------------------------------------------------------
# RESOLUTION
# --------------------------------------------------------
height, width = image.shape[:2]
channels = (
image.shape[2]
if len(image.shape) == 3
else 1
)
# --------------------------------------------------------
# CREATE DATE FOLDER
# --------------------------------------------------------
today = datetime.now().strftime(
"%Y-%m-%d"
)
save_dir = IMAGE_DIR / today
save_dir.mkdir(
parents=True,
exist_ok=True
)
# --------------------------------------------------------
# CREATE UNIQUE FILE NAME
# --------------------------------------------------------
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 AS JPEG
# --------------------------------------------------------
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]
# ============================================================
# ANALYZE IMAGE WITH OPENCV
# ============================================================
def analyze_image(filepath):
# --------------------------------------------------------
# READ IMAGE
# --------------------------------------------------------
image = cv2.imread(
str(filepath),
cv2.IMREAD_COLOR
)
if image is None:
raise ValueError(
"OpenCV gagal membaca gambar"
)
# --------------------------------------------------------
# RESOLUTION
# --------------------------------------------------------
height, width = image.shape[:2]
channels = (
image.shape[2]
if len(image.shape) == 3
else 1
)
# --------------------------------------------------------
# CONVERT TO GRAYSCALE
# --------------------------------------------------------
gray = cv2.cvtColor(
image,
cv2.COLOR_BGR2GRAY
)
# --------------------------------------------------------
# BRIGHTNESS
#
# Mean pixel intensity.
#
# 0 = hitam
# 255 = putih
# --------------------------------------------------------
brightness = float(
np.mean(gray)
)
# --------------------------------------------------------
# CONTRAST
#
# Standard deviation.
# Semakin tinggi = variasi terang/gelap
# semakin besar.
# --------------------------------------------------------
contrast = float(
np.std(gray)
)
# --------------------------------------------------------
# HISTOGRAM
# --------------------------------------------------------
histogram = cv2.calcHist(
[gray],
[0],
None,
[256],
[0, 256]
)
# --------------------------------------------------------
# PENTING:
# Ubah histogram menjadi ARRAY 1 DIMENSI.
#
# Ini menghindari error:
# "invalid index to scalar variable"
#
# dan aman untuk OpenCV 5.0.0
# --------------------------------------------------------
histogram = histogram.flatten()
# --------------------------------------------------------
# NORMALIZE HISTOGRAM
# --------------------------------------------------------
histogram = cv2.normalize(
histogram,
None,
0,
220,
cv2.NORM_MINMAX
)
histogram = histogram.flatten()
# --------------------------------------------------------
# CREATE HISTOGRAM IMAGE
# --------------------------------------------------------
hist_width = 768
hist_height = 300
hist_image = np.ones(
(
hist_height,
hist_width,
3
),
dtype=np.uint8
) * 255
# --------------------------------------------------------
# GRID
# --------------------------------------------------------
for y in range(
0,
hist_height,
50
):
cv2.line(
hist_image,
(0, y),
(hist_width, y),
(220, 220, 220),
1
)
# --------------------------------------------------------
# DRAW HISTOGRAM
# --------------------------------------------------------
for i in range(
1,
256
):
x1 = int(
(i - 1)
* hist_width
/ 256
)
x2 = int(
i
* hist_width
/ 256
)
y1 = hist_height - int(
histogram[i - 1]
)
y2 = hist_height - int(
histogram[i]
)
cv2.line(
hist_image,
(x1, y1),
(x2, y2),
(0, 0, 0),
2
)
# --------------------------------------------------------
# HISTOGRAM LABELS
# --------------------------------------------------------
cv2.putText(
hist_image,
"0",
(5, hist_height - 10),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 0),
1
)
cv2.putText(
hist_image,
"128",
(
hist_width // 2 - 15,
hist_height - 10
),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 0),
1
)
cv2.putText(
hist_image,
"255",
(
hist_width - 35,
hist_height - 10
),
cv2.FONT_HERSHEY_SIMPLEX,
0.5,
(0, 0, 0),
1
)
# --------------------------------------------------------
# ENCODE HISTOGRAM TO BASE64
# --------------------------------------------------------
histogram_ok, encoded_hist = cv2.imencode(
".png",
hist_image
)
if not histogram_ok:
raise ValueError(
"Gagal membuat gambar histogram"
)
histogram_base64 = base64.b64encode(
encoded_hist.tobytes()
).decode(
"utf-8"
)
# --------------------------------------------------------
# ENCODE ORIGINAL IMAGE TO BASE64
# --------------------------------------------------------
image_ok, encoded_image = cv2.imencode(
".jpg",
image
)
if not image_ok:
raise ValueError(
"Gagal membaca gambar untuk dashboard"
)
image_base64 = base64.b64encode(
encoded_image.tobytes()
).decode(
"utf-8"
)
# --------------------------------------------------------
# FILE SIZE
# --------------------------------------------------------
file_size = filepath.stat().st_size
# --------------------------------------------------------
# RETURN RESULT
# --------------------------------------------------------
return {
"filename": filepath.name,
"path": str(filepath),
"resolution": {
"width": width,
"height": height,
"channels": channels
},
"brightness": round(
brightness,
2
),
"contrast": round(
contrast,
2
),
"file_size_bytes": file_size,
"histogram": histogram_base64,
"image": image_base64
}
# ============================================================
# ANALYZE LATEST IMAGE - JSON
# ============================================================
@app.get(
"/api/v1/vision/analyze/latest"
)
def analyze_latest():
latest = get_latest_image()
if latest is None:
raise HTTPException(
status_code=404,
detail="Belum ada foto di storage/images"
)
try:
result = analyze_image(
latest
)
return {
"status": "success",
"data": result
}
except Exception as e:
raise HTTPException(
status_code=500,
detail=str(e)
)
# ============================================================
# VISION DASHBOARD
# ============================================================
@app.get(
"/vision",
response_class=HTMLResponse
)
def vision_dashboard():
latest = get_latest_image()
# --------------------------------------------------------
# NO IMAGE
# --------------------------------------------------------
if latest is None:
return HTMLResponse(
content="""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
Smart Incubator CV-02
</title>
</head>
<body>
<h1>
Smart Incubator
</h1>
<h2>
CV-02 OpenCV Image Analyzer
</h2>
<p>
Belum ada foto yang tersimpan.
</p>
<p>
Silakan upload foto terlebih dahulu
ke endpoint:
</p>
<pre>
POST /api/v1/vision/upload
</pre>
</body>
</html>
"""
)
# --------------------------------------------------------
# ANALYZE IMAGE
# --------------------------------------------------------
try:
result = analyze_image(
latest
)
except Exception as e:
return HTMLResponse(
content=f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>
CV-02 Error
</title>
</head>
<body>
<h1>
OpenCV Error
</h1>
<pre>
{e}
</pre>
</body>
</html>
""",
status_code=500
)
# --------------------------------------------------------
# DATA
# --------------------------------------------------------
resolution = result[
"resolution"
]
brightness = result[
"brightness"
]
contrast = result[
"contrast"
]
image_base64 = result[
"image"
]
histogram_base64 = result[
"histogram"
]
# --------------------------------------------------------
# BRIGHTNESS STATUS
# --------------------------------------------------------
if brightness < 60:
brightness_status = (
"TERLALU GELAP"
)
elif brightness < 100:
brightness_status = (
"GELAP"
)
elif brightness <= 180:
brightness_status = (
"NORMAL"
)
elif brightness <= 220:
brightness_status = (
"TERANG"
)
else:
brightness_status = (
"TERLALU TERANG"
)
# --------------------------------------------------------
# CONTRAST STATUS
# --------------------------------------------------------
if contrast < 25:
contrast_status = (
"RENDAH"
)
elif contrast < 60:
contrast_status = (
"NORMAL"
)
else:
contrast_status = (
"TINGGI"
)
# --------------------------------------------------------
# HTML DASHBOARD
# --------------------------------------------------------
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta
http-equiv="refresh"
content="5"
>
<title>
Smart Incubator CV-02
</title>
<style>
body {{
font-family:
Arial,
sans-serif;
background:
#f2f2f2;
margin:
30px;
}}
.container {{
max-width:
1000px;
margin:
auto;
}}
.card {{
background:
white;
padding:
20px;
margin-top:
20px;
border-radius:
10px;
box-shadow:
0 2px 8px
rgba(0,0,0,0.15);
}}
.main-image {{
max-width:
100%;
max-height:
500px;
display:
block;
margin:
auto;
}}
.histogram {{
width:
100%;
max-width:
768px;
display:
block;
margin:
auto;
}}
table {{
border-collapse:
collapse;
width:
100%;
}}
td {{
padding:
12px;
border-bottom:
1px solid #ddd;
}}
td:first-child {{
font-weight:
bold;
width:
40%;
}}
.value {{
font-size:
20px;
}}
.status {{
font-weight:
bold;
}}
</style>
</head>
<body>
<div class="container">
<h1>
Smart Incubator
</h1>
<p>
Computer Vision CV-02 —
OpenCV Image Analyzer
</p>
<!-- =====================================================
IMAGE
====================================================== -->
<div class="card">
<h2>
Foto Terakhir
</h2>
<img
class="main-image"
src="data:image/jpeg;base64,{image_base64}"
>
<p>
<b>
File:
</b>
{result["filename"]}
</p>
</div>
<!-- =====================================================
INFORMATION
====================================================== -->
<div class="card">
<h2>
Image Information
</h2>
<table>
<tr>
<td>
Resolution
</td>
<td class="value">
{resolution["width"]}
×
{resolution["height"]}
pixels
</td>
</tr>
<tr>
<td>
Channels
</td>
<td>
{resolution["channels"]}
</td>
</tr>
<tr>
<td>
Brightness
</td>
<td class="value">
{brightness}
/
255
<span class="status">
{brightness_status}
</span>
</td>
</tr>
<tr>
<td>
Contrast
</td>
<td class="value">
{contrast}
<span class="status">
{contrast_status}
</span>
</td>
</tr>
<tr>
<td>
File Size
</td>
<td>
{result["file_size_bytes"]:,}
bytes
</td>
</tr>
</table>
</div>
<!-- =====================================================
HISTOGRAM
====================================================== -->
<div class="card">
<h2>
Grayscale Histogram
</h2>
<img
class="histogram"
src="data:image/png;base64,{histogram_base64}"
>
<p>
<b>
0
</b>
=
gelap
<b>
128
</b>
=
sedang
<b>
255
</b>
=
terang
</p>
</div>
<!-- =====================================================
SYSTEM
====================================================== -->
<div class="card">
<h2>
CV-02 System Status
</h2>
<p>
OpenCV Version:
<b>
{cv2.__version__}
</b>
</p>
<p>
Analyzer:
<b>
OpenCV
</b>
</p>
<p>
Status:
<b>
RUNNING
</b>
</p>
<p>
Dashboard otomatis
refresh setiap 5 detik.
</p>
</div>
</div>
</body>
</html>
"""
return HTMLResponse(
content=html
)
# ============================================================
# START SERVER
# ============================================================
if __name__ == "__main__":
import uvicorn
print()
print("=" * 60)
print("SMART INCUBATOR COMPUTER VISION SERVER")
print("CV-02 - OpenCV Image Analyzer")
print("=" * 60)
print()
print(
"OpenCV:",
cv2.__version__
)
print()
print(
"Dashboard:"
)
print(
"http://127.0.0.1:8000/vision"
)
print()
print(
"API:"
)
print(
"http://127.0.0.1:8000/api/v1/vision/analyze/latest"
)
print()
print("=" * 60)
print()
uvicorn.run(
app,
host="0.0.0.0",
port=8000
)
3. Jalankan
Di Anaconda Prompt:
conda activate vision
cd C:\smart-incubator\server
python main.py
Sekarang harus muncul:
============================================================
SMART INCUBATOR COMPUTER VISION SERVER
CV-02 - OpenCV Image Analyzer
============================================================
OpenCV: 5.0.0
Dashboard:
http://127.0.0.1:8000/vision
API:
http://127.0.0.1:8000/api/v1/vision/analyze/latest
============================================================
Uvicorn running on http://0.0.0.0:8000
4. Tes berurutan
Jangan langsung ke /vision. Kita cek satu per satu.
Tes 1 — Server
Buka:
http://127.0.0.1:8000
Harus muncul JSON:
{
"application": "Smart Incubator Computer Vision Server",
"version": "CV-02",
"status": "running",
"module": "CV-02 OpenCV Image Analyzer",
"opencv_version": "5.0.0"
}
Tes 2 — OpenCV
Buka:
http://127.0.0.1:8000/health
Harus:
{
"status": "ok",
"opencv_version": "5.0.0"
}
Tes 3 — Dashboard
Kemudian:
http://127.0.0.1:8000/vision
Kalau foto
Post a Comment for "CV-02"