Metadata-Version: 2.4
Name: cvgo
Version: 1.0.2
Summary: Simple Computer Vision for Python
Author: Ajang Rahmat
License-Expression: MIT
Project-URL: Homepage, https://kelasrobot.com
Project-URL: Source, https://github.com/ajangrahmat/cvgo
Project-URL: Issues, https://github.com/ajangrahmat/cvgo/issues
Keywords: computer-vision,opencv,mediapipe,arduino,telegram,education
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Education
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: <3.13,>=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy==1.26.4
Requires-Dist: opencv-contrib-python==4.11.0.86
Requires-Dist: mediapipe==0.10.9; sys_platform == "linux" and platform_machine == "aarch64"
Requires-Dist: mediapipe==0.10.9; sys_platform == "linux" and platform_machine == "arm64"
Requires-Dist: mediapipe==0.10.21; sys_platform != "linux"
Requires-Dist: mediapipe==0.10.21; sys_platform == "linux" and platform_machine != "aarch64" and platform_machine != "arm64"
Requires-Dist: pyserial>=3.5
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: twine>=5; extra == "dev"
Provides-Extra: mqtt
Requires-Dist: paho-mqtt>=2.0; extra == "mqtt"
Provides-Extra: websocket
Requires-Dist: websocket-client>=1.8; extra == "websocket"
Provides-Extra: robotics
Requires-Dist: paho-mqtt>=2.0; extra == "robotics"
Requires-Dist: websocket-client>=1.8; extra == "robotics"
Provides-Extra: recognition
Requires-Dist: deepface==0.0.100; extra == "recognition"
Requires-Dist: tensorflow==2.16.2; extra == "recognition"
Requires-Dist: tf-keras==2.16.0; extra == "recognition"
Requires-Dist: opencv-python==4.11.0.86; extra == "recognition"
Requires-Dist: protobuf<4,>=3.20.3; (sys_platform == "linux" and platform_machine == "aarch64") and extra == "recognition"
Requires-Dist: protobuf<4,>=3.20.3; (sys_platform == "linux" and platform_machine == "arm64") and extra == "recognition"
Requires-Dist: protobuf<5,>=4.25.3; sys_platform != "linux" and extra == "recognition"
Requires-Dist: protobuf<5,>=4.25.3; (sys_platform == "linux" and platform_machine != "aarch64" and platform_machine != "arm64") and extra == "recognition"
Dynamic: license-file

<p align="center">
    <img src="logo-cvgo.png" alt="CVGO" width="520">
</p>

<h1 align="center">CVGO</h1>

<p align="center"><strong>Simple Computer Vision for Python</strong></p>

<p align="center">
    <a href="https://kelasrobot.com">Homepage</a> ·
    <a href="examples/">Examples</a> ·
    <a href="FACE_RECOGNITION.md">Face Recognition</a> ·
    <a href="ARMBIAN.md">Armbian Guide</a> ·
    <a href="CHANGELOG.md">Changelog</a> ·
    <a href="LICENSE">MIT License</a>
</p>

CVGO simplifies the repetitive parts of OpenCV and MediaPipe while keeping the
main program flow visible. Users still write `while True`, read frames, inspect
detection results, make decisions, and choose a desktop GUI, terminal output,
or a browser monitor with an optional setup form for headless systems.

> Simple by default, customizable when needed.

## Installation

Install CVGO from PyPI:

```bash
pip install cvgo
```

CVGO 1.0 uses a tested dependency profile for each platform:

| Platform | Python | MediaPipe |
|---|---|---|
| Windows, macOS, Linux x86-64 | 3.9–3.12 | `0.10.21` |
| Linux AArch64 / Armbian | 3.9–3.11 | `0.10.9` |

Both profiles use CVGO `1.0.1`, OpenCV Contrib `4.11.0.86`, NumPy `1.26.4`,
and PySerial `3.5` or newer. CVGO selects the correct MediaPipe version from
the operating system and CPU architecture recorded by Python package metadata.

The OpenCV package used is `opencv-contrib-python` to avoid installing two
separate `cv2` variants alongside MediaPipe dependencies.

Python 3.11 is recommended. Python 3.9 remains available for legacy systems,
while Python 3.8 and Python 3.13 are not supported by this release.

Check the installed stack, and optionally discover a working camera:

```bash
python -m cvgo check
python -m cvgo check --camera auto
python -m cvgo check --recognition
```

It is recommended to install CVGO in a dedicated virtual environment:

```powershell
py -3.11 -m venv .venv-cvgo
.venv-cvgo\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install cvgo
```

In a new virtual environment, `pip` installs dependencies once. If the
installation command is run again in the same environment, matching packages are
reported as `Requirement already satisfied`.

In a core-only environment, do not install `opencv-python`,
`opencv-python-headless`, or other OpenCV variants alongside
`opencv-contrib-python`. They all provide the same `cv2` module. The
recognition extra is the documented exception and pins both distributions to
one matching OpenCV release because DeepFace requires `opencv-python` in its
package metadata.

### Armbian AArch64

The following stack has been tested on an AArch64 STB using Python 3.11.9 and
MediaPipe 0.10.9. Install the operating-system libraries once, then install
CVGO inside the virtual environment:

```bash
sudo apt update
sudo apt install -y python3-venv libgl1 libglib2.0-0 v4l-utils

python3.11 -m venv .venv-cvgo
source .venv-cvgo/bin/activate
python -m ensurepip --upgrade
python -m pip install --upgrade pip setuptools wheel
python -m pip install "cvgo==1.0.1"

python -m cvgo check --camera auto
```

When `uv` is already installed, the package installation line can be replaced
with the faster equivalent below. The explicit MediaPipe version also works
when testing a local CVGO wheel before it is published:

```bash
uv pip install "mediapipe==0.10.9" "cvgo==1.0.1"
```

The diagnostic reports the first camera index that can return a frame. Use that
number in the program, for example `go.Camera(1)`. `v4l-utils` is only needed
for deeper camera troubleshooting. PySerial is already a core dependency, and
`pygame` is not required by CVGO's built-in `Alarm`, so neither needs a separate
installation. Do not use `sudo` to edit or run project Python files.

### Optional Face Recognition

Face recognition uses DeepFace only when `FaceRecognizer` is called. Keep this
large optional stack out of a normal CVGO environment unless it is needed.
Create a fresh environment and use CVGO's constrained extra instead of running
an unconstrained `pip install deepface` in an existing MediaPipe environment:

```powershell
py -3.11 -m venv .venv-cvgo-face
.venv-cvgo-face\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install "cvgo[recognition]==1.0.1"
python -m pip check
```

For a local source checkout, replace the install command with:

```bash
python -m pip install -e ".[recognition]"
```

The extra constrains DeepFace `0.0.100`, TensorFlow `2.16.2`, tf-keras `2.16.0`,
OpenCV `4.11.0.86`, and NumPy `1.26.4`. Protobuf is selected per profile:
`>=4.25.3,<5` with MediaPipe 0.10.21, or `>=3.20.3,<4` with MediaPipe 0.10.9.
These bounds prevent a current unconstrained DeepFace installation from
replacing CVGO's NumPy and MediaPipe-compatible protobuf versions. DeepFace
declares `opencv-python` while MediaPipe uses `opencv-contrib-python`, so the
extra pins both distributions to the same OpenCV release inside this dedicated
environment.

CVGO also corrects the pinned DeepFace MediaPipe adapter so OpenCV BGR input is
converted to RGB for face detection without changing the color of the crop sent
to the embedding model. This keeps training and recognition consistent on both
MediaPipe 0.10.9 and 0.10.21.

The TensorFlow/DeepFace stack is intended for desktop x86-64 testing first.
Availability and performance on an AArch64 Armbian image still depend on wheels
provided for that exact Python and operating-system combination.

