Metadata-Version: 2.4
Name: chloros-sdk
Version: 1.2.0
Summary: Official Python SDK for MAPIR Chloros image processing
Home-page: https://www.mapir.camera
Author: MAPIR Inc.
Author-email: "MAPIR Inc." <info@mapir.camera>
License: LicenseRef-Proprietary
Project-URL: Homepage, https://www.mapir.camera
Project-URL: Documentation, https://mapir.gitbook.io/chloros
Project-URL: Source, https://github.com/mapircamera/chloros-sdk
Project-URL: Support, https://www.mapir.camera/community/contact
Keywords: chloros,mapir,multispectral,ndvi,image-processing,agriculture,remote-sensing
Platform: Windows
Platform: Linux
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Image Processing
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: license_cache.py
Requires-Dist: requests>=2.25.0
Provides-Extra: progress
Requires-Dist: sseclient-py>=1.7.2; extra == "progress"
Provides-Extra: camera
Requires-Dist: bleak>=0.21.0; extra == "camera"
Provides-Extra: dev
Requires-Dist: pytest>=6.0; extra == "dev"
Requires-Dist: pytest-cov>=2.10; extra == "dev"
Requires-Dist: black>=20.8b1; extra == "dev"
Requires-Dist: mypy>=0.800; extra == "dev"
Dynamic: author
Dynamic: home-page
Dynamic: license-file
Dynamic: platform
Dynamic: requires-python

# Chloros Python SDK

Official Python SDK for MAPIR Chloros image processing software. Provides programmatic access to the Chloros API for automation, integration, and custom workflows.

## 🚀 Quick Start

```python
from chloros_sdk import ChlorosLocal
from pathlib import Path

# Initialize SDK (auto-starts backend)
chloros = ChlorosLocal()

# Create project and import images
chloros.create_project("MyProject", camera="Survey3N_RGN")
chloros.import_images(str(Path.home() / "DroneImages" / "Flight001"))

# Configure settings
chloros.configure(
    vignette_correction=True,
    reflectance_calibration=True,
    indices=["NDVI", "NDRE", "GNDVI"]
)

# Process images
chloros.process(mode="parallel", wait=True)
```

### One-Line Processing

```python
from chloros_sdk import process_folder
from pathlib import Path

images_dir = Path.home() / "DroneImages" / "Flight001"
results = process_folder(str(images_dir), indices=["NDVI", "NDRE"])
```

