Saturday, October 31, 2026

Parking Space Counter with Python and YOLO: From Detections to Occupancy

Finding vehicles is only half of a parking-space counter. The application must also decide which detected vehicle belongs to which monitored space.

In this Python and YOLO example, six calibrated regions produce four occupied and two free statuses. Raising one application threshold changes the result to three occupied and three free, even though the image has not changed. Following that change back to one detection makes the rule much easier to understand.

We use a synthetic 1024 × 1024 course scene and a local Windows/CPU run. This is a static-image demonstration, not a live-camera reliability or accuracy benchmark. Other partial vehicles appear outside the six monitored regions; the reported total is about those six spaces, not every vehicle in the image.

Watch the Parking Lot Monitor walkthrough

Watch the complete Python and YOLO tutorial, or see the quick parking-space demonstration.

Actual parking result at application threshold 0.20: six calibrated spaces, four occupied and two free, with accepted detection boxes and centers
Red regions are occupied and green regions free according to the rule. Yellow rectangles and points show accepted detector boxes and their centers. The regions approximate perspective bays rather than tracing their painted outlines.

The instructional figures include extra yellow detection boxes and centers to make the decisions inspectable. The minimal script below saves the colored region-and-ID overlays and prints the assignment rows; it does not add those extra yellow annotations to its saved images.

The pipeline: image, detections, regions, decisions

  1. Load one explicit local image, checkpoint and region calibration.
  2. Run YOLOv8n once with a candidate confidence floor of 0.10.
  3. Keep vehicle classes: car, truck and bus.
  4. Filter those fixed candidates using the parking acceptance threshold.
  5. Assign accepted box centers to the calibrated rectangles.
  6. Count the resulting occupied and free rows.

YOLO supplies predictions; our Python rule supplies the occupancy decision. A free row means that no accepted candidate center matched that region. It does not independently verify that the physical space is empty.

Prepare the local inputs

Save the complete script below as parking.py. Place yolov8n.pt, parking_lot_sample.png and parking_zones.json beside it. The demonstrated input and its calibration are included in lesson 50 of my course. You can instead use your own permitted image, but you must calibrate regions for that image.

The tested checkpoint matches the official YOLOv8n release asset, SHA-256 f59b3d833e2ff32e194b5bb8e08d211dc7c5bdf144b90d2c8412c47ccfc83b36. Obtain that file as a separate setup step and review the model license and Ultralytics licensing options. The script checks that the required local files exist before loading the model; it does not download missing inputs.

For Windows with Python 3.13 installed:

py -3.13 -m venv .venv
.venv\Scripts\python.exe -m pip install ultralytics==8.4.24 numpy==2.3.5 opencv-python==4.13.0.92
.venv\Scripts\python.exe parking.py

The measured environment used Python 3.13.9, Ultralytics 8.4.24, PyTorch 2.10.0, NumPy 2.3.5 and OpenCV 4.13.0.92. The installation command pins three direct dependencies, not the complete transitive environment. This is not a fresh-install claim for every operating system. Installation and obtaining the model may need internet access; the demonstrated processing runs offline with local files.

Calibrate regions in the input image's coordinates

Here is the demonstrated calibration. Save it as parking_zones.json. Coordinates are [left, top, right, bottom] in the original 1024 × 1024 image:

{
  "image_size": [1024, 1024],
  "zones": [
    {"spot_id": "A1", "rect": [146, 55, 399, 337]},
    {"spot_id": "A2", "rect": [402, 55, 649, 337]},
    {"spot_id": "A3", "rect": [651, 55, 856, 337]},
    {"spot_id": "B1", "rect": [72, 570, 373, 978]},
    {"spot_id": "B2", "rect": [375, 570, 696, 978]},
    {"spot_id": "B3", "rect": [698, 570, 959, 978]}
  ]
}
Six calibrated non-overlapping rectangular regions labelled A1 A2 A3 and B1 B2 B3 on the unchanged synthetic parking input
These coordinates belong to this image and viewpoint. Equal dimensions alone do not make them valid for another camera.

load_zones rejects a calibration with different image dimensions, an empty region list, duplicate identifiers, invalid rectangles, or overlapping regions. Those checks catch structural mistakes. They do not prove that a rectangle corresponds to the correct physical bay. Inspect the overlay, especially after changing the image or viewpoint.

Complete runnable parking-space counter

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 ARTSCI - Author of the AI/ML/CV course materials.
"""Assign local YOLO candidates to calibrated parking spaces; no asset downloads."""
from pathlib import Path
import json
import sys
import cv2
import numpy as np
from ultralytics import YOLO


def load_zones(path, shape):
    payload = json.loads(Path(path).read_text(encoding="utf-8"))
    if payload["image_size"] != [shape[1], shape[0]]:
        raise ValueError("Zone calibration dimensions differ from the selected image.")
    zones = payload["zones"]
    if not zones or len({z["spot_id"] for z in zones}) != len(zones):
        raise ValueError("Use nonempty zones with unique spot IDs.")
    for i, zone in enumerate(zones):
        x1, y1, x2, y2 = zone["rect"]
        if not 0 <= x1 < x2 <= shape[1] or not 0 <= y1 < y2 <= shape[0]:
            raise ValueError("Invalid or out-of-image zone.")
        for other in zones[:i]:
            a, b, c, d = other["rect"]
            if max(x1, a) < min(x2, c) and max(y1, b) < min(y2, d):
                raise ValueError("Overlapping zones could assign one center twice.")
    return zones


def detect_candidates(model, image):
    result = model.predict(
        source=image, conf=0.10, imgsz=640, device="cpu", iou=0.70,
        max_det=300, rect=True, half=False, classes=None,
        augment=False, agnostic_nms=False, verbose=False,
    )[0]
    detections = []
    for box in result.boxes:
        label = result.names[int(box.cls.item())]
        if label not in {"car", "truck", "bus"}:
            continue
        x1, y1, x2, y2 = box.xyxy[0].cpu().tolist()
        detections.append(dict(
            label=label, confidence=float(box.conf.item()), box=[x1, y1, x2, y2],
            center=[(x1 + x2) / 2, (y1 + y2) / 2],
        ))
    return detections


def assign_spots(detections, zones, confidence=0.20):
    accepted = [d for d in detections if d["confidence"] >= confidence]
    rows = []
    for zone in zones:
        x1, y1, x2, y2 = zone["rect"]
        matches = [d for d in accepted
                   if x1 <= d["center"][0] < x2 and y1 <= d["center"][1] < y2]
        best = max(matches, key=lambda d: d["confidence"]) if matches else None
        rows.append(dict(spot_id=zone["spot_id"], status="occupied" if best else "free",
                         confidence=round(best["confidence"], 3) if best else None,
                         center=best["center"] if best else None))
    return rows