See [`FACE_RECOGNITION.md`](FACE_RECOGNITION.md) for dataset capture, model
training, deployment, privacy, and troubleshooting.

`ObjectDetector` and `GestureRecognizer` use official MediaPipe models that are
downloaded automatically on first use. The computer only needs internet access
for the first download; afterward, the model is reused from cache.

## Import

```python
import cvgo as go
```

All components can also be imported directly:

```python
from cvgo import Camera, ObjectDetector, Telegram, WebViewer, train_faces
```

## CVGO 1.0 Features

| Feature | Main API | Output |
|---|---|---|
| Camera and GUI | `Camera` | OpenCV frames |
| Headless web monitor | `WebViewer` | Browser video with annotated frames and live status |
| Face detection | `FaceDetector` | Fast face boxes with confidence |
| Face landmarks | `FaceLandmarks` | Face landmarks and metrics |
| Face recognition | `FaceRecognizer`, `train_faces` | Optional DeepFace matches and reusable embedding models |
| Hand tracking | `HandTracker` | 21 landmarks per hand |
| Pose tracking | `PoseTracker` | 33 landmarks and a person box |
| Holistic tracking | `HolisticTracker` | Face, pose, and both hands |
| Gesture | `GestureRecognizer` | Gesture, score, landmarks, and task modes |
| Object detection | `ObjectDetector` | Labels, boxes, and image/video/live modes |
| Human segmentation | `SelfieSegmenter` | Human mask and background |
| Driver monitor | Modular components | Drowsiness, head direction, and alarms |
| Arduino | `Serial` | Serial communication, sync or queued |
| Telegram | `Telegram` | Text and photos, sync or non-blocking |
| Robotics connectivity | `MqttClient`, `WebSocketClient` | Optional JSON messaging |
| Shared geometry | `BoundingBox` | Coordinates, center, area, and drawing |

This is the stable CVGO 1.0 API for camera, learning, and robotics projects.
MediaPipe features outside this scope can still be added in later versions without
changing the main API pattern.

## Python Naming Style

CVGO follows common Python naming conventions (PEP 8):

- classes use `PascalCase`: `HandTracker`, `PoseTracker`;
- functions and methods use `snake_case`: `read_fps()`, `put_text()`;
- constants use `UPPER_CASE`: `BIT_DROWSY`, `LEFT_WRIST`.

So use `read_fps()`, not `readFPS()`. Since the object is already named `FPS`,
the shortest form is `fps.read()`. `fps.read_fps()` remains available as a more
explicit alias.

## Simple Defaults

```python
camera = go.Camera()
faces = go.FaceDetector()
landmarks = go.FaceLandmarks()
hands = go.HandTracker()
pose = go.PoseTracker()
objects = go.ObjectDetector()
arduino = go.Serial()
viewer = go.WebViewer()
timer = go.Timer()
```

Main defaults:

| Component | Default |
|---|---|
| `Camera()` | Camera `0` |
| `FaceDetector()` | Fast engine, maximum one face |
| `FaceLandmarks()` | Maximum one face |
| `HandTracker()` | Maximum two hands |
| `PoseTracker()` | One main pose |
| `HolisticTracker()` | One main person, 543 landmarks |
| `GestureRecognizer()` | Video mode, maximum two hands |
| `ObjectDetector()` | Video mode, 10 objects, confidence `0.5` |
| `SelfieSegmenter()` | Landscape model for webcams |
| `Serial()` | Automatic port, `9600` baud |
| `WebViewer()` | All interfaces, port `8000`, JPEG at up to 15 FPS |
| `Telegram()` | Environment-based configuration, 30-second cooldown |
| `Timer()` | One-second duration |
| `Smoother()` | Alpha `0.45` |

Only set parameters when you want to change the default behavior:

```python
camera = go.Camera(1, width=1280, height=720)
arduino = go.Serial("COM5", baud=115200)
timer = go.Timer(1.5)
hands = go.HandTracker(max_hands=1, detection_confidence=0.7)
pose = go.PoseTracker(model_complexity=0)
objects = go.ObjectDetector(confidence=0.7, allow=["person"])
telegram = go.Telegram(cooldown=60)
```

## Advanced Parameter Guide

The complete examples intentionally start with simple defaults. For advanced
projects, add only the parameters that need to change; the remaining parameters
keep their defaults.

```python
camera = go.Camera()  # All defaults
camera = go.Camera(4)  # Change only the camera source
camera = go.Camera(4, width=1280, height=720, fps=30)
```

Parameters written after `*` in the signatures below are keyword-only. Write
their names explicitly, such as `width=1280`, so the code remains readable.

Common rules:

| Value | Meaning |
|---|---|
| Confidence | A value from `0.0` to `1.0`; higher is stricter |
| OpenCV color | BGR order, for example green is `(0, 255, 0)` |
| `None` | Let CVGO, OpenCV, or the operating system use its default |
| `static=True` | Optimize for unrelated still images instead of sequential frames |
| `mode="live"` | Keep a camera loop responsive using asynchronous task results |

### Camera and Drawing Parameters

`go.Camera(source=0, *, width=None, height=None, fps=None, backend=None)`

| Parameter | Default | Description |
|---|---|---|
| `source` | `0` | Camera index, video path, or stream URL |
| `width` | `None` | Requested capture width in pixels |
| `height` | `None` | Requested capture height in pixels |
| `fps` | `None` | Requested camera frame rate |
| `backend` | `None` | OpenCV backend; `None` uses `cv2.CAP_ANY` |

Camera resolution and FPS are requests. The camera driver can select the nearest
supported value. After the camera opens, inspect `camera.size` or use the raw
`camera.capture` object for other OpenCV properties.

`camera.show(frame, *, title="CVGO", delay=1, quit_key="q")`

| Parameter | Default | Description |
|---|---|---|
| `title` | `"CVGO"` | Window title |
| `delay` | `1` | `cv2.waitKey` delay in milliseconds |
| `quit_key` | `"q"` | One keyboard character that closes the loop |

`camera.close(windows=True)` releases capture. Set `windows=False` in a purely
headless process when no OpenCV GUI windows should be destroyed.

`go.put_text(frame, text, position=(20, 35), *, color=(0, 255, 0), scale=0.7, thickness=2, background=False)`

| Parameter | Default | Description |
|---|---|---|
| `position` | `(20, 35)` | Text origin in pixels |
| `color` | `(0, 255, 0)` | Text color in BGR |
| `scale` | `0.7` | OpenCV font scale |
| `thickness` | `2` | Text stroke width |
| `background` | `False` | Draw a black background behind the text |

All shared boxes also accept `color=(0, 255, 0)`, `thickness=2`, and
`label=None` in `box.draw()`. Face, hand, and pose boxes provide the default
labels `"Face"`, `"Hand"`, and `"Person"`.

```python
camera = go.Camera(1, width=1280, height=720, fps=30)

go.put_text(
    frame,
    "Security active",
    color=(0, 255, 255),
    background=True,
)
camera.show(frame, title="Security Camera", quit_key="x")
```

### Headless Web Monitor Parameters

`go.WebViewer(title="CVGO Web Monitor", *, host="0.0.0.0", port=8000, jpeg_quality=80, stream_fps=15.0, start=True, log_requests=False)`

| Parameter | Default | Description |
|---|---|---|
| `title` | `"CVGO Web Monitor"` | Page title shown in the browser |
| `host` | `"0.0.0.0"` | Listen on all network interfaces; use `"127.0.0.1"` for local-only access |
| `port` | `8000` | HTTP port; use `0` to let the operating system choose a free port |
| `jpeg_quality` | `80` | JPEG quality from `1` to `100` |
| `stream_fps` | `15.0` | Maximum browser stream frame rate |
| `start` | `True` | Start the background web server during construction |
| `log_requests` | `False` | Print HTTP request logs in the terminal |

