Langkah 3

from fastapi import FastAPI, UploadFile, File, HTTPException
from fastapi.responses import HTMLResponse
from pathlib import Path
from datetime import datetime
import uuid
import cv2
import numpy as np
import base64


# ============================================================
# SMART INCUBATOR COMPUTER VISION SERVER
# CV-03 - OpenCV PREPROCESSING + DASHBOARD
#
# Compatible with OpenCV 5.0.0
# ============================================================

APP_NAME = "Smart Incubator Computer Vision Server"
VERSION = "CV-03"

BASE_DIR = Path(__file__).resolve().parent.parent

IMAGE_DIR = BASE_DIR / "storage" / "images"

IMAGE_DIR.mkdir(
    parents=True,
    exist_ok=True
)


# ============================================================
# FASTAPI
# ============================================================

app = FastAPI(
    title=APP_NAME,
    version=VERSION
)


# ============================================================
# HELPER
# ============================================================

def image_to_base64(image, extension=".jpg"):

    success, encoded = cv2.imencode(
        extension,
        image
    )

    if not success:
        raise ValueError(
            f"Gagal melakukan encode gambar {extension}"
        )

    return base64.b64encode(
        encoded.tobytes()
    ).decode("utf-8")


# ============================================================
# ROOT
# ============================================================

@app.get("/")
def root():

    return {
        "application": APP_NAME,
        "version": VERSION,
        "status": "running",
        "module": "CV-03 OpenCV Preprocessing",
        "opencv_version": cv2.__version__
    }


# ============================================================
# HEALTH
# ============================================================

@app.get("/health")
def health():

    return {
        "status": "ok",
        "opencv_version": cv2.__version__
    }


# ============================================================
# UPLOAD IMAGE
# ============================================================

@app.post("/api/v1/vision/upload")
async def upload_image(
    file: UploadFile = File(...)
):

    allowed_types = [
        "image/jpeg",
        "image/jpg",
        "image/png"
    ]

    if file.content_type not in allowed_types:

        raise HTTPException(
            status_code=400,
            detail="File harus JPG, JPEG atau PNG"
        )

    data = await file.read()

    if not data:

        raise HTTPException(
            status_code=400,
            detail="File kosong"
        )

    # --------------------------------------------------------
    # Decode
    # --------------------------------------------------------

    image_array = np.frombuffer(
        data,
        dtype=np.uint8
    )

    image = cv2.imdecode(
        image_array,
        cv2.IMREAD_COLOR
    )

    if image is None:

        raise HTTPException(
            status_code=400,
            detail="OpenCV tidak dapat membaca gambar"
        )

    # --------------------------------------------------------
    # Resolution
    # --------------------------------------------------------

    height, width = image.shape[:2]

    channels = (
        image.shape[2]
        if len(image.shape) == 3
        else 1
    )

    # --------------------------------------------------------
    # Folder tanggal
    # --------------------------------------------------------

    today = datetime.now().strftime(
        "%Y-%m-%d"
    )

    save_dir = IMAGE_DIR / today

    save_dir.mkdir(
        parents=True,
        exist_ok=True
    )

    # --------------------------------------------------------
    # Filename
    # --------------------------------------------------------

    timestamp = datetime.now().strftime(
        "%Y%m%d_%H%M%S"
    )

    unique_id = uuid.uuid4().hex[:6]

    filename = (
        f"{timestamp}_{unique_id}.jpg"
    )

    filepath = save_dir / filename

    # --------------------------------------------------------
    # Save
    # --------------------------------------------------------

    success = cv2.imwrite(
        str(filepath),
        image,
        [
            cv2.IMWRITE_JPEG_QUALITY,
            95
        ]
    )

    if not success:

        raise HTTPException(
            status_code=500,
            detail="Gagal menyimpan gambar"
        )

    return {

        "status": "success",

        "message": "Foto berhasil diterima",

        "filename": filename,

        "path": str(filepath),

        "width": width,

        "height": height,

        "channels": channels,

        "size_bytes": len(data),

        "timestamp": datetime.now().isoformat()

    }