def draw_status(image, zones, rows):
    canvas = image.copy()
    states = {row["spot_id"]: row["status"] for row in rows}
    for zone in zones:
        x1, y1, x2, y2 = zone["rect"]
        color = (70, 170, 70) if states[zone["spot_id"]] == "free" else (50, 70, 225)
        cv2.rectangle(canvas, (x1, y1), (x2, y2), color, 4)
        cv2.putText(canvas, zone["spot_id"], (x1 + 8, y1 + 32),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, color, 3)
    return canvas


def main():
    root = Path(__file__).resolve().parent
    image_path = Path(sys.argv[1]) if len(sys.argv) > 1 else root / "parking_lot_sample.png"
    zones_path = Path(sys.argv[2]) if len(sys.argv) > 2 else root / "parking_zones.json"
    weights = root / "yolov8n.pt"
    for path in (image_path, zones_path, weights):
        if not path.is_file():
            raise FileNotFoundError(f"Required local file: {path}")
    image = cv2.imdecode(np.fromfile(image_path, dtype=np.uint8), cv2.IMREAD_COLOR)
    if image is None:
        raise ValueError(f"Cannot decode image: {image_path}")
    zones = load_zones(zones_path, image.shape)
    detections = detect_candidates(YOLO(str(weights)), image)
    output = root / "output"
    output.mkdir(exist_ok=True)
    for confidence in (0.20, 0.30):
        rows = assign_spots(detections, zones, confidence)
        occupied = sum(row["status"] == "occupied" for row in rows)
        print(f"threshold={confidence:.2f}: {occupied} occupied, {len(rows)-occupied} free")
        for row in rows:
            print(f"  {row['spot_id']}: {row['status']} (score={row['confidence']})")
        if not cv2.imwrite(str(output / f"parking_{confidence:.2f}.png"),
                           draw_status(image, zones, rows)):
            raise OSError("Could not save parking overlay.")


if __name__ == "__main__":
    main()

Complete sample source above: copyright © 2026 ARTSCI — Author of the AI/ML/CV course materials, licensed under GNU AGPL version 3, without warranty. Keep applicable notices and provide corresponding source when sharing covered modifications. Ultralytics software and weights retain their own terms. Independent course images, narration, slides and videos are not automatically licensed under AGPL.

The script's default paths are relative to the saved Python file. To select your own image and its matching calibration, pass both paths:

.venv\Scripts\python.exe parking.py "C:\images\my_parking.png" "C:\images\my_zones.json"

Keep detector inference separate from the parking threshold

detect_candidates runs prediction once, with conf=0.10, imgsz=640, CPU and the other shown settings fixed. In this execution it returns eight vehicle candidates. Some are outside the monitored spaces, and two predictions can describe the same region. A candidate count is not a parking-space count.

The loop in main compares application thresholds 0.20 and 0.30 by filtering that same post-NMS candidate set. It does not rerun YOLO or non-maximum suppression. This controlled comparison keeps the image, model and underlying predictions unchanged. The official prediction documentation describes confidence and overlap-suppression settings; our additional acceptance threshold belongs to the application rule.

Assign the center, then count the space

For each box, the center is ((x1+x2)/2, (y1+y2)/2). assign_spots tests that center against each region, including left/top boundaries and excluding right/bottom boundaries. With non-overlapping regions, a point cannot belong to two spaces. If several accepted centers match one space, the rule keeps the highest-scoring match and still counts only one occupied row.

This is a center-in-rectangle rule, not an intersection-over-union or segmentation calculation. A long, partly occluded or incorrectly localized vehicle may not behave as expected. Test the geometry and the detector output separately.

Inspect the actual output

threshold=0.20: 4 occupied, 2 free
  A1: occupied (score=0.771)
  A2: occupied (score=0.428)
  A3: free (score=None)
  B1: occupied (score=0.222)
  B2: occupied (score=0.501)
  B3: free (score=None)
threshold=0.30: 3 occupied, 3 free
  A1: occupied (score=0.771)
  A2: occupied (score=0.428)
  A3: free (score=None)
  B1: free (score=None)
  B2: occupied (score=0.501)
  B3: free (score=None)

At 0.20, A1, A2, B1 and B2 are occupied; A3 and B3 are free according to the rule. Seven candidate detections pass that threshold, but only four monitored spaces receive a match. The detector labels the red vehicle in B2 as bus with score about 0.501. The application accepts that class as a vehicle, so a useful occupancy result here does not establish perfect vehicle classification.

Why did B1 become free when the car stayed?

The B1 candidate has score approximately 0.22236 and center (229.74, 740.91). It passes 0.20, but fails 0.30. B1 therefore loses its accepted center and changes to free. Five candidates pass the higher threshold, while only three monitored spaces receive matches.

Crop of B1 at threshold 0.20 showing the real accepted vehicle box, center and score about 0.22
Inspecting one candidate explains the changed decision. The center coordinates refer to the original full image, not this crop's coordinate system.
Actual result at application threshold 0.30 on the identical image: B1 changes to free although the vehicle remains visible
Same pixels, same calibration and same detector candidate set. Only the application acceptance threshold changed.

A stricter threshold can remove mistakes, but it can also miss occupied spaces. A confidence score is not calibrated certainty, and a higher threshold does not automatically make a counter more accurate.

What to verify before adapting the example

Use permitted inputs, recalibrate the regions, and inspect each selected vehicle center alongside the assignment table. Test weak detections, occlusion, shadows and boundaries. Check representative labelled examples before relying on physical occupancy claims. This single synthetic scene does not measure field accuracy or live-camera performance.

For related projects, see how to read YOLO detection outputs and why track IDs are not vehicle counts. My English course, Computer Vision: OpenCV, Machine Learning and YOLO, develops the full Parking Lot Monitor application alongside other practical projects.

Friday, October 30, 2026

Python Document Scanner: Detect Corners and Correct Perspective

A tilted page can become a rectangular scan without training a neural network. The useful idea is to find a plausible page boundary, keep its four corners in a consistent order, and map them onto a rectangle.

This walkthrough builds a small Python document scanner with OpenCV. It runs locally on a CPU and saves both a color result and an optional black-and-white view. There is no model checkpoint and no OCR. The example uses an explicitly synthetic document scene from my course, so a clean result here is a demonstration of the pipeline rather than evidence that every photograph will work.

Watch the working example

Watch the complete Python and OpenCV scanner walkthrough, or see the quick before-and-after demonstration.

Actual OpenCV perspective correction: synthetic tilted page input beside the rectangular color scan
Same input pixels, transformed using four detected corners. The result is 740 × 523 pixels in this execution.

What does the scanner actually do?

  1. Convert the image to grayscale and blur small variations.
  2. Use Canny edges and morphological closing to prepare boundaries.
  3. Select the largest qualifying convex four-corner contour.
  4. Order those corners and calculate a perspective transform.
  5. Save the flattened color image and an optional thresholded view.