Publish an already annotated OpenCV frame with `viewer.update(frame,
status={...})`. The call does not wait for a browser or JPEG encoder. Properties
`viewer.url`, `viewer.port`, `viewer.running`, `viewer.viewers`, and
`viewer.frame_number` expose monitor state; `viewer.close()` stops only the web
monitor and never closes the camera.

`WebViewer` uses Python's built-in HTTP server and `cv2.imencode()`. It adds no
Flask, FastAPI, Uvicorn, or desktop-display dependency. The server and JPEG
encoder use daemon threads, and encoding pauses when no browser stream is
connected. It is read-only by default. An optional small form can send setup
values into a queue consumed by the main CLI loop. It is intended for a trusted
local network, not a public internet application server.

```python
viewer = go.WebViewer(
    "CVGO Security",
    host="0.0.0.0",
    port=8000,
    jpeg_quality=75,
    stream_fps=10,
)

viewer.update(
    frame,
    status={"Security": "SAFE", "People": 0},
)
```

Optional browser form:

```python
viewer.set_form(
    {
        "person_name": {
            "label": "Person name",
            "value": "Ajang",
            "required": True,
            "max_length": 60,
        },
        "photo_count": {
            "label": "Number of photos",
            "type": "number",
            "value": 5,
            "min": 3,
            "max": 30,
            "step": 1,
            "required": True,
        },
    },
    submit_label="Start face training",
)

settings = viewer.wait_form()
print(settings["person_name"], settings["photo_count"])
```

`set_form(fields, *, submit_label="Kirim", description="")` supports `text`
and `number` fields. Text options are `label`, `value`, `required`,
`placeholder`, `min_length`, and `max_length`; number fields also support
`min`, `max`, and `step`. `poll_form()` is non-blocking, `wait_form(timeout)`
waits for one submission, and `clear_form()` returns the page to monitor-only
mode. Submitted number fields are returned as `int` or `float`. The HTTP thread
only validates and queues data; it never runs camera or training callbacks.
Closing the browser therefore cannot stop the CLI process.

### Face Parameters

`go.FaceDetector(*, max_faces=1, padding=10, model=0, detection_confidence=0.5, engine="auto", refine=False, tracking_confidence=0.5)`

`go.FaceLandmarks(*, max_faces=1, refine=False, detection_confidence=0.5, tracking_confidence=0.5)`

| Parameter | Default | Description |
|---|---|---|
| `max_faces` | `1` | Maximum faces returned per frame |
| `padding` | `10` | Extra pixels around a `FaceDetector` box |
| `model` | `0` | Fast model: `0` for near-range or `1` for full-range faces |
| `engine` | `"auto"` | Select `auto`, `fast`, or Face Mesh-compatible `mesh` |
| `refine` | `False` | Refine eye and lip landmarks and add iris landmarks |
| `detection_confidence` | `0.5` | Minimum confidence for the initial face detection |
| `tracking_confidence` | `0.5` | Minimum confidence for landmark tracking |

The default `auto` mode uses MediaPipe Face Detection, which is lighter than
running every Face Mesh landmark just to calculate a box. It automatically keeps
the legacy mesh engine when `refine=True` or a custom `tracking_confidence` is
used. Choose `engine="mesh"` explicitly when that behavior is required.
`detector.raw_result` remains available in both modes; `detector.faces` contains
landmark faces only in mesh mode.

| Result method | Parameter | Default | Description |
|---|---|---|---|
| `face.box()` | `padding` | `10` | Extra pixels around landmark bounds |
| `face.draw()` | `style` | `"contours"` | `contours`, `tesselation`, `iris`, or `all` |
| `face.draw()` | `color` | `(0, 255, 0)` | Landmark and connection color |
| `face.draw()` | `thickness` | `1` | Connection thickness |
| `face.draw()` | `radius` | `1` | Landmark radius |
| `FaceBox.draw()` | `color` | `(0, 255, 0)` | Box and label color |
| `FaceBox.draw()` | `thickness` | `2` | Box and label thickness |
| `FaceBox.draw()` | `label` | `"Face"` | Box label; use `None` to hide it |

Every fast `FaceBox` also exposes `confidence`. To tune only box detection:

```python
detector = go.FaceDetector(
    max_faces=2,
    detection_confidence=0.5,
    model=0,
)
```

```python
landmarker = go.FaceLandmarks(
    max_faces=2,
    refine=True,
    detection_confidence=0.5,
)

faces = landmarker.detect(frame)

for face in faces:
    face.draw(frame, style="tesselation", color=(255, 180, 0))
    face.box(padding=20).draw(frame, label="Tracked face")
```

### Face Recognition Parameters

`go.FaceRecognizer(db_path, *, model_name="Facenet512", detector_backend="mediapipe", distance_metric="cosine", threshold=None, max_faces=1, mode="video", stream=None, align=True, expand_percentage=10, anti_spoofing=False, refresh_database=True)`

| Parameter | Default | Description |
|---|---|---|
| `db_path` | Required | Folder containing reference face images |
| `model_name` | `"Facenet512"` | DeepFace recognition model; `SFace` is a lighter alternative |
| `detector_backend` | `"mediapipe"` | Corrected RGB MediaPipe detector used before recognition |
| `distance_metric` | `"cosine"` | `cosine`, `euclidean`, `euclidean_l2`, or `angular` |
| `threshold` | `None` | Use the model's tuned threshold, or supply a stricter custom value |
| `max_faces` | `1` | Maximum detected faces recognized per processed frame |
| `mode` | `"video"` | Synchronous `image`/`video`, or background `live` mode |
| `stream` | `None` | Legacy Boolean mode alias; new code should use `mode` |
| `align` | `True` | Align a detected face before creating its embedding |
| `expand_percentage` | `10` | Add a percentage of context around each detected face |
| `anti_spoofing` | `False` | Ask DeepFace to reject a detected presentation attack |
| `refresh_database` | `True` | Scan reference images on first recognition |

In `mode="live"`, `recognize(frame)` returns the latest completed list without
blocking the camera loop. Use `result_ready` for the first completed result,
`busy` while a frame is being processed, `last_error` for a background error,
and `refresh()` after adding or removing reference images.

Each `FaceMatch` exposes `name`, `identity`, `box`, `confidence`, `distance`,
`threshold`, `recognized`/`known`, `draw()`, and `to_dict()`. An unknown face is
still returned with its box and `name == "Unknown"`.

CVGO defaults DeepFace's process-wide MediaPipe confidence to `0.5`. For a dark
camera, set `MEDIAPIPE_MIN_DETECTION_CONFIDENCE` before `import cvgo` and before
the first training/recognition call. The examples' foreground `FaceDetector`
can be tuned independently with `detection_confidence`.

Organize multiple photos per person in subfolders:

```text
faces/
├── Ajang/
│   ├── 01.jpg
│   └── 02.jpg
└── Budi/
    └── 01.jpg
```

```python
recognizer = go.FaceRecognizer(
    "faces",
    mode="live",
    model_name="Facenet512",
    detector_backend="mediapipe",
    max_faces=2,
)
detector = go.FaceDetector(engine="fast", detection_confidence=0.5)

faces = detector.detect(frame)
matches = recognizer.recognize(frame) if faces else []

for index, face in enumerate(faces):
    label = matches[index].name if index < len(matches) else "RECOGNIZING"
    face.draw(frame, label=label)
```

The first run downloads the selected model and builds a local representation
cache. Treat reference photos and embeddings as biometric data: obtain consent,
restrict access, and do not commit them to a public repository.

### Face Model Training Parameters

`go.train_faces(dataset_path, model_path=None, *, model_name="Facenet512", detector_backend="mediapipe", distance_metric="cosine", threshold=None, align=True, expand_percentage=10, min_images=1, strict=False, progress=None)`