# ============================================================
# FIND LATEST IMAGE
# ============================================================

def get_latest_image():

    files = list(
        IMAGE_DIR.rglob("*.jpg")
    )

    if not files:
        return None

    files.sort(
        key=lambda x: x.stat().st_mtime,
        reverse=True
    )

    return files[0]


# ============================================================
# BASIC IMAGE INFORMATION
# ============================================================

def get_basic_info(image):

    height, width = image.shape[:2]

    channels = (
        image.shape[2]
        if len(image.shape) == 3
        else 1
    )

    gray = cv2.cvtColor(
        image,
        cv2.COLOR_BGR2GRAY
    )

    brightness = float(
        np.mean(gray)
    )

    contrast = float(
        np.std(gray)
    )

    return {
        "width": width,
        "height": height,
        "channels": channels,
        "brightness": round(
            brightness,
            2
        ),
        "contrast": round(
            contrast,
            2
        )
    }


# ============================================================
# PREPROCESSING
# ============================================================

def preprocess_image(image):

    # --------------------------------------------------------
    # 1. GRAYSCALE
    # --------------------------------------------------------

    gray = cv2.cvtColor(
        image,
        cv2.COLOR_BGR2GRAY
    )

    # --------------------------------------------------------
    # 2. GAUSSIAN BLUR
    #
    # Mengurangi noise kamera.
    # --------------------------------------------------------

    blurred = cv2.GaussianBlur(
        gray,
        (5, 5),
        0
    )

    # --------------------------------------------------------
    # 3. CLAHE
    #
    # Meningkatkan kontras lokal.
    # --------------------------------------------------------

    clahe = cv2.createCLAHE(
        clipLimit=2.0,
        tileGridSize=(8, 8)
    )

    enhanced = clahe.apply(
        blurred
    )

    # --------------------------------------------------------
    # 4. OTSU THRESHOLD
    #
    # Membuat gambar hitam-putih
    # secara otomatis.
    # --------------------------------------------------------

    threshold_value, threshold = cv2.threshold(
        enhanced,
        0,
        255,
        cv2.THRESH_BINARY + cv2.THRESH_OTSU
    )

    # --------------------------------------------------------
    # 5. CANNY EDGE
    #
    # Mendeteksi garis / tepi objek.
    # --------------------------------------------------------

    edges = cv2.Canny(
        enhanced,
        50,
        150
    )

    # --------------------------------------------------------
    # 6. MORPHOLOGICAL CLEANUP
    #
    # Menutup lubang kecil dan noise.
    # --------------------------------------------------------

    kernel = cv2.getStructuringElement(
        cv2.MORPH_ELLIPSE,
        (5, 5)
    )

    morph = cv2.morphologyEx(
        threshold,
        cv2.MORPH_CLOSE,
        kernel,
        iterations=2
    )

    # --------------------------------------------------------
    # 7. CONTOUR
    # --------------------------------------------------------

    contours, hierarchy = cv2.findContours(
        morph,
        cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_SIMPLE
    )

    # --------------------------------------------------------
    # Buat gambar contour
    # --------------------------------------------------------

    contour_image = image.copy()

    contour_count = 0

    image_area = (
        image.shape[0]
        * image.shape[1]
    )

    min_area = image_area * 0.002

    max_area = image_area * 0.95

    valid_contours = []

    for contour in contours:

        area = cv2.contourArea(
            contour
        )

        if (
            area >= min_area
            and area <= max_area
        ):

            valid_contours.append(
                contour
            )

    contour_count = len(
        valid_contours
    )

    # --------------------------------------------------------
    # Draw contour
    # --------------------------------------------------------

    cv2.drawContours(
        contour_image,
        valid_contours,
        -1,
        (0, 255, 0),
        2
    )

    # --------------------------------------------------------
    # Label contour
    # --------------------------------------------------------

    for index, contour in enumerate(
        valid_contours,
        start=1
    ):

        x, y, w, h = cv2.boundingRect(
            contour
        )

        cv2.putText(
            contour_image,
            str(index),
            (x, y - 5),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.6,
            (0, 0, 255),
            2
        )

    return {

        "gray": gray,

        "blurred": blurred,

        "enhanced": enhanced,

        "threshold": threshold,

        "edges": edges,

        "morph": morph,

        "contour": contour_image,

        "threshold_value": round(
            float(threshold_value),
            2
        ),

        "contour_count": contour_count

    }