For LATTICE captures (no calibration panel — per-serial factory calibration is read from each image's XMP), use the LATTICE-tuned wrapper:

```python
from chloros_sdk import process_lattice_capture

results = process_lattice_capture("C:/Captures/2026-05-13_Field", indices=["NDVI"])
```

## 📋 Requirements

| Requirement          | Details                                                             |
|---------------------|---------------------------------------------------------------------|
| **Chloros Desktop** | Must be installed locally                                           |
| **License**         | **Chloros+ required** ([paid plan](https://cloud.mapir.camera/pricing)) |
| **Operating System**| Windows 10/11 or Linux (64-bit)                                     |
| **Python**          | Python 3.7 or higher                                                |
| **Memory**          | 8GB RAM minimum (16GB recommended)                                  |

> **⚠️ License Requirement**: The Chloros SDK requires an active Chloros+ subscription — Copper, Bronze, Silver, or Gold. The free **Iron** tier has no SDK/CLI access. Upgrade at [https://cloud.mapir.camera/pricing](https://cloud.mapir.camera/pricing)
>
> The floor is enforced by the backend, not just the client: an SDK call from an account without a paid plan fails with `403 PLAN_UPGRADE_REQUIRED`, while a logged-out caller gets `401 AUTH_REQUIRED`. `ChlorosLocal` surfaces the 403 as `ChlorosLicenseError`; the `connect_*` session helpers raise `ChlorosConnectError` (note it derives from plain `Exception`, so `except ChlorosError` will not catch it). Re-running `login` won't help in either case — the account is authenticated, just not entitled. Access keeps working offline for the plan's grace period (30 days on monthly plans, to expiry on yearly) and stops when that lapses until the machine reaches the server once.

## 📥 Installation

### From PyPI (Recommended)

```bash
pip install chloros-sdk
```

Progress monitoring (the `progress_callback` on `process()`) works with the base install — no extras required.

### From Source

```bash
git clone https://github.com/mapircamera/chloros-sdk.git
cd chloros-sdk
pip install -e .
```

### Platform-Specific Notes

**Linux:**
- Requires `exiftool`: `sudo apt install libimage-exiftool-perl`
- Data stored in `~/.local/share/chloros` (XDG compliant)
- Config stored in `~/.config/chloros`

**Windows:**
- ExifTool bundled with Chloros Desktop
- Data stored in `%LOCALAPPDATA%\Chloros`

## 🔌 Smart Connect

Drive live hardware straight from Python. The `connect_*` helpers are thin HTTP wrappers over the same local backend endpoints the Chloros GUI and CLI use, so connect/prep behavior is identical across all three surfaces. Each helper returns a session handle that works as a context manager, and each auto-starts the backend if it isn't already running.

### Single LATTICE camera

```python
import chloros_sdk

with chloros_sdk.connect_camera("213800234") as cam:
    cam.set_settings(exposure_time=10000)
    frames = cam.capture("output/", processing="all")
```

`connect_camera(serial, *, preset=None, settings=None, backend_url=..., timeout=60.0, auto_start_backend=True)` preserves the camera's current sticky GenICam state by default; pass `preset="default"` or `settings={...}` to seed specific values. The returned `CameraSession` exposes `set_settings(**kwargs)`, `read_nodes(names)`, `capture(output_dir, processing=..., levels=..., force_daq=...)`, and `disconnect()`.

### Synchronized camera array

```python
# serials[0] is the MASTER; the smart prep flow runs network analysis,
# picks a frame size that fits true simultaneous capture, and enables PTP.
serials = ["214701288", "213800234", "214000533"]

with chloros_sdk.connect_array(serials) as arr:
    result = arr.capture("output/", processing="all", aligned=True)
    print(len(result), "frames saved;", result.skipped)
```

`ArraySession` also provides `status()`, `capture_fastest()` (raw + `.daq` sidecar for later reprocessing), `capture_repeated()`, `record()` / `burst()` (returning a `RecorderHandle` with `stats()` / `stop()`), and `disconnect()`. Capture calls return a `CaptureResult` — a plain list of saved-frame dicts with an extra `.skipped` attribute explaining per-camera skips.

To grab a handle to an array that the GUI, CLI, or a previous script already connected (without reconnecting it):

```python
arr = chloros_sdk.attach_array("array-1779862544497")   # or a list of member serials
```

### DAQ spectral sensor

```python
# Smart-detect: opens whichever sensor is visible (ETH > BLE > USB)
with chloros_sdk.connect_daq_sensor() as daq:
    for frame in daq.latest(n=10):
        print(frame["spectrum"][:5])

# Or pin a transport / address:
daq = chloros_sdk.connect_daq_sensor(eth_host="daq-e-def330.local")  # DAQ-E
daq = chloros_sdk.connect_daq_sensor(mac="AA:BB:CC:DD:EE:FF")        # DAQ-M (BLE)
daq = chloros_sdk.connect_daq_sensor(transport="usb", port="COM3")   # DAQ-U

# Don't know the address? Scan for it. This is the only way to get a
# DAQ-M's BLE MAC -- it isn't printed on the device or listed by the OS.
for s in chloros_sdk.discover_daq_sensors(transports=["ble"]):
    print(s["transport"], s["address"], s["model"])   # ble C3:D8:.. DAQ-M
```

The sensor stays open in the backend pool, so SDK scripts, the CLI, and the GUI share one live handle instead of fighting over the serial port. `DAQSensorSession` exposes `status()`, `latest(n=...)`, `stream_start()` / `stream_stop()`, `record_start()` / `record_stop()` (calibrated `.daq` logging), and `disconnect()`.

### Backend auto-start

`ChlorosLocal` and every `connect_*` helper probe the backend URL first; if nothing is listening on a local URL, they locate the installed Chloros backend executable, start it window-less, and wait for it to come up (60 s default — `backend_startup_timeout` on `ChlorosLocal`). Set `auto_start_backend=False` to disable this, e.g. when pointing at a remote backend (remote URLs are never spawned). A backend started by a `connect_*` helper stays running for reuse; a backend started by `ChlorosLocal` is shut down when the instance is used as a context manager (or via `shutdown_backend()`).

## 🔬 DAQ Spectral Sensors

MAPIR's three spectral sensors (DAQ-U USB, DAQ-M BLE, DAQ-E Ethernet) are reachable two ways:

1. **Backend-pooled sessions** — `chloros_sdk.connect_daq_sensor()` (shown above). This is the pip-friendly path: pure HTTP against the local backend, works with the base `pip install chloros-sdk`.
2. **In-process driver classes** — `DAQUSensor` / `DAQMSensor` / `DAQESensor` / `SensorFleet`, which own the sensor transport directly with no backend required. **These ship with the Chloros Desktop install (`chloros-backend` / `chloros-cli`), not with the pip package** — on a pip-only install, `chloros_sdk.DAQ_AVAILABLE` is `False` and the classes are unavailable. Transport libraries install via plain pip: `pyserial` for DAQ-U, `bleak` for DAQ-M, `zeroconf` for DAQ-E discovery.

On a machine with Chloros Desktop installed, the in-process API looks like this:

```python
# Desktop install only — on pip-only installs chloros_sdk.DAQ_AVAILABLE
# is False and this import fails
from chloros_sdk import DAQESensor

sensor = DAQESensor(host="daq-e-def330.local", transport="multicast",
                    integration_time=32, frame_avg_num=20)
sensor.connect()

def on_spectrum(spectrum, is_saturated, integration_time, x, y, z):
    print(f"{integration_time} ms  Y={y:.2f}  sat={is_saturated}")

sensor.add_spectrum_callback(on_spectrum)
sensor.start_streaming()
# ... later ...
sensor.stop()
```

DAQ-E and LATTICE cameras share a PTP time domain anchored by the Chloros host; pass `wait_ptp=True` so the sensor blocks until PTP lock before streaming when strict frame-to-spectrum alignment matters. See the [DAQ sensor guide](https://mapir.gitbook.io/chloros/daq) in the Chloros manual.

## 📖 Documentation

Complete documentation available at: **https://mapir.gitbook.io/chloros/api-python-sdk** (full API detail: **https://mapir.gitbook.io/chloros/reference/sdk-reference**)

## 🎯 Use Cases

### Research & Academia
```python
# Integrate Chloros into analysis pipelines
from chloros_sdk import ChlorosLocal

chloros = ChlorosLocal()

for survey in field_surveys:
    chloros.create_project(survey.name)
    chloros.import_images(survey.folder)
    chloros.configure(indices=["NDVI"])
    results = chloros.process()
    # Exported index TIFFs land in the project's output folder,
    # ready for rasterio / numpy / pandas analysis
```

### Batch Processing
```python
# Process multiple flights automatically
from chloros_sdk import ChlorosLocal

chloros = ChlorosLocal()

for flight in flight_database:
    chloros.create_project(flight.name)
    chloros.import_images(flight.folder)
    chloros.configure(indices=flight.requested_indices)
    chloros.process()
```

### Custom Workflows
```python
# Advanced progress monitoring
from pathlib import Path

def progress_callback(progress, message):
    print(f"[{progress}%] {message}")

chloros = ChlorosLocal()
chloros.create_project("CustomWorkflow")
chloros.import_images(str(Path.home() / "Data"))
chloros.configure(indices=["NDVI", "NDRE"])
chloros.process(progress_callback=progress_callback)
```

## 🔑 License Activation

The SDK uses the same license as Chloros Desktop:

1. Open Chloros Desktop GUI
2. Login with your Chloros+ credentials (one-time)
3. SDK automatically uses cached license
4. License persists across reboots (30-day offline support on monthly plans; yearly plans stay valid offline until subscription expiration)

## 🛠️ API Reference

### ChlorosLocal Class

Main SDK class for local Chloros processing.

```python
chloros = ChlorosLocal(
    backend_url="http://127.0.0.1:5000",  # Backend URL (canonical name;
                                          #   api_url is the legacy alias —
                                          #   backend_url wins if both given)
    auto_start_backend=True,              # Auto-start backend if not running
    backend_exe=None,                     # Auto-detect backend path
    timeout=30,                           # Per-request timeout (seconds)
    backend_startup_timeout=60,           # Wait for auto-started backend (seconds)
    processing_timeout=14400,             # Hard cap for a process() run (4 h)
    processing_stuck_timeout=1800,        # Abort if progress stalls (30 min;
                                          #   resets on any progress change)
)
```

### Methods

#### `create_project(project_name, camera=None)`
Create a new Chloros project.

#### `import_images(folder_path, recursive=False)`
Import images from a folder.

#### `configure(**settings)`
Configure processing settings.

#### `process(mode="parallel", wait=True, progress_callback=None, poll_interval=2.0)`
Start processing images. `progress_callback(progress, message)` is polled every `poll_interval` seconds. On newer backends the returned dict includes a `summary` of what the run produced, with actionable hints surfaced as Python warnings.

#### `get_config()`
Get current project configuration.

#### `get_status()`
Get backend status.

### Module-Level Functions

- **`process_folder(folder_path, ...)`** — create project + import + configure + process in one call.
- **`process_lattice_capture(folder_path, ...)`** — LATTICE-tuned `process_folder` wrapper (per-serial XMP calibration, standard debayer, mixed Survey3/LATTICE folders supported).
- **`read_image_audit_tags(image_path)`** — read the `Chloros:*` audit metadata (calibration source, vignette source, processing level, ...) from a processed image.
- **`analyze_array_network(master_serial, slave_serials=None, width=2048, height=1536, pixel_format="BayerRG8", binning=1, ...)`** — network capability check for a proposed LATTICE array. The returned `status` is `ok`, `auto_shrunk` (use the returned `recommended` settings), `needs_force_slip`, or `error`.
- **`connect_camera` / `connect_array` / `attach_array` / `connect_daq_sensor`** — smart-connect sessions (see above), plus `list_cameras()`, `list_arrays()`, `list_daq_sensors()`, `discover_daq_sensors()`, and `discover_lattice_cameras()`.
- **`open_project(path)`** — open a saved Chloros project; the returned `ChlorosProject` exposes `.cameras` / `.arrays` / `.sensors` from the saved manifests, with `connect_all()`, `capture_all()`, and `disconnect_all()`.

### Availability Flags

Optional surfaces degrade gracefully — check these module constants instead of catching ImportError:

- **`chloros_sdk.CAMERA_AVAILABLE`** — in-process `lattice_sdk` camera control (requires the Arena SDK runtime).
- **`chloros_sdk.DAQ_AVAILABLE`** — in-process DAQ driver classes (`DAQUSensor` / `DAQMSensor` / `DAQESensor` / `SensorFleet`); `False` on pip-only installs — use `connect_daq_sensor()` instead.
- **`chloros_sdk.PROJECT_AVAILABLE`** — the saved-project surface (`open_project` / `ChlorosProject`).

## 🔐 Security

- **Proprietary Software**: Licensed under MAPIR proprietary license
- **Local Processing**: All processing happens locally (localhost API)
- **License Enforcement**: Requires active Chloros+ subscription
- **No Data Transmission**: Images never leave your computer

## 💡 Examples

See complete examples in the [SDK reference](https://mapir.gitbook.io/chloros/reference/sdk-reference).

## 🐛 Support

- **Email**: info@mapir.camera
- **Website**: [https://www.mapir.camera](https://www.mapir.camera)
- **Documentation**: [https://mapir.gitbook.io/chloros](https://mapir.gitbook.io/chloros)
- **Pricing**: [https://cloud.mapir.camera/pricing](https://cloud.mapir.camera/pricing)

## 📄 License

Copyright (c) 2026 MAPIR Inc. All rights reserved.

This is proprietary software requiring an active Chloros+ subscription.
Unauthorized use, distribution, or modification is prohibited.

## 🔄 Version History

### v1.1.0 – v1.1.5 (2026)
- Smart-connect sessions: `connect_camera` / `connect_array` / `attach_array` / `connect_daq_sensor` with backend-pooled `CameraSession`, `ArraySession`, and `DAQSensorSession` handles
- LATTICE capture processing: `process_lattice_capture`, `read_image_audit_tags`
- Array network analysis: `analyze_array_network`
- `process()` post-run processing summary with actionable hints
- New `enhanced` color profile for RGB captures

### v1.0.4 (2025)
- Added Linux support
- Cross-platform path handling
- XDG-compliant directories on Linux

### v1.0.0 (2025)
- Initial release
- Full API coverage for local processing
- Auto-backend startup
- Progress monitoring support
- Context manager support