| Parameter | Default | Description |
|---|---|---|
| `dataset_path` | Required | Folder containing one subfolder per identity |
| `model_path` | `None` | Output `.pkl`; defaults to the dataset name with a `.pkl` suffix |
| `model_name` | `"Facenet512"` | Embedding model saved into the model metadata |
| `detector_backend` | `"mediapipe"` | Corrected RGB MediaPipe detector used on every training image |
| `distance_metric` | `"cosine"` | Distance calculation stored for inference |
| `threshold` | `None` | Model-specific default, or a positive custom threshold |
| `align` | `True` | Align the training face before embedding |
| `expand_percentage` | `10` | Add context around the training crop |
| `min_images` | `1` | Minimum valid images needed to retain an identity |
| `strict` | `False` | Skip bad images; `True` stops on the first error |
| `progress` | `None` | Optional `(done, total, image_path)` callback |

```python
result = go.train_faces(
    "faces",
    "face_model.pkl",
    detector_backend="mediapipe",
    min_images=3,
)

print(result.identity_count, result.images, result.skipped)

recognizer = go.FaceRecognizer.from_model(
    "face_model.pkl",
    mode="live",
)
```

`FaceTrainingResult` exposes the model path, retained identities, valid image
count, skipped-image messages, embedding size, threshold, model configuration,
and `to_dict()`. Use `go.face_model_info("face_model.pkl")` to inspect safe
metadata without returning embedding vectors. A model-backed recognizer can call
`refresh()` after an atomic model replacement. See
[`FACE_RECOGNITION.md`](FACE_RECOGNITION.md) for the complete workflow.

### Hand Parameters

`go.HandTracker(*, max_hands=2, model_complexity=1, detection_confidence=0.5, tracking_confidence=0.5, static=False, mirrored=False)`

| Parameter | Default | Description |
|---|---|---|
| `max_hands` | `2` | Maximum hands returned per frame |
| `model_complexity` | `1` | `0` is lighter; `1` is more accurate |
| `detection_confidence` | `0.5` | Minimum confidence for hand detection |
| `tracking_confidence` | `0.5` | Minimum confidence for landmark tracking |
| `static` | `False` | Set `True` for independent still images |
| `mirrored` | `False` | Set `True` when the input frame was already flipped horizontally |

| Result method | Parameter | Default | Description |
|---|---|---|---|
| `hand.box()` | `padding` | `10` | Extra pixels around the hand |
| `hand.draw()` | `color` | `(0, 255, 0)` | Connection color |
| `hand.draw()` | `point_color` | `(255, 0, 255)` | Landmark color |
| `hand.draw()` | `thickness` | `2` | Connection thickness |
| `hand.draw()` | `radius` | `2` | Landmark radius |
| `HandBox.draw()` | `label` | `"Hand"` | Box label; use `None` to hide it |

```python
tracker = go.HandTracker(
    max_hands=1,
    model_complexity=0,
    detection_confidence=0.7,
)

hands = tracker.detect(frame)

for hand in hands:
    hand.draw(frame, color=(255, 200, 0), point_color=(0, 0, 255))
```

### Pose Parameters

`go.PoseTracker(*, model_complexity=1, detection_confidence=0.5, tracking_confidence=0.5, smooth=True, segmentation=False, static=False)`

| Parameter | Default | Description |
|---|---|---|
| `model_complexity` | `1` | `0` Lite, `1` Full, or `2` Heavy |
| `detection_confidence` | `0.5` | Minimum confidence for pose detection |
| `tracking_confidence` | `0.5` | Minimum confidence for landmark tracking |
| `smooth` | `True` | Smooth landmarks and an optional segmentation mask |
| `segmentation` | `False` | Also produce `pose.mask` |
| `static` | `False` | Set `True` for independent still images |

| Result method | Parameter | Default | Description |
|---|---|---|---|
| `pose.visible()` | `confidence` | `0.5` | Required landmark visibility |
| `pose.box()` | `padding` | `20` | Extra pixels around the visible body |
| `pose.box()` | `min_visibility` | `0.5` | Ignore weaker landmarks when building the box |
| `pose.draw()` | `color` | `(0, 255, 0)` | Skeleton connection color |
| `pose.draw()` | `point_color` | `(255, 0, 255)` | Landmark color |
| `pose.draw()` | `thickness` | `2` | Connection thickness |
| `pose.draw()` | `radius` | `2` | Landmark radius |
| `PoseBox.draw()` | `label` | `"Person"` | Box label; use `None` to hide it |

Use `model_complexity=0` for a lighter model on an STB or low-power board.

```python
tracker = go.PoseTracker(
    model_complexity=0,
    detection_confidence=0.6,
    segmentation=True,
)

pose = tracker.detect(frame)

if pose:
    pose.box(padding=30, min_visibility=0.6).draw(
        frame,
        label="Person",
    )
```

### Holistic Parameters

`go.HolisticTracker(*, model_complexity=1, detection_confidence=0.5, tracking_confidence=0.5, smooth=True, refine_face=False, segmentation=False, static=False)`

| Parameter | Default | Description |
|---|---|---|
| `model_complexity` | `1` | Pose model complexity: `0`, `1`, or `2` |
| `detection_confidence` | `0.5` | Minimum initial detection confidence |
| `tracking_confidence` | `0.5` | Minimum landmark tracking confidence |
| `smooth` | `True` | Smooth landmarks and an optional mask |
| `refine_face` | `False` | Refine face landmarks around the eyes and lips |
| `segmentation` | `False` | Also produce `result.mask` |
| `static` | `False` | Set `True` for independent still images |

`result.draw(frame, face=True, pose=True, hands=True)` lets each group be shown
or hidden independently. Each Boolean parameter defaults to `True`.

```python
tracker = go.HolisticTracker(model_complexity=0, refine_face=True)
result = tracker.detect(frame)

if result:
    result.draw(frame, face=False, pose=True, hands=True)
```

### Object Detection Parameters

`go.ObjectDetector(model_path=None, *, confidence=0.5, max_objects=10, allow=None, deny=None, locale="en", mode="video", stream=None, download=True)`

| Parameter | Default | Description |
|---|---|---|
| `model_path` | `None` | Path to a compatible custom `.tflite` model |
| `confidence` | `0.5` | Minimum object score |
| `max_objects` | `10` | Maximum results returned per frame |
| `allow` | `None` | Return only these labels, for example `["person"]` |
| `deny` | `None` | Exclude these labels |
| `locale` | `"en"` | Preferred display-name locale in model metadata |
| `mode` | `"video"` | `image`, `video`, or asynchronous `live` |
| `stream` | `None` | Legacy mode option; new code should use `mode` |
| `download` | `True` | Download the default model when it is not cached |

`allow` and `deny` cannot be used together. In `live` mode, `detect()` returns
the latest completed result and `detector.result_ready` reports whether the first
result has completed.

| Result method | Parameter | Default | Description |
|---|---|---|---|
| `item.draw()` | `color` | `(0, 255, 0)` | Box and label color |
| `item.draw()` | `thickness` | `2` | Box and label thickness |
| `item.draw()` | `show_score` | `True` | Include confidence in the label |

```python
detector = go.ObjectDetector(
    confidence=0.65,
    max_objects=3,
    allow=["person", "car"],
)
```

### Gesture Parameters

`go.GestureRecognizer(model_path=None, *, max_hands=2, gesture_confidence=0.5, detection_confidence=0.5, presence_confidence=0.5, tracking_confidence=0.5, mirrored=False, mode="video", stream=None, download=True)`

| Parameter | Default | Description |
|---|---|---|
| `model_path` | `None` | Path to a compatible custom `.task` model |
| `max_hands` | `2` | Maximum hands recognized per frame |
| `gesture_confidence` | `0.5` | Minimum score before a gesture is considered recognized |
| `detection_confidence` | `0.5` | Minimum hand detection confidence |
| `presence_confidence` | `0.5` | Minimum hand presence confidence |
| `tracking_confidence` | `0.5` | Minimum landmark tracking confidence |
| `mirrored` | `False` | Set `True` when the input frame was already flipped |
| `mode` | `"video"` | `image`, `video`, or asynchronous `live` |
| `stream` | `None` | Legacy mode option; new code should use `mode` |
| `download` | `True` | Download the default model when it is not cached |