# ============================================================
# ANALYZE + PREPROCESS LATEST IMAGE
# ============================================================

def analyze_latest_image():

    filepath = get_latest_image()

    if filepath is None:

        raise FileNotFoundError(
            "Belum ada foto di storage/images"
        )

    # --------------------------------------------------------
    # Read
    # --------------------------------------------------------

    image = cv2.imread(
        str(filepath),
        cv2.IMREAD_COLOR
    )

    if image is None:

        raise ValueError(
            "OpenCV gagal membaca foto"
        )

    # --------------------------------------------------------
    # Basic information
    # --------------------------------------------------------

    info = get_basic_info(
        image
    )

    # --------------------------------------------------------
    # Preprocessing
    # --------------------------------------------------------

    processed = preprocess_image(
        image
    )

    return {

        "filename": filepath.name,

        "path": str(filepath),

        "info": info,

        "processed": processed

    }


# ============================================================
# JSON ANALYSIS
# ============================================================

@app.get(
    "/api/v1/vision/preprocess/latest"
)
def preprocess_latest():

    try:

        result = analyze_latest_image()

    except FileNotFoundError as e:

        raise HTTPException(
            status_code=404,
            detail=str(e)
        )

    except Exception as e:

        raise HTTPException(
            status_code=500,
            detail=str(e)
        )

    processed = result[
        "processed"
    ]

    return {

        "status": "success",

        "filename": result[
            "filename"
        ],

        "resolution": result[
            "info"
        ],

        "preprocessing": {

            "gaussian_blur": "5x5",

            "clahe_clip_limit": 2.0,

            "clahe_grid": "8x8",

            "otsu_threshold": processed[
                "threshold_value"
            ],

            "canny_low": 50,

            "canny_high": 150,

            "morphology_kernel": "5x5",

            "contour_count": processed[
                "contour_count"
            ]

        }

    }


# ============================================================
# DASHBOARD
# ============================================================