OpenCV's contour documentation explains how contours represent boundaries in a binary image. A boundary is a shape, not an understanding of the object. That distinction is the main limitation to keep in mind.

Prepare Python, OpenCV and one local image

Save the full code below as scanner.py. Place document_scene.png beside it to use the demonstrated course input, available in lesson 53. Alternatively, supply the path to an image you have permission to use. The example does not download or silently replace your image.

On Windows with Python 3.13 installed, create a separate environment and install the two demonstrated package versions:

py -3.13 -m venv .venv
.venv\Scripts\python.exe -m pip install numpy==2.3.5 opencv-python==4.13.0.92
.venv\Scripts\python.exe scanner.py

To process your own file instead of the default input:

.venv\Scripts\python.exe scanner.py "C:\images\my_page.jpg"

The measured execution used Python 3.13.9, NumPy 2.3.5 and OpenCV 4.13.0.92 on Windows. These commands describe that setup; they are not a fresh-install test of every operating system. Package installation may require internet access. Once the packages and image are present, this scanner needs no network connection, model download or GPU.

Complete runnable scanner

# Copyright (c) 2026 ARTSCI - Author of the AI/ML/CV course materials.
"""Straighten a page candidate with local OpenCV operations; no trained model."""
from pathlib import Path
import sys
import cv2
import numpy as np


def edge_map(image, low=50, high=150):
    if not 0 <= low < high:
        raise ValueError("Require 0 <= low < high.")
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    edges = cv2.Canny(blurred, low, high)
    return cv2.morphologyEx(edges, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))


def find_page(edges):
    contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    for contour in sorted(contours, key=cv2.contourArea, reverse=True):
        perimeter = cv2.arcLength(contour, True)
        candidate = cv2.approxPolyDP(contour, 0.02 * perimeter, True)
        large = cv2.contourArea(candidate) >= 0.10 * edges.size
        if len(candidate) == 4 and large and cv2.isContourConvex(candidate):
            return candidate.reshape(4, 2).astype(np.float32)
    return None


def order_corners(points):
    points = np.asarray(points, dtype=np.float32).reshape(4, 2)
    if not np.isfinite(points).all() or len(np.unique(points, axis=0)) != 4:
        raise ValueError("Four distinct finite corners are required.")
    center = points.mean(axis=0)
    angles = np.arctan2(points[:, 1] - center[1], points[:, 0] - center[0])
    ordered = points[np.argsort(angles)]
    if not cv2.isContourConvex(ordered) or abs(cv2.contourArea(ordered)) < 1:
        raise ValueError("Corners must form a non-degenerate convex quadrilateral.")
    start = np.argmin(ordered.sum(axis=1))
    return np.roll(ordered, -start, axis=0)


def flatten_page(image, corners):
    tl, tr, br, bl = corners
    width = max(2, round(max(np.linalg.norm(tr - tl), np.linalg.norm(br - bl))))
    height = max(2, round(max(np.linalg.norm(bl - tl), np.linalg.norm(br - tr))))
    target = np.float32([[0, 0], [width-1, 0], [width-1, height-1], [0, height-1]])
    matrix = cv2.getPerspectiveTransform(np.float32(corners), target)
    return cv2.warpPerspective(image, matrix, (width, height))


def document_mode(photo):
    gray = cv2.cvtColor(photo, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (3, 3), 0)
    normalized = cv2.normalize(blurred, None, 0, 255, cv2.NORM_MINMAX)
    return cv2.adaptiveThreshold(normalized, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                                 cv2.THRESH_BINARY, 31, 15)


def main():
    root = Path(__file__).resolve().parent
    source = Path(sys.argv[1]) if len(sys.argv) > 1 else root / "document_scene.png"
    if not source.is_file():
        raise FileNotFoundError(f"Required local image: {source}")
    image = cv2.imdecode(np.fromfile(source, dtype=np.uint8), cv2.IMREAD_COLOR)
    if image is None:
        raise ValueError(f"Cannot decode image: {source}")
    edges = edge_map(image)
    candidate = find_page(edges)
    if candidate is None:
        raise ValueError("No qualifying page candidate. Check the image and thresholds.")
    corners = order_corners(candidate)
    photo = flatten_page(image, corners)
    binary = document_mode(photo)
    output = root / "output"
    output.mkdir(exist_ok=True)
    for name, pixels in [("edges", edges), ("photo", photo), ("document", binary)]:
        if not cv2.imwrite(str(output / f"{name}.png"), pixels):
            raise OSError(f"Could not save {name}.png")
    print("Corners [TL, TR, BR, BL]:", corners.astype(int).tolist())
    print(f"Scan: {photo.shape[1]} x {photo.shape[0]} pixels")
    print("Saved edges.png, photo.png and document.png in output/")


if __name__ == "__main__":
    main()

Sample code © 2026 ARTSCI — Author of the AI/ML/CV course materials. OpenCV and NumPy retain their own licenses. OpenCV 4.5 and later use Apache License 2.0; NumPy's license is documented separately. This example uses no Ultralytics software or model weights.

Why grayscale, blur and Canny come first

edge_map converts the input to grayscale and applies a 5 × 5 Gaussian blur. Canny uses thresholds 50 and 150 in this run. Closing with a 5 × 5 kernel can connect small gaps, making a boundary easier to follow. These values are starting parameters, not settings that are guaranteed to work for all lighting and backgrounds.

The edge image includes text and background details as well as the page boundary. find_page sorts contours by area, approximates each polygon with a tolerance of 2% of its perimeter, then accepts the first convex candidate with four corners and at least 10% of the image area. Always inspect that selected boundary before interpreting the output as a document scan.

Actual selected page boundary and four ordered corner labels TL, TR, BR and BL on the synthetic input
Detected corners for this input: TL=(199,156), TR=(818,127), BR=(884,643), BL=(145,676). The yellow outline is the selected candidate.

Corner order controls the perspective mapping

The source and destination corners must correspond. Here the order is top left, top right, bottom right, bottom left. The code sorts points around their center and rotates that sequence so the point with the smallest x+y comes first. It also rejects duplicate, nonfinite and degenerate corner sets.

This ordering assumes a roughly upright page. It does not identify which way the text should face. Strongly rotated or ambiguous pages need additional orientation handling. Mixing corner correspondence can twist or flip the output even when all four points lie near a page.

cv2.getPerspectiveTransform calculates the mapping from the four input points to the four rectangle points. cv2.warpPerspective resamples the image using that mapping. See the official perspective transformation tutorial for the source-to-destination point relationship.

Read the actual output

Corners [TL, TR, BR, BL]: [[199, 156], [818, 127], [884, 643], [145, 676]]
Scan: 740 x 523 pixels
Saved edges.png, photo.png and document.png in output/