| Result method | Parameter | Default | Description |
|---|---|---|---|
| `gesture.box()` | `padding` | `10` | Extra pixels around the gesture hand |
| `gesture.draw()` | `color` | `(0, 255, 0)` | Hand connections and box color |
| `gesture.draw()` | `point_color` | `(255, 0, 255)` | Hand landmark color |

```python
recognizer = go.GestureRecognizer(
    max_hands=1,
    gesture_confidence=0.7,
)
```

The task models can be prepared before an offline deployment:

```python
model = go.download_model(
    "gesture_recognizer",
    directory="models",
    timeout=180,
)
recognizer = go.GestureRecognizer(model)
```

| `download_model()` parameter | Default | Description |
|---|---|---|
| `name` | Required | `"object_detection"` or `"gesture_recognizer"` |
| `directory` | `None` | Custom download folder |
| `force` | `False` | Download again even when a valid model exists |
| `timeout` | `120.0` | Download timeout in seconds |

Set `CVGO_MODEL_DIR` to change the shared default model cache directory.
CVGO verifies the header and SHA-256 checksum of both pinned official models.
A damaged or incomplete cache file is downloaded again before it is used.

### Segmentation and Timing Parameters

| API | Parameter | Default | Description |
|---|---|---|---|
| `SelfieSegmenter()` | `model` | `1` | `0` general model; `1` landscape/webcam model |
| `result.foreground()` | `threshold` | `0.5` | Minimum mask value treated as foreground |
| `result.apply()` | `background` | `(0, 0, 0)` | BGR color or image with the same frame size |
| `result.apply()` | `threshold` | `0.5` | Foreground cutoff |
| `result.blur()` | `amount` | `35` | Gaussian blur kernel; even values are raised to the next odd value |
| `result.blur()` | `threshold` | `0.5` | Foreground cutoff |
| `Timer()` | `seconds` | `1.0` | Time a condition must stay true |
| `Smoother()` | `alpha` | `0.45` | EMA weight; lower is smoother, higher reacts faster |
| `FPS()` | `update_every` | `1.0` | Seconds between displayed FPS updates |

```python
segmenter = go.SelfieSegmenter(model=0)
timer = go.Timer(1.5)
smoother = go.Smoother(alpha=0.3)
fps = go.FPS(update_every=0.5)

result = segmenter.segment(frame)
frame = result.blur(frame, amount=51, threshold=0.6)
```

### Serial, Telegram, and Alarm Parameters

`go.Serial(port=None, *, baud=9600, timeout=1.0, reconnect_after=5.0, settle_time=2.0, newline=False, connect=True)`

| Parameter | Default | Description |
|---|---|---|
| `port` | `None` | Auto-detect a serial port; or use `COM5`, `/dev/ttyUSB0`, and so on |
| `baud` | `9600` | Serial baud rate; it must match the board |
| `timeout` | `1.0` | Serial read timeout in seconds |
| `reconnect_after` | `5.0` | Minimum delay between reconnect attempts |
| `settle_time` | `2.0` | Wait after a board resets on connect; use `0` when unnecessary |
| `newline` | `False` | Append `\n` to outgoing values |
| `connect` | `True` | Connect during construction |

Use `send(value)` when the result is needed immediately. In a camera loop,
`send_async(value)` queues the write on one background worker and returns a
`Future`. A successful synchronous send flushes the serial output before it
returns, so immediately calling `close()` does not leave buffered bytes behind.
Call `close()` during cleanup; `close(wait=False)` cancels queued work.

`go.Telegram(token=None, chat_id=None, *, cooldown=30.0, timeout=15.0, silent=False, protect=False)`

| Parameter | Default | Description |
|---|---|---|
| `token` | Environment | Bot token or `CVGO_TELEGRAM_TOKEN` |
| `chat_id` | Environment | Target ID or `CVGO_TELEGRAM_CHAT_ID` |
| `cooldown` | `30.0` | Minimum seconds between messages with the same `key` |
| `timeout` | `15.0` | HTTP request timeout in seconds |
| `silent` | `False` | Send without a notification sound |
| `protect` | `False` | Ask Telegram to protect message content |

`telegram.send_message(text, *, key="message", force=False, silent=None, protect=None, parse_mode=None)`

| Parameter | Default | Description |
|---|---|---|
| `text` | Required | Message text, from 1 to 4096 characters |
| `key` | `"message"` | Independent cooldown name |
| `force` | `False` | Bypass cooldown intentionally |
| `silent` | `None` | Use the constructor setting, or override it |
| `protect` | `None` | Use the constructor setting, or override it |
| `parse_mode` | `None` | Telegram formatting mode such as `"HTML"` |

`telegram.send_photo(photo, caption="", *, key="photo", force=False, filename=None, quality=85, silent=None, protect=None, parse_mode=None)`

| Parameter | Default | Description |
|---|---|---|
| `photo` | Required | OpenCV frame, image bytes, or image path |
| `caption` | `""` | Caption up to 1024 characters |
| `key` | `"photo"` | Independent cooldown name |
| `force` | `False` | Bypass cooldown intentionally |
| `filename` | `None` | Optional uploaded filename |
| `quality` | `85` | JPEG quality from 1 to 100 for OpenCV frames |
| `silent` | `None` | Use the constructor setting, or override it |
| `protect` | `None` | Use the constructor setting, or override it |
| `parse_mode` | `None` | Telegram caption formatting mode |

`send_message_async()` and `send_photo_async()` accept the same parameters and
return a `Future[bool]`. They share one ordered background queue so a slow
network does not freeze frame capture. OpenCV frames are copied before being
queued. Call `telegram.close()` after the loop; its `wait=True` default finishes
queued sends first.

`go.Alarm(*, frequency=1500, duration=180, repeat=3, cooldown=0.8)`

| Parameter | Default | Description |
|---|---|---|
| `frequency` | `1500` | Beep frequency in Hz on Windows |
| `duration` | `180` | Length of each beep in milliseconds |
| `repeat` | `3` | Number of beeps per trigger |
| `cooldown` | `0.8` | Minimum seconds between alarm starts |

```python
arduino = go.Serial("/dev/ttyUSB0", baud=115200, newline=True)
telegram = go.Telegram(cooldown=60, silent=True)
alarm = go.Alarm(frequency=1800, repeat=2, cooldown=1.0)

arduino.send_async("1")
telegram.send_message_async("CVGO active")
```

### Advanced Driver Monitor Parameters

`DriverMonitor` groups its thresholds into small configuration objects. This
keeps the constructor readable and lets each part be calibrated separately.

| `EyeConfig` parameter | Default | Description |
|---|---|---|
| `closed_threshold` | `0.20` | Enter the closed-eye state below this EAR |
| `open_threshold` | `0.24` | Leave the closed-eye state above this EAR |
| `alert_after` | `1.5` | Closed-eye seconds before drowsiness is active |
| `smoothing` | `0.45` | EAR smoother alpha |

| `HeadConfig` parameter | Default | Description |
|---|---|---|
| `yaw_normal` | `0.50` | Calibrated straight-ahead yaw ratio |
| `turn_threshold` | `0.12` | Enter looking-away state beyond this offset |
| `turn_release` | `0.07` | Leave looking-away state below this offset |
| `turn_alert_after` | `0.7` | Looking-away seconds before an alert |
| `pitch_normal` | `0.50` | Calibrated upright pitch ratio |
| `down_threshold` | `0.055` | Enter head-down state beyond this offset |
| `down_release` | `0.030` | Leave head-down state below this offset |
| `down_alert_after` | `0.7` | Head-down seconds before an alert |

