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

&nbsp;

<span class="status">

{brightness_status}

</span>

</td>

</tr>


<tr>

<td>
Contrast
</td>

<td class="value">

{contrast}

&nbsp;

<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

&nbsp;&nbsp;&nbsp;

<b>
128
</b>
=
sedang

&nbsp;&nbsp;&nbsp;

<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"