The printed 740 × 523 dimensions are a result of this image and the edge-length rule in flatten_page. They are not a known physical paper size. If the real page dimensions matter, supply that prior information and choose the target aspect ratio accordingly.

The actual 740 by 523 pixel color output of the perspective transform
Flattened color output. Perspective correction does not recover blurred characters or remove a curved page's three-dimensional shape.

Black and white is an optional view, not OCR

document_mode converts the flattened image to grayscale, blurs it, normalizes its intensity range and applies adaptive Gaussian thresholding with a 31-pixel neighborhood and C=15. A local threshold can help with brightness variation, but it can also discard fine detail or amplify noise. Compare this output with the color scan before deciding which one to keep.

Actual black-and-white adaptive threshold result from the same flattened synthetic page
A document-style image. No text has been recognized or exported as characters.

When a convincing scan can still be wrong

A large picture frame, desk mat or other convex four-corner object can satisfy exactly the same rule as a page. In a separate controlled test, the algorithm accepted a plain large rectangle that was not a document. A blank image produced no candidate. Those outcomes are useful checks, but neither establishes general document-detection accuracy.

For your own input, inspect the selected outline, check all four corners and compare the output with the original. Test shadows, low-contrast borders, clutter and rotation. If the script reports no qualifying page, investigate the input and parameters; do not silently crop an arbitrary region and call it a successful scan.

This project demonstrates classical image geometry. If you want to compare it with learned detection, see my first working YOLO object-detection example. My English course, Computer Vision: OpenCV, Machine Learning and YOLO, develops these operations through notebooks and practical applications, including the complete Document Scanner project.

Thursday, October 29, 2026

Object Detection in Python with YOLO: Boxes, Classes and Confidence

An object detector gives you more than a name. It estimates where objects are, assigns class labels and reports scores. The useful first project is to connect those predictions to a visible image, then change one setting and inspect the result.

In this example, YOLOv8n processes a synthetic 1024 × 1024 harbor image on a Windows CPU. With the same image and model, it returns 9 boxes at confidence 0.15 and 4 boxes at 0.70. Those are retained predictions, not independently verified counts of physical boats.

Watch the Python walkthrough

Watch the full tutorial on YouTube · Watch the confidence-threshold Short

What does object detection return?

Image classification answers a question about an image's category. Object detection adds estimated locations. In this run, every returned class label is boat, but each prediction has its own score and rectangle. The official detection documentation describes these outputs.

A bounding box does not trace an object's exact outline. It gives four edges around an estimated region. A confidence score helps filter predictions; it is not a certificate that a box is correct. Neither a convincing overlay nor a large number of boxes establishes model accuracy.

Nine actual YOLOv8n prediction boxes on the synthetic harbor image at confidence 0.15; labels identify prediction rows
Actual low-threshold result. Numbered labels identify returned prediction rows, not persistent object IDs. Overlapping boxes and a questionable region near the seawall are reasons to inspect the output.

Prepare two local files and a small script

Use a local yolov8n.pt checkpoint and an image named harbor_scene.jpg, placed beside detect.py. The demonstrated input and checkpoint are included in lesson 28 of the course. For your own experiment, use an image you have permission to process and update the image path in the script. Your results will depend on that image; the numbers below belong to the demonstrated input.

The checkpoint in this run matches the official Ultralytics YOLOv8n release asset, SHA-256 f59b3d833e2ff32e194b5bb8e08d211dc7c5bdf144b90d2c8412c47ccfc83b36. Downloading the model is a separate setup step; the example deliberately stops if a local file is missing. Review the model license and Ultralytics licensing information before integrating it into another project.

For Windows with Python 3.13 installed, create an isolated environment and install the demonstrated Ultralytics version. Save the following script and put the two assets beside it before running the final command:

py -3.13 -m venv .venv
.venv\Scripts\python.exe -m pip install ultralytics==8.4.24
.venv\Scripts\python.exe detect.py

The measured run used Python 3.13.9, Ultralytics 8.4.24, PyTorch 2.10.0, NumPy 2.3.5 and OpenCV 4.13.0.92. The short installation command pins Ultralytics, not every transitive dependency. It is a starting setup, not a claim that a fresh installation on every machine reproduces the complete tested environment. Installing packages or obtaining assets may need internet access; prediction can run offline once they are present. No GPU or model training is required for this example.

Run the complete Python example

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 ARTSCI - Author of the AI/ML/CV course materials.
"""Run detection on two local files; do not download missing assets."""
from pathlib import Path
from ultralytics import YOLO

ROOT = Path(__file__).resolve().parent
weights = ROOT / "yolov8n.pt"
image = ROOT / "harbor_scene.jpg"
for path in (weights, image):
    if not path.is_file():
        raise FileNotFoundError(f"Required local file: {path}")

model = YOLO(str(weights))
for confidence in (0.15, 0.70):
    result = model.predict(
        source=str(image), conf=confidence, iou=0.70,
        imgsz=640, device="cpu", max_det=300,
        classes=None, rect=True, half=False,
        augment=False, agnostic_nms=False, verbose=False,
    )[0]
    print(f"conf={confidence:.2f}: {len(result.boxes)} detections")
    for box in result.boxes:
        label = result.names[int(box.cls.item())]
        score = float(box.conf.item())
        xyxy = [round(float(v), 1) for v in box.xyxy[0].tolist()]
        print(f"  {label}: score={score:.3f}, xyxy={xyxy}")
    result.save(filename=str(ROOT / f"detections_{confidence:.2f}.jpg"))

Complete sample source above: copyright © 2026 ARTSCI — Author of the AI/ML/CV course materials. Licensed under GNU AGPL version 3, without warranty. Preserve applicable notices when sharing modified code. Ultralytics software and weights retain their own terms. This code notice does not automatically license independent images, narration or course videos.

Path(__file__).resolve().parent makes the asset paths relative to the saved script. Use it in a Python file; an interactive notebook needs paths appropriate to its working directory instead. The existence checks prevent an accidental missing-model download. YOLO(str(weights)) loads the local checkpoint, and model.predict(...)[0] selects the result for this single image.

The loop runs twice, changing only conf. It keeps IoU at 0.70, processing size at 640, the device on CPU and the other shown options fixed. For this YOLOv8 model, IoU controls overlap suppression. It is a different setting from confidence, so changing both at once would make this comparison harder to interpret. See the prediction argument documentation for their roles.

Read one prediction all the way back to the image

result.boxes contains the retained boxes. The class identifier indexes result.names to obtain a readable label. box.conf supplies the score. box.xyxy supplies left, top, right and bottom coordinates in the original input image's pixel coordinate system.

One prediction over the far-left red boat has label boat, score approximately 0.621 and coordinates [60.3, 517.6, 339.8, 852.8]. The rounded rectangle begins near x = 60 and y = 518, ending near x = 340 and y = 853. Resizing an illustration for display does not turn these into display-pixel coordinates; a custom overlay must apply the image's display transform once.