@app.get(
    "/vision",
    response_class=HTMLResponse
)
def vision_dashboard():

    try:

        result = analyze_latest_image()

    except FileNotFoundError:

        return HTMLResponse(

            content="""

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>
Smart Incubator CV-03
</title>

</head>

<body>

<h1>
Smart Incubator
</h1>

<h2>
CV-03 OpenCV Preprocessing
</h2>

<p>
Belum ada foto di:
</p>

<pre>
storage/images
</pre>

</body>

</html>

"""

        )

    except Exception as e:

        return HTMLResponse(

            content=f"""

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>
CV-03 Error
</title>

</head>

<body>

<h1>
OpenCV Error
</h1>

<pre>
{e}
</pre>

</body>

</html>

""",

            status_code=500

        )

    # --------------------------------------------------------
    # DATA
    # --------------------------------------------------------

    filepath = get_latest_image()

    image = cv2.imread(
        str(filepath),
        cv2.IMREAD_COLOR
    )

    processed = result[
        "processed"
    ]

    info = result[
        "info"
    ]

    # --------------------------------------------------------
    # Convert images to Base64
    # --------------------------------------------------------

    original_b64 = image_to_base64(
        image,
        ".jpg"
    )

    gray_b64 = image_to_base64(
        processed["gray"],
        ".jpg"
    )

    blurred_b64 = image_to_base64(
        processed["blurred"],
        ".jpg"
    )

    enhanced_b64 = image_to_base64(
        processed["enhanced"],
        ".jpg"
    )

    threshold_b64 = image_to_base64(
        processed["threshold"],
        ".jpg"
    )

    edges_b64 = image_to_base64(
        processed["edges"],
        ".jpg"
    )

    morph_b64 = image_to_base64(
        processed["morph"],
        ".jpg"
    )

    contour_b64 = image_to_base64(
        processed["contour"],
        ".jpg"
    )

    # --------------------------------------------------------
    # HTML
    # --------------------------------------------------------

    html = f"""

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<meta
    http-equiv="refresh"
    content="5"
>

<title>
Smart Incubator CV-03
</title>

<style>

body {{

    font-family:
        Arial,
        sans-serif;

    background:
        #f2f2f2;

    margin:
        20px;

}}

.container {{

    max-width:
        1200px;

    margin:
        auto;

}}

.header {{

    background:
        white;

    padding:
        20px;

    border-radius:
        10px;

    box-shadow:
        0 2px 8px
        rgba(0,0,0,0.15);

}}

.grid {{

    display:
        grid;

    grid-template-columns:
        repeat(2, 1fr);

    gap:
        20px;

    margin-top:
        20px;

}}

.card {{

    background:
        white;

    padding:
        15px;

    border-radius:
        10px;

    box-shadow:
        0 2px 8px
        rgba(0,0,0,0.15);

}}

.card img {{

    width:
        100%;

    max-height:
        350px;

    object-fit:
        contain;

    background:
        #111;

}}

.card h2 {{

    font-size:
        18px;

}}

.info {{

    background:
        white;

    margin-top:
        20px;

    padding:
        20px;

    border-radius:
        10px;

    box-shadow:
        0 2px 8px
        rgba(0,0,0,0.15);

}}

table {{

    width:
        100%;

    border-collapse:
        collapse;

}}

td {{

    padding:
        10px;

    border-bottom:
        1px solid #ddd;

}}

td:first-child {{

    font-weight:
        bold;

    width:
        45%;

}}

.value {{

    font-size:
        18px;

}}

.badge {{

    display:
        inline-block;

    padding:
        6px 12px;

    border-radius:
        6px;

    background:
        #eeeeee;

    font-weight:
        bold;

}}

@media(max-width:800px) {{

    .grid {{

        grid-template-columns:
            1fr;

    }}

}}

</style>

</head>


<body>


<div class="container">


<!-- ======================================================
     HEADER
======================================================= -->

<div class="header">

<h1>
Smart Incubator
</h1>

<h2>
CV-03 — OpenCV Preprocessing
</h2>

<p>
File:
<b>
{result["filename"]}
</b>
</p>

<p>
OpenCV:
<b>
{cv2.__version__}
</b>
</p>

</div>


<!-- ======================================================
     BASIC INFORMATION
======================================================= -->

<div class="info">

<h2>
Informasi Foto
</h2>

<table>


<tr>

<td>
Resolusi
</td>

<td class="value">

{info["width"]}
×
{info["height"]}
pixels

</td>

</tr>


<tr>

<td>
Brightness
</td>

<td class="value">

{info["brightness"]}

/
255

</td>

</tr>


<tr>

<td>
Contrast
</td>

<td class="value">

{info["contrast"]}

</td>

</tr>


<tr>

<td>
Otsu Threshold
</td>

<td class="value">

{processed["threshold_value"]}

</td>

</tr>


<tr>

<td>
Valid Contours
</td>

<td class="value">

<span class="badge">

{processed["contour_count"]}

</span>

</td>

</tr>


</table>

</div>


<!-- ======================================================
     IMAGE GRID
======================================================= -->

<div class="grid">


<!-- ORIGINAL -->

<div class="card">

<h2>
1. Original
</h2>

<img
src="data:image/jpeg;base64,{original_b64}"
>

<p>
Foto asli dari kamera.
</p>

</div>


<!-- GRAYSCALE -->

<div class="card">

<h2>
2. Grayscale
</h2>

<img
src="data:image/jpeg;base64,{gray_b64}"
>

<p>
Foto diubah menjadi
grayscale 8-bit.
</p>

</div>


<!-- BLUR -->

<div class="card">

<h2>
3. Gaussian Blur
</h2>

<img
src="data:image/jpeg;base64,{blurred_b64}"
>

<p>
Kernel:
<b>
5 × 5
</b>
</p>

</div>


<!-- CLAHE -->

<div class="card">

<h2>
4. CLAHE Enhancement
</h2>

<img
src="data:image/jpeg;base64,{enhanced_b64}"
>

<p>
Local contrast enhancement.
</p>

</div>


<!-- THRESHOLD -->

<div class="card">

<h2>
5. Otsu Threshold
</h2>

<img
src="data:image/jpeg;base64,{threshold_b64}"
>

<p>
Threshold otomatis:
<b>
{processed["threshold_value"]}
</b>
</p>

</div>


<!-- CANNY -->

<div class="card">

<h2>
6. Canny Edge
</h2>

<img
src="data:image/jpeg;base64,{edges_b64}"
>

<p>
Canny:
<b>
50 — 150
</b>
</p>

</div>


<!-- MORPH -->

<div class="card">

<h2>
7. Morphological Cleanup
</h2>

<img
src="data:image/jpeg;base64,{morph_b64}"
>

<p>
Kernel:
<b>
5 × 5
</b>
</p>

</div>


<!-- CONTOUR -->

<div class="card">

<h2>
8. Contour Detection
</h2>

<img
src="data:image/jpeg;base64,{contour_b64}"
>

<p>

Valid contours:
<b>
{processed["contour_count"]}
</b>

</p>

</div>


</div>


<!-- ======================================================
     PIPELINE
======================================================= -->

<div class="info">

<h2>
CV-03 Processing Pipeline
</h2>

<p>

<b>
Original
</b>

→

<b>
Grayscale
</b>

→

<b>
Gaussian Blur
</b>

→

<b>
CLAHE
</b>

→

<b>
Otsu Threshold
</b>

→

<b>
Morphology
</b>

→

<b>
Contour
</b>

</p>

<p>

Canny Edge digunakan sebagai
jalur tambahan untuk melihat
tepi objek.

</p>

</div>


<!-- ======================================================
     SYSTEM
======================================================= -->

<div class="info">

<h2>
System Status
</h2>

<p>
Server:
<b>
RUNNING
</b>
</p>

<p>
OpenCV:
<b>
{cv2.__version__}
</b>
</p>

<p>
Mode:
<b>
CV-03 PREPROCESSING
</b>
</p>

<p>
Dashboard refresh:
<b>
5 detik
</b>
</p>

</div>


</div>


</body>

</html>

"""

    return HTMLResponse(
        content=html
    )


# ============================================================
# START SERVER
# ============================================================

if __name__ == "__main__":

    import uvicorn

    print()
    print("=" * 65)
    print(
        "SMART INCUBATOR COMPUTER VISION SERVER"
    )
    print(
        "CV-03 - OpenCV PREPROCESSING"
    )
    print("=" * 65)
    print()

    print(
        "OpenCV:",
        cv2.__version__
    )

    print()

    print(
        "Dashboard:"
    )

    print(
        "http://127.0.0.1:8000/vision"
    )

    print()

    print(
        "Preprocessing API:"
    )

    print(
        "http://127.0.0.1:8000/api/v1/vision/preprocess/latest"
    )

    print()

    print("=" * 65)
    print()

    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8000
    )

Post a Comment for "Langkah 3"