| Other parameter | Default | Description |
|---|---|---|
| `FaceConfig.missing_alert_after` | `2.0` | Missing-face seconds before an alert |
| `DriverMonitor.camera` | `0` | Camera index, path, URL, or a configured `Camera` object |
| `DriverMonitor.serial` | `False` | `True` for auto serial, or pass a `Serial` object |
| `DriverMonitor.sound` | `False` | Enable its built-in alarm |
| `serial_repeat_after` | `0.5` | Public attribute controlling repeated mask transmissions |

```python
eyes = go.EyeConfig(
    closed_threshold=0.22,
    open_threshold=0.26,
    alert_after=1.2,
)
head = go.HeadConfig(turn_threshold=0.10, down_threshold=0.05)
face = go.FaceConfig(missing_alert_after=3.0)

monitor = go.DriverMonitor(
    camera=go.Camera(4, width=640, height=480),
    serial=go.Serial("/dev/ttyUSB0", baud=115200),
    sound=True,
    eyes=eyes,
    head=head,
    face=face,
)
monitor.serial_repeat_after = 1.0
```

Available events for `monitor.on(...)` are `drowsy`, `looking_away`,
`looking_left`, `looking_right`, `head_down`, `face_missing`, and `normal`.
`MonitorResult` keeps the measured `ear`, `yaw`, `pitch`, durations, `fps`,
landmarks, alert booleans, numeric `mask`, and Arduino-ready `mask_hex` available
for fully custom logic.

| Method parameter | Default | Description |
|---|---|---|
| `show.title` | `"CVGO Driver Monitor"` | GUI window title |
| `show.draw_landmarks` | `True` | Draw face landmarks before showing |
| `show.landmark_style` | `"contours"` | Face drawing style |
| `show.quit_key` | `"q"` | GUI quit key |
| `run.show` | `False` | Enable a GUI window in shortcut mode |
| `run.draw_landmarks` | `False` | Draw landmarks in shortcut mode |
| `run.print_status` | `True` | Print status twice per second |
| `run.quit_key` | `"q"` | GUI quit key when `show=True` |

### Raw Results for Further Customization

CVGO does not lock advanced users into its helpers:

| Access | Raw object |
|---|---|
| `camera.capture` | OpenCV `VideoCapture` |
| `tracker.raw_result` | Latest raw MediaPipe result |
| `face.raw`, `hand.raw`, `pose.raw` | Raw MediaPipe landmarks |
| `item.raw`, `gesture.raw` | Raw MediaPipe Tasks result/category |
| `face.points`, `hand.points`, `pose.points` | Readable CVGO landmark points |

## Consistent Bounding Boxes

`FaceBox`, `HandBox`, `PoseBox`, and `ObjectBox` share the
`BoundingBox` API:

```python
box = pose.box()

print(box.xyxy)
print(box.center)
print(box.area)

box.draw(
    frame,
    label="Person",
)
```

Face and hand landmark results use the same pattern:

```python
face.box().draw(frame)
hand.box().draw(frame)
```

The original landmark points remain available, so padding, visibility, colors,
labels, and the surrounding project logic are still customizable.

## Camera

By default, CVGO leaves OpenCV to choose the best camera backend (`CAP_ANY`). A
custom backend can still be passed using the `backend` parameter.

```python
import cvgo as go


camera = go.Camera()

while True:
    frame = camera.read()

    if frame is None:
        break

    if not camera.show(frame):
        break

camera.close()
```

Press `q` to quit.

## Face Detection

```python
import cvgo as go


camera = go.Camera()
detector = go.FaceDetector()

while True:
    frame = camera.read()

    if frame is None:
        break

    faces = detector.detect(frame)

    for face in faces:
        face.draw(frame)

    if not camera.show(frame):
        break

camera.close()
detector.close()
```

## Face Landmarks and Metrics

```python
import cvgo as go


camera = go.Camera()
landmarker = go.FaceLandmarks()

while True:
    frame = camera.read()

    if frame is None:
        break

    faces = landmarker.detect(frame)

    if faces:
        face = faces[0]

        ear = go.eye_ratio(face)
        yaw = go.yaw_ratio(face)
        pitch = go.pitch_ratio(face)

        go.put_text(frame, f"EAR: {ear:.3f}")
        face.draw(frame)

    if not camera.show(frame):
        break

camera.close()
landmarker.close()
```

## Hand Tracking

```python
import cvgo as go


camera = go.Camera()
tracker = go.HandTracker()

while True:
    frame = camera.read()

    if frame is None:
        break

    hands = tracker.detect(frame)

    for hand in hands:
        hand.draw(frame)

    if not camera.show(frame):
        break

camera.close()
tracker.close()
```

Each `Hand` has 21 points, `handedness`, `confidence`, `box()`, and raw MediaPipe
results via `raw`. Named landmarks make customization clearer:

```python
tip = hand.point(go.HandLandmark.INDEX_FINGER_TIP)
x, y = tip.pixel(frame)
```

OpenCV gives webcam frames without mirroring, so `HandTracker` automatically
adjusts left/right labels by default. If the frame is already flipped horizontally
before detection, use `go.HandTracker(mirrored=True)`.

## Pose Tracking

```python
import cvgo as go


camera = go.Camera()
tracker = go.PoseTracker()

while True:
    frame = camera.read()

    if frame is None:
        break

    pose = tracker.detect(frame)

    if pose:
        pose.draw(frame)
        pose.box().draw(
            frame,
            label="Person",
        )

    if not camera.show(frame):
        break

camera.close()
tracker.close()
```

`PoseTracker` produces 33 body landmarks, a visibility-based
`pose.confidence`, a person box, and world coordinates through
`pose.world_points`. Example access to a point:

```python
shoulder = pose.point(go.PoseLandmark.LEFT_SHOULDER)
```

For lightweight person-presence detection without drawing a skeleton:

```python
tracker = go.PoseTracker(
    model_complexity=0,
)

pose = tracker.detect(frame)

if pose:
    pose.box(
        padding=30,
    ).draw(
        frame,
        label="Person",
    )
```

`pose is not None` means the model found a sufficiently visible body pose. This can
be used as a signal for the presence of one main person, but it is not a general
person detector or a multi-person counter. For distant CCTV, multi-person, or
crowd counting scenarios, use the object detection model.

## FPS

```python
fps = go.FPS()

while True:
    frame = camera.read()
    value = fps.read()
```

The value is updated every second by default. The interval can be changed with
`go.FPS(update_every=0.5)`.

## MediaPipe Task Modes

`ObjectDetector` and `GestureRecognizer` support three running modes:

| Mode | Behavior | Recommended for |
|---|---|---|
| `image` | Synchronous, each image is independent | Photos and unrelated images |
| `video` | Synchronous, sequential frames with timestamps | Video files and simple loops |
| `live` | Asynchronous, returns the latest completed result | Cameras, GUI, and low-power boards |

`video` remains the default, so existing code still waits for and receives the
result of the current frame. For a responsive camera loop, select live mode:

```python
detector = go.ObjectDetector()
```

In live mode, `detect(frame)` submits a frame and immediately returns the latest
completed result. The first calls can return an empty list while the first result
is still being processed. Check `detector.result_ready` when that distinction
matters. MediaPipe can skip incoming frames while its model is busy to keep
latency low.

`fps.read()` measures the camera loop in a live example, not the number of model
results produced per second. The most recent result can be reused across several
display frames.

The old `stream` argument remains compatible: `stream=True` selects `video` and
`stream=False` selects `image`. New code should use `mode` because its intent is
clearer.

## Object Detection

```python
import cvgo as go


camera = go.Camera()
detector = go.ObjectDetector()

while True:
    frame = camera.read()

    if frame is None:
        break

    objects = detector.detect(frame)

    for item in objects:
        item.draw(frame)

    if not camera.show(frame):
        break

camera.close()
detector.close()
```