Crop of the unchanged harbor input with the actual selected prediction box over the far-left red boat, score about 0.621
A crop used to inspect one actual prediction. Its coordinates still refer to the original 1024 × 1024 input.

Here is an abbreviated excerpt from the printed output. The ellipses omit other rows; the script prints every returned row:

conf=0.15: 9 detections
  ...
  boat: score=0.621, xyxy=[60.3, 517.6, 339.8, 852.8]
  ...
conf=0.70: 4 detections

Why does a visible boat lose its box?

The selected score, about 0.621, passes 0.15 but falls below 0.70. In the second run that prediction is absent. The underlying image has not changed, and the visible boat has not disappeared. The acceptance threshold has changed.

Four actual YOLOv8n prediction boxes on the same harbor input at confidence 0.70, with the selected far-left red boat box absent
Same input, checkpoint, IoU and processing size; higher confidence threshold. Four boxes remain in this execution.

Keeping fewer predictions can remove weak duplicates or mistakes, but it can also remove useful detections. Keeping more can reveal weak candidates while introducing extra errors. We have not annotated every true object in this synthetic scene, so this is a controlled output comparison, not an accuracy benchmark. A score of 0.621 should not be read as a demonstrated 62.1% probability of correctness.

The two saved images make the result inspectable. For another experiment, retain the original input, record the model and versions, and change one parameter at a time. Test representative images and decide which missed or false detections matter for your application.

For the next step, see how detection becomes tracking and crossing events in Traffic Counter. The full notebooks, supporting files and complete applications are developed in my English course, Computer Vision: OpenCV, Machine Learning and YOLO.

Wednesday, October 28, 2026

A Knee Angle Is Not a Repetition Counter

A knee angle is a measurement. A repetition count is a decision about a sequence. Connect them without defining that decision and a plausible skeleton can give a misleading impression of what the camera observed.

This synthetic demonstration contains 241 frames at 24 frames per second. Our pose pipeline records two bottom-to-standing returns, at about 1.79 and 8.17 seconds. Yet the first frame already shows the person down. Only the second event has a complete standing–down–standing cycle visible within the recording.

Watch the pose-counter breakdown

Watch the full breakdown on YouTube · Watch the short demonstration

Actual pose-model output on the first synthetic video frame, with the person already at the bottom of the movement
The first frame, with predicted landmarks. The recording starts at the bottom, so it cannot show the descent that preceded the first return.

Follow the input all the way to an event

The input is a 1280 × 720 video, analysed offline with YOLOv8n-pose on a CPU. Pose estimation supplies predicted landmark coordinates and scores. The official Ultralytics pose documentation describes these keypoint outputs. The model does not decide what our application should call a repetition.

This example accepts exactly one detected pose per frame. For each leg, it examines the hip, knee and ankle scores, then chooses the leg with the higher minimum score. That minimum must reach 0.15, and all three coordinates must be finite. This filter selects observations; passing it does not establish landmark correctness.

The hip–knee and ankle–knee vectors meet at the knee. Their normalized dot product and arccosine produce an estimated image-space angle. It depends on the camera view and predicted points. An angle alone contains no memory of whether the person was previously standing or down.

Before counting, the implementation smooths the angles with a centered nine-frame moving average. It pads segment edges and processes each continuous valid segment separately, leaving missing observations as gaps. Because a centered filter uses future frames, this is an offline example. A live version needs a deliberate causal filter and new checks for delay and changed event timing.

Remember a bottom, then accept a return

The counter starts unarmed. A finite angle at or below 70° arms it. A later armed observation at or above 150° records the current frame and disarms it. Another standing frame adds nothing. A missing angle clears the remembered state.

The separated thresholds provide hysteresis: movement around the standing threshold cannot repeatedly count without another bottom observation. These are demonstration settings for this signal, not a definition of correct exercise technique. Here is the complete counting rule; it expects angles that have already been measured and smoothed. Represent a missing numeric angle as float("nan"), rather than passing None or a pose record:

import math


def count_returns(angles, down=70.0, up=150.0):
    if not 0 < down < up <= 180:
        raise ValueError("Require 0 < down < up <= 180.")
    armed, events = False, []
    for frame, angle in enumerate(angles):
        if not math.isfinite(angle):
            armed = False
        elif angle <= down:
            armed = True
        elif armed and angle >= up:
            events.append(frame)
            armed = False
    return events

Code adapted from the course practical source. Copyright © 2026 ARTSCI — Author of the AI/ML/CV course materials. Licensed under GNU AGPL version 3, without warranty. This code notice does not automatically license independent images, audio, video or course presentation. Ultralytics software and model terms remain applicable.

Inspect the frame that changes the count

All 241 frames in this run provide usable leg observations. At frame 42, the smoothed angle is about 148.5°. At frame 43, it is about 150.9°. The counter was armed by an earlier bottom observation, so frame 43 satisfies the upper threshold and records the first return.

Actual frame 43 with inferred hip, knee and ankle points and a smoothed knee-angle label of 150.9 degrees
Frame 43: the estimated leg points come from the recorded pose output; the displayed 150.9° is the smoothed angle used by the rule.
Accepted returnFrame indexTime
First431.79 s
Second1968.17 s

Frame indices start at zero; the times are frame index divided by 24. These are two detections under this rule for this particular clip. They are not an accuracy score against independently labelled movement.

Test the meaning, not just the final total

Run these small examples after the function. They separate state-machine behavior from pose-model quality and make the recording-boundary issue explicit:

# Standing -> bottom -> standing: one accepted return.
assert count_returns([160, 60, 100, 150, 170]) == [3]
# Starting down still counts; it does not prove a full observed cycle.
assert count_returns([60, 150, 160]) == [1]
# A shallow bend never arms the rule.
assert count_returns([160, 145, 160]) == []
# Missing data clears the remembered bottom.
assert count_returns([60, float("nan"), 150]) == []
# Both thresholds are inclusive; remaining high does not count again.
assert count_returns([70, 149.9, 150, 170]) == [2]

The second test matters most here. A clip that begins down immediately arms this rule. Its next return counts even though no preceding descent was recorded. If your requirement is a fully observed cycle, begin with an observed standing state, then require a bottom and a return. That is a different state machine; it should reject the initial partial cycle.

Thresholds also affect timing. Keeping this clip and smoothed signal fixed but raising the upper threshold to 160° moves the events to frames 48 and 200. That change does not by itself establish better accuracy. Inspect which observations satisfy the rule before interpreting a changed total or timestamp.

The same distinction appears in the traffic-counter example: track IDs and accepted crossings answer different questions. For pose data, define the event, inspect recording boundaries and missing observations, then explain each accepted transition. The full notebook, local application and broader practical workflow are developed in my English course, Computer Vision: OpenCV, Machine Learning and YOLO.

Tuesday, October 27, 2026

Count Cars with YOLO: Why Track IDs Are Not Vehicle Counts

A box follows a car, a number appears above it, and a counter increases. It looks simple. But what exactly did the application count: detections, tracking identities, or vehicles passing a particular point? Those are different measurements.

In this Python example, YOLOv8n detects vehicles, ByteTrack associates observations across frames, and a separate rule accepts downward crossings of a horizontal line. Keeping those responsibilities separate makes the result easier to explain and test.

Watch the tutorial

Watch the full tutorial on YouTube · Watch the short demonstration

Traffic counter showing tracked vehicle boxes, temporary IDs and a horizontal counting segment
The demonstration uses a synthetic traffic scene. Boxes, IDs and crossing events come from the same recorded tracking run.

Three measurements that should stay separate

A detection describes an object in one frame. A track ID associates observations over time. A crossing event records an observation sequence that satisfies our counting rule.

One car can appear in many frames. A tracker may also lose a vehicle and assign a new ID when it is detected again. Counting every detection would repeatedly count visible objects; counting distinct IDs would measure the tracker's associations. Neither automatically tells us how many physical vehicles passed our chosen location.

In this particular 241-frame, 1280 × 720 clip, the recorded run contains 23 temporary IDs. Our selected rule accepts 7 crossing events. This is a result for one clip and configuration, not an accuracy claim for general traffic footage.

Define a crossing before calculating a total

Image coordinates start at the top left: y increases downward. We place the line at line_y = 413 and restrict its horizontal span to x_range = (80, 1000). A qualifying center must move from above the line to the line or below it. Its current x must fall within that span.

The complete rule below remembers the previous y for each ID. It also remembers IDs already counted during this function call.

# Turn tracked positions into one downward pass per ID.
def count_passes(frame_tracks, line_y, x_range):
    previous_y, counted_ids, events = {}, set(), []
    for frame in frame_tracks:
        for obj in frame["objects"]:
            track_id = obj["track_id"]
            x, y = obj["center"]
            crossed = previous_y.get(track_id, y) < line_y <= y
            if crossed and x_range[0] <= x <= x_range[1] and track_id not in counted_ids:
                counted_ids.add(track_id)
                events.append({"frame": frame["frame_idx"], "id": track_id, "class": obj["class_name"]})
            previous_y[track_id] = y
    return events

Code: Copyright © 2026 ARTSCI — Author of the AI/ML/CV course materials. Licensed under the GNU AGPL version 3, without warranty. This code notice does not automatically apply to the independent images, video or course presentation.

The decisive comparison is previous_y.get(track_id, y) < line_y <= y. For a first observation, the default previous value is the current y, so the comparison is false. An object first detected below the line therefore does not produce an invented crossing.

counted_ids prevents a second accepted event for the same ID in one call. The function updates the previous y after examining every observation, even when the object is outside the horizontal span. It does not interpolate where the center crossed the line: the span check uses the current x.

Explain one event with pictures and numbers

For ID 3, the recorded center moves from y = 411.236 in frame 36 to y = 414.239 in frame 37. The rule accepts frame 37 because 411.236 < 413 <= 414.239. Its current x is approximately 474.9, inside the permitted span. ID 3 has not yet been counted.

Before, crossing and after observations for track ID 3 in frames 36, 37 and 38
The same ID before, at and after its accepted event. Coordinates are rounded for display; the rule uses the original values.

This evidence is more informative than a final total alone. You can inspect the visible vehicle, the relevant center positions and the exact decision together.

Equal totals can hide different events

Move only the line upward by 30 pixels, from y = 413 to y = 383. Reuse the same recorded tracks and horizontal span. The result is again 7 events, but the accepted IDs differ:

LineEventsAccepted IDs
y = 41374, 3, 2, 5, 9, 7, 55
y = 38373, 2, 5, 9, 7, 55, 48

ID 4 is first observed at y ≈ 398.5, already below the shifted line. Its observations support a crossing of y = 413, but not y = 383. ID 48 supplies an event only at the shifted line. Comparing event membership exposes what the matching totals hide.

Check logic separately from tracking quality

Run these checks after defining count_passes above. Their deliberately chosen center positions isolate Python logic from model quality. The three assertions test repeated downward crossings by one ID, an ID first seen below the line, and upward movement. All x positions are inside the counting segment.

def sample(y_values):
    return [{"frame_idx": i, "objects": [{"track_id": 1,
             "class_name": "car", "center": (200, y)}]}
            for i, y in enumerate(y_values)]

assert len(count_passes(sample([411, 413, 410, 416]), 413, (80, 1000))) == 1
assert len(count_passes(sample([414, 420]), 413, (80, 1000))) == 0
assert len(count_passes(sample([420, 410]), 413, (80, 1000))) == 0
print("All three counting-rule checks passed.")

The expected counts are 1, 0 and 0. After these checks pass, inspect a short video interval manually and match individual passages to event rows.

This compact rule has limits. A changed ID can allow the same vehicle to count again. Missing observations may leave a long gap between the two positions used by the comparison. There is no gap limit or hysteresis band here, and motion upward is intentionally excluded. Real deployment needs representative footage and separate checks for missed passages and false counts.

For the tracking API and tracker configuration, see the official Ultralytics tracking documentation. This example explicitly selects bytetrack.yaml; it does not rely on whichever tracker a library version uses by default.

If you want to build the full project, including the notebook, local application and practical exercises, see my English course: Computer Vision: OpenCV, Machine Learning and YOLO.

New Udemy Course: Computer Vision with OpenCV, Machine Learning and YOLO

I have published my new English-language course on Udemy: Computer Vision: OpenCV, Machine Learning and YOLO.

The course takes you from working with image arrays to building Python applications that detect, track and count objects in images and video. It combines theory with separate practical lessons, so you can see the inputs, follow the code and inspect the results.

What you will learn

  • Python, NumPy and OpenCV: image representation, color channels, transformations, filtering and edge detection.
  • Machine learning: feature extraction, classification, model comparison and evaluation with scikit-learn.
  • Deep learning and CNNs: neural network foundations, convolution, pooling, image classification and transfer learning with TensorFlow and PyTorch.
  • YOLO: object detection, custom training, object tracking, segmentation and image classification.
  • Pose estimation and deployment: body keypoints, movement features, model export, CPU performance measurements and comparisons with alternative detectors.

Four projects that bring the ideas together

  • Parking Lot Monitor: work with parking-space regions and occupancy information.
  • Traffic Counter: detect and track vehicles, then count crossings of a defined line.
  • Fitness Tracker: use pose keypoints and movement states to count squat repetitions.
  • Document Scanner: find document boundaries and correct perspective.

These are learning projects that make the connection between model outputs and application logic visible.

Video lessons, notebooks and knowledge checks