Each `DetectedObject` has `label`, `score`, `box`, `is_person`, and raw results via
`raw`. For person detection specifically:

```python
detector = go.ObjectDetector(allow=["person"])
```

The official MediaPipe EfficientDet-Lite0 model is downloaded automatically once
on first use. After that, the model is read from cache. Custom models can still be
used:

```python
detector = go.ObjectDetector("models/custom_model.tflite")
```

The model can also be prepared in advance:

```python
go.download_model("object_detection")
```

To process separate images instead of sequential video frames, use
`go.ObjectDetector(mode="image")`.

## Gesture Recognition

```python
import cvgo as go


camera = go.Camera()
recognizer = go.GestureRecognizer()

while True:
    frame = camera.read()

    if frame is None:
        break

    gestures = recognizer.detect(frame)

    for gesture in gestures:
        gesture.draw(frame)

    if not camera.show(frame):
        break

camera.close()
recognizer.close()
```

The default model recognizes `Closed_Fist`, `Open_Palm`, `Pointing_Up`,
`Thumb_Down`, `Thumb_Up`, `Victory`, and `ILoveYou`. Each result exposes
`gesture.hand`, `gesture.handedness`, `gesture.points`, `gesture.box()`,
`gesture.score`, and the raw result for custom logic.

Use `go.GestureRecognizer(mode="image")` for static images that are not part of
a continuous video stream.

## Holistic Tracking

```python
import cvgo as go


camera = go.Camera()
tracker = go.HolisticTracker()

while True:
    frame = camera.read()

    if frame is None:
        break

    result = tracker.detect(frame)

    if result:
        result.draw(frame)

    if not camera.show(frame):
        break

camera.close()
tracker.close()
```

The result parts can still be accessed directly through `result.face`,
`result.pose`, `result.left_hand`, and `result.right_hand`.

## Selfie Segmentation

```python
import cvgo as go


camera = go.Camera()
segmenter = go.SelfieSegmenter()

while True:
    frame = camera.read()

    if frame is None:
        break

    result = segmenter.segment(frame)
    frame = result.blur(frame)

    if not camera.show(frame):
        break

camera.close()
segmenter.close()
```

Replace the background with a color or image:

```python
frame = result.apply(frame, background=(40, 40, 40))
frame = result.apply(frame, background=background_image)
```

## Condition Timer

`Timer` becomes active when a condition remains true for the configured duration.

```python
eye_timer = go.Timer(1.5)

eyes_closed = ear < 0.20
drowsy = eye_timer.check(eyes_closed)
```

When `eyes_closed` becomes false again, the timer resets automatically.

## Easy-to-Understand Drowsiness Detection

```python
import cvgo as go


EAR_THRESHOLD = 0.20

camera = go.Camera()
landmarker = go.FaceLandmarks()
eye_timer = go.Timer(1.5)
ear_smoother = go.Smoother()
alarm = go.Alarm()

while True:
    frame = camera.read()

    if frame is None:
        break

    faces = landmarker.detect(frame)
    drowsy = False

    if faces:
        face = faces[0]
        ear = ear_smoother.update(go.eye_ratio(face))
        drowsy = eye_timer.check(ear < EAR_THRESHOLD)

        status = "DROWSY" if drowsy else "NORMAL"
        color = (0, 0, 255) if drowsy else (0, 255, 0)

        go.put_text(frame, f"Status: {status}", color=color)
        go.put_text(frame, f"EAR: {ear:.3f}", (20, 70))
        face.draw(frame, color=color)
    else:
        eye_timer.reset()
        ear_smoother.reset()

    alarm.trigger(drowsy)

    if not camera.show(frame):
        break

camera.close()
landmarker.close()
```

The important values remain visible: EAR, threshold, smoothing, timer, status,
alarm, and GUI.

## Serial Arduino

```python
import time
import cvgo as go


arduino = go.Serial(
    settle_time=4.0,
)

try:
    if not arduino.connected:
        print("Arduino: NOT CONNECTED")
    else:
        print(f"Arduino: CONNECTED | Port: {arduino.port}")
        sent = arduino.send("1")
        status = "SENT" if sent else "FAILED"
        print(f"{status}: 1")

        if sent:
            # Give the sketch time to process a one-shot command before close.
            time.sleep(0.5)
finally:
    arduino.close()
```

Opening the port resets many Arduino-compatible boards. The longer
`settle_time` in this one-shot example lets `setup()` finish before sending. If
the sketch reads a complete line, also set `newline=True`. The short delay after
`send()` gives the sketch time to act before a one-shot program closes the port.
In a real GUI, create `Serial` once when the application starts and close it only
when the window exits. Camera-loop projects should use `send_async()` when they
cannot wait for a serial write.

## Face Recognition

Install the optional recognition extra in a fresh virtual environment. Then
choose either direct-folder recognition or a reusable trained model.

Direct-folder recognition:

```bash
python examples/21_face_recognition.py
```

Capture labeled photos and build `face_model.pkl`:

```bash
python examples/22_face_training.py
```

Load that model for recognition:

```bash
python examples/23_face_model.py
```

Each topic also has `examples/cli/` and `examples/cli_web/` variants. The
training step creates an embedding index; it does not fine-tune a neural
network. All recognition examples use `mode="live"`, so inference does not
block the camera loop. A lightweight `FaceDetector` first confirms the current
face and draws its box; DeepFace identifies it in the background. The box stays
visible with `RECOGNIZING` while the worker is busy. Known identities use green
boxes and unknown faces use red boxes.

The CLI Web variant displays a browser form for the person name, photo count,
and camera index. After the form is submitted, capture and model training stay
inside the terminal process and continue if the browser is closed. Run example
22 once per identity, or prepare `faces/Name/*.jpg` manually and call:

```python
result = go.train_faces(
    "faces",
    "face_model.pkl",
    detector_backend="mediapipe",
    min_images=3,
)
print(result.to_dict())
```