The course includes more than 10 hours of English video, downloadable notebooks and lesson resources, and 12 section quizzes with 36 questions. Practical lessons include setup and dependency instructions to help you run the examples locally.

It is aimed at developers, students and engineers who want to move from computer vision concepts to working Python examples. Some programming experience is helpful; the opening sections review Python and NumPy foundations.

Launch offer: US$12.99

Coupon code: CBEE0F25D4870B5F9123

Open the course with the US$12.99 coupon

The coupon is valid until 27 November 2026 at 06:20 UTC (26 November, 22:20 PST). Check that the coupon is applied and review the final price shown by Udemy before purchasing.

You can also use my standard Udemy course link to view the course and its curriculum. This separate link does not include the coupon code.

I would be glad to hear which computer vision applications you would like to build.

Monday, July 27, 2020

Inteligentne miasto (Smart City - Polish)

Inteligentne miasto 

Wstęp

Chciałbym dzisiaj zaproponować przygotowanie projektu architektonicznego opisującego zbudowanie systemów dla  inteligentnego miasta. W celu wykonania transformacji cyfrowej miasta należy podejść do problemu całościowo tzn. najpierw należy wykonać inwentaryzację posiadanych zasobów i oferowanych usług biznesowych. Następnie należy zebrać wywiad z użytkownikami obecnych systemów oraz z  interesariuszami, a w tym z odbiorcami końcowymi usług. Tak pozyskane dane wraz z wykonaną inwentaryzacją pozwolą zdiagnozować problemy dotyczące obsługi sygnalizacji (brak zielonej fali), obsługa miejsc parkingowych (brak możliwości rezerwacji miejsc parkingowych) czy komunikacja miejsca (nieoptymalne połączenia - mieszkańcy tracą dużo czasu na dojazd do pracy). Dobra diagnoza pozwoli zaproponować architekturę TO-BE w podziale na domeny:

  • biznesową,
  • aplikacyjną,
  • danych,
  • techniczną (infrastrukturalną)

Dodatkowo należy zdefiniować wymagania niefunkcjonalne oraz pryncypia architektoniczne, które będą drogowskazem dla wszystkich dalszych działań.

Pryncypia architektoniczne

Pryncypia architektoniczne dla naszego miasta wraz z modelem motywacji zostały umieszczone na poniższym diagramie.

 

    

 

Stan obecny i architektura AS-IS

W celu przygotowania architektury AS-IS należy przeprowadzić inwentaryzację w podziale na domeny:

    •  architektura biznesowa (procesy biznesowe, usługi biznesowe, aktorzy, itp.)
    • architektura aplikacyjna (stworzone lub zakupione systemy i aplikacje, licencje, opisy integracji między komponentami, interfejsy)
    • architektura danych (należy zdefiniować encje danych, właścicieli danych biznesowych, zdefiniować dane referencyjne oraz przepływy danych oraz sposoby ich wykorzystania)
    • architektura techniczna (serwery fizyczne i wirtualne, systemy operacyjne, bazy danych, licencje, komponenty sieciowe fizyczne i wirtualne, systemy do backupu danych, ułożenie komponentów w podsieciach, pozostałe komponenty takie jak: firewall, WAF, IAM, HSM, SIEM, Gateway, itp.)

Z uwagi na możliwość wystąpienia drobnej przestępczości należy zweryfikować możliwość ataków na systemy. Należy wykonać testy penetracyjne i wykonać stosowne zabezpieczenia jeśli będą potrzebne.

Po pozyskaniu wiedzy dotyczącej architektury AS-IS należy przeprowadzić badania satysfakcji w obszarach:

1.       Użytkownicy wewnętrzni systemu w podziale na departamenty:

    • Zebranie informacji czy procesy biznesowe działają bez problemów
    • Czy są wąskie gardła w procesach
    • Jak działa i czy jest dostępna automatyzacja procesów
    • Propozycje usprawnień

2.       Interesariusze i odbiorcy biznesowi (grupa, która chce jak najszybciej dojechać (sterowanie ruchem) i móc zaparkować pojazd):

    • Zebranie opinii o jakości działania usługi
    • Pozyskanie informacji o problemach poruszania się i parkowania
    • Propozycje i uwagi dotyczące usprawnień (lista życzeń)

3.       Użytkownicy końcowi (użytkownicy indywidualni potrzebujący dojechać w określone miejsce)

    • Krótkie uwagi dotyczące usługi (co jest dobre, a co nie),
    • możliwości usprawnień

Architektura TO-BE

Warstwa biznesowa

Rozpoczynając od warstwy architektury biznesowej należy zdefiniować podstawowe procesy biznesowe oraz powiązane usługi biznesowe. Najważniejsze są oczywiście zewnętrzne usługi i procesy, które możemy podzielić na usługi dla biznesu i usługi dla odbiorcy indywidualnego. Same usługi zostały zdefiniowane na wysokim poziomie ogólności.

 


Część elementów powinna zostać rozbudowana i opisana dokładniej:

  • rozliczenia za korzystanie z usługi powinny być naliczane za faktyczne wykorzystanie usługi 
  •  powinna być weryfikacja, czy miejsce jest zajęte przez samochód, dla którego była wykonana rezerwacja. Miejsca powinny być monitorowane, a obraz powinien być przetwarzany i powinien być rejestrowany numer rejestracyjny
  • podstawowa usługa przejazdu i parkowania powinna być spójna i intuicyjna do przejścia

 

Procesu i usługi dedykowane dla klienta wewnętrznego zostały zwizualizowane poniżej


 

Warstwa aplikacyjna

Wymagania dotyczące warstwy aplikacyjnej i funkcjonalności jakie muszą zostać dostarczone wynikają bezpośrednio lub pośrednio z wyższej warstwy – warstwy biznesowej. Główne założenia dla warstwy aplikacyjnej:

1.       Wszystkie czujniki, komponenty kontrolujące światła drogowe i dostępność miejsc parkingowych są podłączone do jednego systemu zarządzającego tymi komponentami – mówimy tutaj o IoT

2.       Użytkownik końcowy ma dostęp do swojego konta i funkcjonalności poprzez portal WWW

3.       Funkcjonalności podstawowe pracują w oparciu o konteneryzację i orchiestrację np. k8s.

4.       Zostaną stworzone minimalnie 2 podsieci:

a.       Dla komponentów udostępniających usługi WWW

b.       Dla wewnętrznych komponentów, które nie udostępniają usług zewnętrznych

5.       Zdefiniowane procesy będą działały w oparciu o zdefiniowane workflow

6.       Procesy wyszukania najlepszej trasy, zarezerwowania miejsca parkingowego w okolicy, czy weryfikacji czy faktycznie oczekiwany samochód zaparkował na miejscu wyznaczanym będą odbywały się z wykorzystaniem ML (rozpoznawanie obrazów, wyznaczenie najlepszej ścieżki lub rezerwacja miejsca o określonej porze w okolicy)