The `.pkl` stores embeddings and configuration, not original image bytes. It is
still biometric data and must remain private. Load only a model generated and
controlled by your own project. See
[`FACE_RECOGNITION.md`](FACE_RECOGNITION.md) for the full dataset, API,
deployment, safety, and troubleshooting guide, and the official
[DeepFace project](https://github.com/serengil/deepface) for available models
and detector backends.

## Telegram

Create a bot through `@BotFather`, then store the token and chat ID in environment
variables. Do not write the real token in source code or the repository.

Windows PowerShell:

```powershell
$env:CVGO_TELEGRAM_TOKEN="TOKEN_FROM_BOTFATHER"
$env:CVGO_TELEGRAM_CHAT_ID="CHAT_ID"
```

Linux/macOS:

```bash
export CVGO_TELEGRAM_TOKEN="TOKEN_FROM_BOTFATHER"
export CVGO_TELEGRAM_CHAT_ID="CHAT_ID"
```

If the chat ID is unknown, send `/start` to the bot and then run:

```python
import cvgo as go


telegram = go.Telegram(token="TOKEN_DARI_BOTFATHER")
print(telegram.find_chat_id())
```

Send text and an OpenCV frame:

```python
telegram = go.Telegram()

telegram.send_message("CVGO aktif")
telegram.send_photo_async(
    frame,
    "Warning: person detected.",
    key="security",
)
telegram.close()
```

`send_photo()` and `send_photo_async()` accept an OpenCV frame, bytes, or an
image file path. The default 30-second cooldown prevents one condition from
sending too many photos. Use a different `key` for each warning type or change
the cooldown with `go.Telegram(cooldown=60)`. The send status is `True`/`False`;
failure details are available through `telegram.last_error`.

In a drowsiness detection project, usage remains simple:

```python
if drowsy:
    telegram.send_photo_async(
        frame,
        "Warning: driver is drowsy.",
        key="drowsy",
    )
```

The complete person detection and photo sending example is available in
`examples/17_telegram_security.py`.

## Driver Monitor Capstone Project

The complete capstone project example is available in:

```text
examples/08_driver_monitor.py
```

The project combines:

- EAR and smoothing;
- head-turn and head-down detection;
- a timer for each condition;
- missing-face detection;
- an Arduino serial bitmask;
- optional Telegram warning photos with `go.Telegram()`;
- a sound alarm;
- status and sensor values in the GUI;
- an FPS counter.

The algorithm is built from small CVGO components rather than one monolithic
function.

## Optional Shortcut

`DriverMonitor` remains available for users who need a quick prototype:

```python
from cvgo import DriverMonitor
```

The main learning example does not use it so that the algorithm remains visible.

## Robotics Connectivity

MQTT and WebSocket clients are optional integrations for robots, devices, and
real-time dashboards. Install them with:

```bash
pip install "cvgo[robotics]"
```

`go.MqttClient(host="localhost", port=1883, *, client_id="", username=None, password=None, keepalive=60, reconnect_after=5.0, connect_timeout=5.0, tls=False, connect=False)`

| MQTT parameter | Default | Description |
|---|---|---|
| `host` | `"localhost"` | Broker hostname or IP address |
| `port` | `1883` | Broker TCP port; commonly `8883` with TLS |
| `client_id` | `""` | Stable MQTT client identifier |
| `username`, `password` | `None` | Optional broker credentials |
| `keepalive` | `60` | Keepalive interval in seconds |
| `reconnect_after` | `5.0` | Minimum delay between reconnect attempts |
| `connect_timeout` | `5.0` | Maximum wait for the broker's `CONNACK` |
| `tls` | `False` | Enable the default TLS configuration |
| `connect` | `False` | Connect during construction |

`connect()` returns `True` only after the broker accepts the session. Existing
subscriptions are stored and registered again after reconnect. `last_error`
contains a timeout, authentication, subscribe, or publish error when an
operation fails.

Publish structured MQTT messages as JSON:

```python
import cvgo as go


with go.MqttClient(host="localhost", client_id="cvgo-camera") as mqtt:
    mqtt.publish("robot/camera/state", {"person_detected": True})
```

Send and receive JSON over a WebSocket:

```python
import cvgo as go


with go.WebSocketClient("ws://localhost:8080") as websocket:
    websocket.send({"type": "person_detected", "confidence": 0.94})
    message = websocket.receive()
```

`go.WebSocketClient(url, *, timeout=10.0, reconnect_after=5.0, connect=False)`

| WebSocket parameter | Default | Description |
|---|---|---|
| `url` | Required | `ws://` or `wss://` server URL |
| `timeout` | `10.0` | Connect, send, and receive timeout in seconds |
| `reconnect_after` | `5.0` | Minimum delay between reconnect attempts |
| `connect` | `False` | Connect during construction |

Both clients support `connect`, `reconnect`, `close`, context managers, and
asynchronous sending with `publish_async()` or `send_async()`. Connections are
not opened automatically unless `connect=True` is passed. A failed WebSocket is
closed before CVGO creates a replacement connection.

## GUI, CLI, and CLI Web Examples

Every topic has three complete programs:

| Mode | Folder | Display behavior |
|---|---|---|
| GUI | `examples/` | Shows an OpenCV desktop window |
| CLI | `examples/cli/` | Prints live results without opening a window |
| CLI Web | `examples/cli_web/` | Keeps the loop in the terminal and publishes annotated frames to a browser |

CLI and CLI Web examples do not require `DISPLAY`. CLI Web also does not open a
browser automatically. The camera, detection, alarm, Serial, Telegram, MQTT,
and WebSocket logic remains in the main Python loop. Closing the browser only
disconnects that viewer; it does not stop or pause the CLI process.

For example, run drowsiness detection with:

```bash
python examples/cli/07_drowsiness.py
```

Press `Ctrl+C` to stop. Run `python -m cvgo check --camera auto` to find a
working camera. If the result reports camera `1`, change the example to:

```python
camera = go.Camera(1)
```

See [`examples/cli/README.md`](examples/cli/README.md) for all 23 CLI examples.

To monitor face detection from a phone or computer while CVGO runs on an STB:

```bash
python examples/cli_web/02_face_detection.py
hostname -I
```

Open `http://IP-STB:8000` from a device on the same network. Detection boxes,
labels, FPS, and status are included in the browser view. Press `Ctrl+C` in the
original terminal to stop the Python program.

```python
import cvgo as go


camera = go.Camera(1)
detector = go.FaceDetector()
viewer = go.WebViewer("CVGO Face Detection")

print(f"Web monitor: http://IP-STB:{viewer.port}")

try:
    while True:
        frame = camera.read()

        if frame is None:
            break

        faces = detector.detect(frame)

        for face in faces:
            face.draw(frame)

        viewer.update(
            frame,
            status={"Face": "DETECTED" if faces else "NOT DETECTED"},
        )
except KeyboardInterrupt:
    pass
finally:
    viewer.close()
    camera.close(windows=False)
    detector.close()
```

See [`examples/cli_web/README.md`](examples/cli_web/README.md) for all 23
headless browser-monitor examples and network notes.

## Installation Diagnostics

`python -m cvgo check` reports the CVGO, Python, NumPy, OpenCV, MediaPipe, and
PySerial versions without starting a vision model. This makes it easier to
compare a Windows development machine with an AArch64 Armbian device.

```bash
python -m cvgo check
python -m cvgo check --camera auto
python -m cvgo check --camera auto --max-camera-index 12
python -m cvgo check --camera 1 --json
python -m cvgo check --recognition
```

The command exits with status `0` when the platform-specific stack is correct
and the optional camera test succeeds. Auto mode scans camera indexes `0` through
`9` by default. Camera drivers, display support, serial permissions, and
Telegram network access still need to be tested on the target STB.

CVGO itself builds as a universal `py3-none-any` wheel, including on AArch64.
On Linux AArch64, CVGO selects MediaPipe 0.10.9 because it provides a CPython
3.11 ARM64 wheel. Desktop systems select MediaPipe 0.10.21. Do not install an
x86-64 wheel on an AArch64 device.

The same checks are available to custom tools as `go.system_info()` and
`go.check_camera(source=0, backend=None, max_index=9)`. Pass `source="auto"`
to find the first readable camera.

## Daftar contoh

```text
examples/
├── 01_camera.py
├── 02_face_detection.py
├── 03_face_landmarks.py
├── 04_face_metrics.py
├── 05_serial_arduino.py
├── 06_face_to_arduino.py
├── 07_drowsiness.py
├── 08_driver_monitor.py
├── 09_hand_tracking.py
├── 10_pose_tracking.py
├── 11_security_pose.py
├── 12_object_detection.py
├── 13_person_security.py
├── 14_gesture_recognition.py
├── 15_holistic_tracking.py
├── 16_selfie_segmentation.py
├── 17_telegram_security.py
├── 18_telegram_person_security.py
├── 19_mqtt_robot.py
├── 20_websocket_robot.py
├── 21_face_recognition.py
├── 22_face_training.py
├── 23_face_model.py
├── cli/
    ├── 01_camera.py
    ├── 02_face_detection.py
    ├── ...
    ├── 21_face_recognition.py
    ├── 22_face_training.py
    └── 23_face_model.py
└── cli_web/
    ├── 01_camera.py
    ├── 02_face_detection.py
    ├── ...
    ├── 21_face_recognition.py
    ├── 22_face_training.py
    └── 23_face_model.py
```

## Local Development

```bash
pip install -e .
```

For build and publishing tools:

```bash
pip install -e ".[dev]"
```

TestPyPI and PyPI instructions are available in [`PUBLISHING.md`](PUBLISHING.md).

## Lisensi

MIT License.