7.       Wszystkie działania będą zbierane i agregowane w BigData – będziemy mogli weryfikować , czy klient zastanawiał się nad wyborem opcji doręczenia, ceną itp. Na podstawie tych danych będziemy mogli skierować do takich użytkowników pakiety promocyjne w przyszłości.  

8.       Tworzone API musi być udokumentowane. W tym celu należy wykorzystać OpenAPI.

9.       Wytwarzany kod aplikacji musi przechodzić automatyczną weryfikację np. Sonar oraz ręczną rewizję kodu

10.   Tworzone oprogramowanie powinno być zgodne z podejściem DevOps

11.   Tworzone oprogramowanie bazuje na wzorcach GOF, przy wykorzystaniu języka java należy bazować dodatkowo na wzorcach J2EE. Integracja między aplikacjami muszą korzystać z dobrych praktyk  integracyjnych wzorców architektonicznych

 

Założenia dla warstwy aplikacyjnej zostały rozpisane powyżej. Z uwagi, że mamy zbudować Smart Citi oraz możemy korzystać z najnowszych dostępnych rozwiązań to nasza architektura na najwyższym poziomie powinna wyglądać następująco (HLD):

 


Istniejące systemy do zarządzania majątkiem, obsługą zgłoszeń i roszczeń oraz systemami geolokalizacyjnymi powinny zostać dostosowane. Powinny zostać zbudowane nowe systemy w oparciu o chmurę AWS, które będą zapewniały HA, bezpieczeństwo dla podłączonych urządzeń IoT (Anazon IoT) oraz analizę danych w BigData (Athena), czy wykorzystanie ML dla lepszego zrozumienia działań klienta w portalu (Amazon Personalize) oraz wyszukanie najlepszej drogi lub wybranie najdogodniejszego miejsca parkingowego w punkcie docelowym podróży (Amazon SageMaker). Proces rezerwacji, płatności oraz wyboru parkingu będzie opierał się o kroki w procesie (Amazon StepFunction). Wszystkie potwierdzenia, zmiany itp. będą notyfikowane do klienta (SNS). Dla potrzeb całości zostaną wykorzystane kolejki SQS oraz komponenty, które będą zasilały systemy typu BigData np. Kinesis.

 

Portal Użytkownika zostanie zbudowany w oparciu konteneryzację, którą będzie zarządzał kubernetes (k8s). Całość zostanie uruchomiona w jednym regionie. VPC (Virtual Private Cloud) zostanie uruchomiona w dwóch strefach (nienależnych serwerowniach w ramach regionu).   Aplikacja zostanie podzielona na frontend i backend. Reguły zostaną tak zdefiniowane, że do warstwy backendowej nie będzie dostępu z zewnątrz. Jedynie będą mogły się tam dostać w komponenty warstwy frontendowej. Na maszynach EC2 zostaną postawione Master i Worker Nody K8s.

 

 

 

 

Integracje z urządzeniami peryferyjnymi wykonane będą za pomocą komponentu Amazon IoT. Do tego komponentu będą podłączone ścieżki:

1.       BigData -> na potrzeby procesowania wyników zestawień wraz z podłączonym narzędziem BI

2.       Workflow -> na potrzeby:

a.        rejestracji przejazdu i sterowania ruchem (wykorzystanie ML), weryfikacji zaparkowanego pojazdu (wykorzystanie ML) na potrzeby przeciwdziałania drobnej przestępczości,

b.       Informowania portalu klienta o dostępności i zwolnieniu miejsca parkingowego. Na tej podstawie będą wykonywane naliczenia


 

Bezpieczeństwo

Główne wymagania dla potrzeb zapewnienie bezpieczeństwa całego rozwiązania:

1.       Dla aplikacji jest wymagany cykliczny audyt bezpieczeństwa

2.       Przed uruchomieniem produkcyjnym wymagane jest uruchomienie testów penetracyjnych

3.       Wszystkie komponenty IoT komunikują się z systemem centralnym z wykorzystaniem przydzielonego certyfikatu X.509

4.       Portal WWW wymaga uwierzytelnienia i autoryzacji. Zabezpieczony jest protokołem TLS w wersji minimalnie 1.2

5.       Wewnętrza komunikacja między komponentami odbywa się z wykorzystaniem certyfikatów (Zero Trust Networking (ZTN))

6.       Wymagane jest użycie Firewalla i WAF

7.       Dostęp do podsieci jest zdefiniowany i określony. Otwarte są jedynie konieczne porty do obsługi funkcjonalności

8.       Wszystkie żądania API muszą być uwierzytelnione, a wewnętrzne wywołania muszą być zgodne ze zdefiniowanymi politykami bezpieczeństwa

9.       Zabezpieczenie API musi być wykonane za pośrednictwem OAuth2

10.   Uwierzytelnienie z systemów zewnętrznych (google, facebook, LinkedIn) musi się odbywać za pośrednictwem SAML 2.0

 

Wymagania niefunkcjonalne

Wymagania niefunkcjonalne są bardzo istotną częścią aplikacji. Wymagania te mogą decydować o kosztach całej aplikacji. Poniżej podstawowe wymagania:

1.       Aplikacje muszą być tak stworzone, aby dawały się skalować horyzontalnie

2.       Aplikacje muszą zapewniać HA więc muszą być rozmieszczone przynajmniej w 2 serwerowniach oddalonych od siebie. Musi być zapewnione zapasowe łącze

3.       Dostęp do danych wrażliwych oraz do systemów musi być audytowany

4.       Musi być zapewniony backup danych

5.       Dokumentacja projektu musi się zgadzać z wytworzonym oprogramowaniem

6.       Dokumentacja API powinna bazować na standardach OpenAPI (wraz ze swaggerem)

 

Analiza luki

Z uwagi, że mamy architekturą AS-IS i TO-BE musimy stworzyć proces dojścia ze stanu obecnego do stanu docelowego. W celu realizacji tego zadania konieczne będzie rozpisanie zadań koniecznych do wykonania z odpowiednio ustawionymi priorytetami. Należy uwzględnić takie elementy jak:

1.       stworzenie fasad do przykrycia wcześniejszych funkcjonalności – jeśli były już integracje z zewnętrznymi dostawcami

2.       przeprowadzenie migracji danych w koniecznych obszarach

3.       przepisanie lub zastąpienie komponentów oznaczonych do usunięcia

4.       stworzenie nowych komponentów i ich priorytetyzacja

 

 

Uwagi końcowe

Sprawny specjalista zauważy, że nie wszystkie domeny zostały pokryte. Brakuje domeny danych i można się pokusić o lepsze rozpisanie domeny technicznej. To jednak zostawiam dla specjalistów lubiących się pobawić :-) Powodzenia.