Spaces:
Runtime error
Runtime error
Commit ·
8096125
1
Parent(s): b0c1552
Add application file
Browse files- README.md +122 -12
- __pycache__/app.cpython-312.pyc +0 -0
- __pycache__/conftest.cpython-312-pytest-8.4.2.pyc +0 -0
- __pycache__/developer_api.cpython-312.pyc +0 -0
- app.py +471 -0
- assets/sample_cells.png +0 -0
- assets/sample_grid.png +0 -0
- assets/sample_portrait_placeholder.png +0 -0
- assets/style_texture_1.png +0 -0
- assets/style_texture_2.png +0 -0
- assets/style_texture_3.png +0 -0
- batch/__init__.py +0 -0
- batch/__pycache__/__init__.cpython-312.pyc +0 -0
- batch/__pycache__/dataset_processor.cpython-312.pyc +0 -0
- batch/dataset_processor.py +61 -0
- cli.py +98 -0
- conftest.py +6 -0
- cv_ops/__init__.py +0 -0
- cv_ops/__pycache__/__init__.cpython-312.pyc +0 -0
- cv_ops/__pycache__/analysis.cpython-312.pyc +0 -0
- cv_ops/__pycache__/morphology.cpython-312.pyc +0 -0
- cv_ops/__pycache__/transforms.cpython-312.pyc +0 -0
- cv_ops/analysis.py +44 -0
- cv_ops/morphology.py +49 -0
- cv_ops/transforms.py +46 -0
- developer_api.py +102 -0
- filters/__init__.py +0 -0
- filters/__pycache__/__init__.cpython-312.pyc +0 -0
- filters/__pycache__/builtin.cpython-312.pyc +0 -0
- filters/__pycache__/custom.cpython-312.pyc +0 -0
- filters/__pycache__/registry.cpython-312.pyc +0 -0
- filters/builtin.py +137 -0
- filters/custom.py +37 -0
- filters/registry.py +98 -0
- models/__init__.py +0 -0
- models/__pycache__/__init__.cpython-312.pyc +0 -0
- models/__pycache__/face_filters.cpython-312.pyc +0 -0
- models/__pycache__/style_transfer.cpython-312.pyc +0 -0
- models/face_filters.py +352 -0
- models/style_transfer.py +41 -0
- pytest.ini +4 -0
- requirements.txt +10 -0
- saved_ar_filters/ar_filters_registry.json +210 -0
- saved_ar_filters/fa0c305e-44b6-459a-ae32-a1256dc2ecc2.json +204 -0
- saved_filters/filters_registry.json +1 -0
- tests/__pycache__/test_cv_ops.cpython-312-pytest-8.4.2.pyc +0 -0
- tests/__pycache__/test_developer_api.cpython-312-pytest-8.4.2.pyc +0 -0
- tests/test_cv_ops.py +90 -0
- tests/test_developer_api.py +51 -0
README.md
CHANGED
|
@@ -1,15 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CV Lab Camera
|
| 2 |
+
|
| 3 |
+
CV Lab Camera is a local Gradio application and developer suite for computer vision, scientific image analysis, geometric transformations, morphology, neural style transfer, AR face overlays, and batch dataset processing.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## Developer Quickstart
|
| 8 |
+
|
| 9 |
+
### 1. Web App
|
| 10 |
+
Launch the interactive Gradio laboratory:
|
| 11 |
+
```bash
|
| 12 |
+
python app.py
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
### 2. Command Line Interface (CLI)
|
| 16 |
+
Process images directly from your terminal or shell scripts:
|
| 17 |
+
```bash
|
| 18 |
+
# Apply built-in filter
|
| 19 |
+
python cli.py filter --input input.jpg --filter Sepia --output result.jpg
|
| 20 |
+
|
| 21 |
+
# Apply geometric transformation
|
| 22 |
+
python cli.py transform --input input.jpg --op Rotation --angle 45 --output result.jpg
|
| 23 |
+
|
| 24 |
+
# Apply morphological operation
|
| 25 |
+
python cli.py morph --input input.jpg --op Opening --size 5 --output result.jpg
|
| 26 |
+
|
| 27 |
+
# Apply AR face overlay
|
| 28 |
+
python cli.py ar --input face.jpg --filter Glasses --output ar_result.jpg
|
| 29 |
+
|
| 30 |
+
# Apply neural style transfer
|
| 31 |
+
python cli.py style --input content.jpg --style-image style.jpg --output stylized.jpg
|
| 32 |
+
|
| 33 |
+
# Batch process dataset
|
| 34 |
+
python cli.py batch --dir ./my_dataset --filter Grayscale --output-zip processed.zip
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### 3. Python Developer SDK (`developer_api.py`)
|
| 38 |
+
Import CV Lab functions directly in your Python code:
|
| 39 |
+
```python
|
| 40 |
+
from developer_api import process_image, generate_python_snippet
|
| 41 |
+
|
| 42 |
+
# Process image programmatically
|
| 43 |
+
result, meta = process_image(
|
| 44 |
+
image_input="photo.png",
|
| 45 |
+
operation_type="filter",
|
| 46 |
+
operation_name="Sepia",
|
| 47 |
+
output_path="output_sepia.png"
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
# Generate copy-pasteable Python code
|
| 51 |
+
code = generate_python_snippet("Sepia", {"contrast": 1.2})
|
| 52 |
+
print(code)
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## Architecture Overview
|
| 58 |
+
|
| 59 |
+
```
|
| 60 |
+
ComputerVision_Project/
|
| 61 |
+
├── app.py # Gradio UI Web App (interactive tabs & code generator)
|
| 62 |
+
├── cli.py # Developer Command Line Interface (CLI)
|
| 63 |
+
├── developer_api.py # Developer SDK / Python API wrapper
|
| 64 |
+
├── batch/
|
| 65 |
+
│ └── dataset_processor.py # Batch directory/zip processing with manifest.json
|
| 66 |
+
├── cv_ops/
|
| 67 |
+
│ ├── analysis.py # Intensity histograms & pixel statistics
|
| 68 |
+
│ ├── morphology.py # Thresholding & morphological operations
|
| 69 |
+
│ └── transforms.py # Translation, rotation, scaling, reflection
|
| 70 |
+
├── filters/
|
| 71 |
+
│ ├── builtin.py # Pure NumPy / OpenCV filter functions
|
| 72 |
+
│ ├── custom.py # Custom kernel & pipeline JSON parsers
|
| 73 |
+
│ └── registry.py # Filter registry & saved JSON persistence
|
| 74 |
+
├── models/
|
| 75 |
+
│ ├── face_filters.py # OpenCV AR face landmark detector & filter engine
|
| 76 |
+
│ └── style_transfer.py # TensorFlow Hub Magenta neural style transfer
|
| 77 |
+
├── saved_filters/ # Saved custom filter JSONs
|
| 78 |
+
├── saved_ar_filters/ # Saved custom AR filter JSONs
|
| 79 |
+
├── tests/ # Pytest suite
|
| 80 |
+
└── requirements.txt # Python dependencies
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
---
|
| 84 |
+
|
| 85 |
+
## Custom Filters & AR Filters
|
| 86 |
+
|
| 87 |
+
### Image Processing Filter Pipelines
|
| 88 |
+
Save multi-step filter pipelines as JSON:
|
| 89 |
+
```json
|
| 90 |
+
[
|
| 91 |
+
{"operation": "Grayscale", "params": {}},
|
| 92 |
+
{"operation": "Sharpen", "params": {"amount": 1.4}}
|
| 93 |
+
]
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### Custom AR Face Filters
|
| 97 |
+
Design landmark-based AR overlays attached to `head_top`, `forehead`, `eyes`, `nose`, `mouth`, `chin`:
|
| 98 |
+
```json
|
| 99 |
+
{
|
| 100 |
+
"elements": [
|
| 101 |
+
{
|
| 102 |
+
"landmark": "forehead",
|
| 103 |
+
"shape": "crown",
|
| 104 |
+
"color": [255, 215, 0],
|
| 105 |
+
"scale": 1.0,
|
| 106 |
+
"offset_y": -0.15
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"landmark": "eyes",
|
| 110 |
+
"shape": "visor",
|
| 111 |
+
"color": [0, 255, 255],
|
| 112 |
+
"scale": 1.0
|
| 113 |
+
}
|
| 114 |
+
]
|
| 115 |
+
}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
---
|
| 119 |
|
| 120 |
+
## Testing
|
| 121 |
+
|
| 122 |
+
Run the test suite:
|
| 123 |
+
```bash
|
| 124 |
+
pytest
|
| 125 |
+
```
|
__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (38.5 kB). View file
|
|
|
__pycache__/conftest.cpython-312-pytest-8.4.2.pyc
ADDED
|
Binary file (603 Bytes). View file
|
|
|
__pycache__/developer_api.cpython-312.pyc
ADDED
|
Binary file (4.98 kB). View file
|
|
|
app.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CV Lab Camera: Gradio scientific camera and image filtering lab."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import tempfile
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
import gradio as gr
|
| 11 |
+
import numpy as np
|
| 12 |
+
from PIL import Image
|
| 13 |
+
|
| 14 |
+
from batch.dataset_processor import process_dataset
|
| 15 |
+
from cv_ops.analysis import histogram_figure, low_resolution_pair, pixel_preview, stats
|
| 16 |
+
from cv_ops.morphology import apply_morphology
|
| 17 |
+
from cv_ops.transforms import reflect, rotate, scale_image, translate
|
| 18 |
+
from filters.builtin import BUILTIN_FILTERS, ensure_rgb
|
| 19 |
+
from filters.custom import parse_kernel, parse_pipeline
|
| 20 |
+
from filters.registry import apply_definition, apply_step, delete_filter, load_definition, names, operation_to_definition, save_filter
|
| 21 |
+
from models.face_filters import apply_face_filter, ar_filter_names, delete_ar_filter, load_ar_definition, save_ar_filter
|
| 22 |
+
from models.style_transfer import stylize
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
ROOT = Path(__file__).resolve().parent
|
| 26 |
+
STYLE_SAMPLES = sorted(str(p) for p in (ROOT / "assets").glob("style_*.png"))
|
| 27 |
+
|
| 28 |
+
DEFAULT_CUSTOM_AR_JSON = json.dumps(
|
| 29 |
+
{
|
| 30 |
+
"elements": [
|
| 31 |
+
{"landmark": "forehead", "shape": "crown", "color": [255, 215, 0], "scale": 1.0, "offset_y": -0.15},
|
| 32 |
+
{"landmark": "eyes", "shape": "visor", "color": [0, 255, 255], "scale": 1.0},
|
| 33 |
+
{"landmark": "mouth", "shape": "mustache", "color": [40, 20, 20], "scale": 0.9, "offset_y": -0.05},
|
| 34 |
+
]
|
| 35 |
+
},
|
| 36 |
+
indent=2,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def export_image(image: np.ndarray | None) -> str | None:
|
| 41 |
+
if image is None:
|
| 42 |
+
return None
|
| 43 |
+
arr = np.asarray(image)
|
| 44 |
+
if arr.ndim == 2:
|
| 45 |
+
img_obj = Image.fromarray(arr)
|
| 46 |
+
else:
|
| 47 |
+
img_obj = Image.fromarray(arr.astype(np.uint8))
|
| 48 |
+
out_dir = Path(tempfile.mkdtemp(prefix="cv_lab_dl_"))
|
| 49 |
+
filepath = out_dir / "cv_lab_result.png"
|
| 50 |
+
img_obj.save(filepath)
|
| 51 |
+
return str(filepath)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def set_global_image(image: np.ndarray | None) -> tuple[np.ndarray | None, np.ndarray | None, str]:
|
| 55 |
+
if image is None:
|
| 56 |
+
return None, None, "No image loaded."
|
| 57 |
+
img = ensure_rgb(image)
|
| 58 |
+
return img, img, f"Loaded image: {img.shape[1]} x {img.shape[0]}"
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def make_code_snippet(operation: str, params: dict[str, Any]) -> str:
|
| 62 |
+
params_str = json.dumps(params, indent=2)
|
| 63 |
+
return f"""import numpy as np
|
| 64 |
+
from PIL import Image
|
| 65 |
+
from filters.registry import apply_step
|
| 66 |
+
|
| 67 |
+
image = np.array(Image.open("input.png").convert("RGB"))
|
| 68 |
+
params = {params_str}
|
| 69 |
+
result = apply_step(image, "{operation}", params)
|
| 70 |
+
Image.fromarray(result).save("output.png")
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def run_builtin(image: np.ndarray | None, filter_name: str, blur_method: str, kernel_size: int, edge_method: str, low: int, high: int, brightness: int, contrast: float, saturation: float, hue_shift: int, channel: str) -> tuple[np.ndarray | None, dict[str, Any], str]:
|
| 75 |
+
if image is None:
|
| 76 |
+
raise gr.Error("Load or capture an image first.")
|
| 77 |
+
params = {
|
| 78 |
+
"method": blur_method if filter_name == "Blur" else edge_method,
|
| 79 |
+
"kernel_size": kernel_size,
|
| 80 |
+
"low": low,
|
| 81 |
+
"high": high,
|
| 82 |
+
"brightness": brightness,
|
| 83 |
+
"contrast": contrast,
|
| 84 |
+
"saturation": saturation,
|
| 85 |
+
"hue_shift": hue_shift,
|
| 86 |
+
"channel": channel,
|
| 87 |
+
}
|
| 88 |
+
result = apply_step(image, filter_name, params)
|
| 89 |
+
definition = operation_to_definition(filter_name, params)
|
| 90 |
+
code = make_code_snippet(filter_name, params)
|
| 91 |
+
return result, definition, code
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def preview_kernel(image: np.ndarray | None, kernel_text: str) -> tuple[np.ndarray | None, dict[str, Any], str]:
|
| 95 |
+
if image is None:
|
| 96 |
+
raise gr.Error("Load or capture an image first.")
|
| 97 |
+
kernel = parse_kernel(kernel_text)
|
| 98 |
+
definition = {"type": "kernel", "kernel": kernel}
|
| 99 |
+
code = f"""import numpy as np
|
| 100 |
+
from PIL import Image
|
| 101 |
+
from filters.builtin import custom_kernel
|
| 102 |
+
|
| 103 |
+
image = np.array(Image.open("input.png").convert("RGB"))
|
| 104 |
+
kernel = {json.dumps(kernel)}
|
| 105 |
+
result = custom_kernel(image, kernel=kernel)
|
| 106 |
+
Image.fromarray(result).save("output.png")
|
| 107 |
+
"""
|
| 108 |
+
return apply_definition(image, definition), definition, code
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def preview_pipeline(image: np.ndarray | None, pipeline_text: str) -> tuple[np.ndarray | None, dict[str, Any], str]:
|
| 112 |
+
if image is None:
|
| 113 |
+
raise gr.Error("Load or capture an image first.")
|
| 114 |
+
definition = parse_pipeline(pipeline_text)
|
| 115 |
+
code = f"""import numpy as np
|
| 116 |
+
from PIL import Image
|
| 117 |
+
from filters.registry import apply_definition
|
| 118 |
+
|
| 119 |
+
image = np.array(Image.open("input.png").convert("RGB"))
|
| 120 |
+
pipeline = {json.dumps(definition, indent=2)}
|
| 121 |
+
result = apply_definition(image, pipeline)
|
| 122 |
+
Image.fromarray(result).save("output.png")
|
| 123 |
+
"""
|
| 124 |
+
return apply_definition(image, definition), definition, code
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def save_current_filter(name: str | None, definition: dict[str, Any] | None) -> tuple[str, gr.Dropdown]:
|
| 129 |
+
if not name or not name.strip():
|
| 130 |
+
raise gr.Error("Enter a filter name before saving.")
|
| 131 |
+
if not definition:
|
| 132 |
+
raise gr.Error("Preview a built-in, kernel, or pipeline filter before saving.")
|
| 133 |
+
saved = save_filter(name, definition)
|
| 134 |
+
return f"Saved filter '{saved['name']}'.", gr.Dropdown(choices=names(False), value=saved["name"])
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def load_saved_filter(image: np.ndarray | None, name: str | None) -> tuple[np.ndarray | None, dict[str, Any], str]:
|
| 138 |
+
if not name:
|
| 139 |
+
raise gr.Error("Select a saved filter from the dropdown to load.")
|
| 140 |
+
if image is None:
|
| 141 |
+
raise gr.Error("Load or capture an image first.")
|
| 142 |
+
try:
|
| 143 |
+
definition = load_definition(name)
|
| 144 |
+
except Exception as exc:
|
| 145 |
+
raise gr.Error(str(exc))
|
| 146 |
+
return apply_definition(image, definition), definition, json.dumps(definition, indent=2)
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def delete_saved(name: str | None) -> tuple[str, gr.Dropdown]:
|
| 150 |
+
if not name:
|
| 151 |
+
raise gr.Error("Select a saved filter from the dropdown to delete.")
|
| 152 |
+
delete_filter(name)
|
| 153 |
+
return f"Deleted '{name}'.", gr.Dropdown(choices=names(False), value=None)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def analyze(image: np.ndarray | None, percent: int, x: int, y: int):
|
| 157 |
+
if image is None:
|
| 158 |
+
raise gr.Error("Load or capture an image first.")
|
| 159 |
+
high_rgb, low_rgb, high_gray, low_gray = low_resolution_pair(image, percent)
|
| 160 |
+
return high_rgb, low_rgb, high_gray, low_gray, pixel_preview(high_rgb, x, y), pixel_preview(low_rgb, x, y), histogram_figure(high_rgb, low_rgb), {"high_rgb": stats(high_rgb), "low_rgb": stats(low_rgb), "high_gray": stats(high_gray), "low_gray": stats(low_gray)}
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def transform(image: np.ndarray | None, op: str, tx: int, ty: int, border: str, angle: float, scale: float, cx: float, cy: float, expand: bool, sx: float, sy: float, interp: str, flip: str):
|
| 164 |
+
if image is None:
|
| 165 |
+
raise gr.Error("Load or capture an image first.")
|
| 166 |
+
if op == "Translation":
|
| 167 |
+
return translate(image, tx, ty, border)
|
| 168 |
+
if op == "Rotation":
|
| 169 |
+
return rotate(image, angle, scale, cx, cy, expand)
|
| 170 |
+
if op == "Scaling":
|
| 171 |
+
return scale_image(image, sx, sy, interp)
|
| 172 |
+
return reflect(image, flip)
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def morph(image: np.ndarray | None, operation: str, threshold_method: str, threshold: int, shape: str, size: int, iterations: int):
|
| 176 |
+
if image is None:
|
| 177 |
+
raise gr.Error("Load or capture an image first.")
|
| 178 |
+
result, kernel = apply_morphology(image, operation, shape, size, iterations, threshold_method, threshold)
|
| 179 |
+
return result, kernel.tolist()
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def run_style(image: np.ndarray | None, style_upload: np.ndarray | None, style_path: str | None, max_size: int):
|
| 183 |
+
if image is None:
|
| 184 |
+
raise gr.Error("Load or capture a content image first.")
|
| 185 |
+
if style_upload is None and not style_path:
|
| 186 |
+
raise gr.Error("Upload a style image or choose a sample style.")
|
| 187 |
+
style = style_upload if style_upload is not None else np.asarray(Image.open(style_path).convert("RGB"))
|
| 188 |
+
result, seconds, message = stylize(image, style, max_size)
|
| 189 |
+
return result, f"{message} Inference time: {seconds:.2f}s"
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def run_face(image: np.ndarray | None, filter_name: str):
|
| 193 |
+
if image is None:
|
| 194 |
+
raise gr.Error("Load or capture an image first.")
|
| 195 |
+
return apply_face_filter(image, filter_name)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def preview_ar_custom(image: np.ndarray | None, custom_json: str | None) -> tuple[np.ndarray | None, str]:
|
| 199 |
+
if image is None:
|
| 200 |
+
raise gr.Error("Load or capture an image first.")
|
| 201 |
+
if not custom_json or not custom_json.strip():
|
| 202 |
+
raise gr.Error("Enter a custom AR filter JSON definition.")
|
| 203 |
+
try:
|
| 204 |
+
definition = json.loads(custom_json)
|
| 205 |
+
except Exception as exc:
|
| 206 |
+
raise gr.Error(f"Invalid AR filter JSON: {exc}")
|
| 207 |
+
return apply_face_filter(image, filter_name="Custom", custom_def=definition)
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def save_custom_ar_filter(name: str | None, custom_json: str | None) -> tuple[str, gr.Dropdown, gr.Dropdown]:
|
| 211 |
+
if not name or not name.strip():
|
| 212 |
+
raise gr.Error("Enter an AR filter name before saving.")
|
| 213 |
+
if not custom_json or not custom_json.strip():
|
| 214 |
+
raise gr.Error("Enter a custom AR filter JSON definition.")
|
| 215 |
+
try:
|
| 216 |
+
definition = json.loads(custom_json)
|
| 217 |
+
except Exception as exc:
|
| 218 |
+
raise gr.Error(f"Invalid AR filter JSON: {exc}")
|
| 219 |
+
saved = save_ar_filter(name, definition)
|
| 220 |
+
return f"Saved AR filter '{saved['name']}'.", gr.Dropdown(choices=ar_filter_names(True), value=saved["name"]), gr.Dropdown(choices=ar_filter_names(False), value=saved["name"])
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
def load_saved_ar_filter(image: np.ndarray | None, name: str | None) -> tuple[np.ndarray | None, str, str]:
|
| 224 |
+
if not name:
|
| 225 |
+
raise gr.Error("Select a saved AR filter from the dropdown to load.")
|
| 226 |
+
if image is None:
|
| 227 |
+
raise gr.Error("Load or capture an image first.")
|
| 228 |
+
try:
|
| 229 |
+
definition = load_ar_definition(name)
|
| 230 |
+
except Exception as exc:
|
| 231 |
+
raise gr.Error(str(exc))
|
| 232 |
+
result, msg = apply_face_filter(image, filter_name=name)
|
| 233 |
+
return result, json.dumps(definition, indent=2), msg
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def delete_saved_ar_filter(name: str | None) -> tuple[str, gr.Dropdown, gr.Dropdown]:
|
| 237 |
+
if not name:
|
| 238 |
+
raise gr.Error("Select a saved AR filter from the dropdown to delete.")
|
| 239 |
+
delete_ar_filter(name)
|
| 240 |
+
return f"Deleted AR filter '{name}'.", gr.Dropdown(choices=ar_filter_names(True), value="Glasses"), gr.Dropdown(choices=ar_filter_names(False), value=None)
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
def run_batch(files: list[str] | None, directory: str, filter_name: str, progress=gr.Progress()):
|
| 245 |
+
if not filter_name:
|
| 246 |
+
raise gr.Error("Choose a built-in or saved filter.")
|
| 247 |
+
return process_dataset(files, directory or None, filter_name, progress)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
custom_theme = gr.themes.Soft(
|
| 251 |
+
primary_hue="indigo",
|
| 252 |
+
secondary_hue="cyan",
|
| 253 |
+
neutral_hue="slate",
|
| 254 |
+
font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
with gr.Blocks(theme=custom_theme, title="CV Lab Camera Studio") as demo:
|
| 258 |
+
current_image = gr.State()
|
| 259 |
+
current_definition = gr.State()
|
| 260 |
+
|
| 261 |
+
gr.Markdown(
|
| 262 |
+
"""# 📸 CV Lab Camera Studio
|
| 263 |
+
### Comprehensive Computer Vision & AR Laboratory for Photo Editors, Scientists & Developers
|
| 264 |
+
*Capture with your webcam or upload an image to use seamlessly across all processing modules below.*
|
| 265 |
+
"""
|
| 266 |
+
)
|
| 267 |
+
with gr.Row():
|
| 268 |
+
global_input = gr.Image(label="Global Image Input", sources=["webcam", "upload"], type="numpy")
|
| 269 |
+
global_preview = gr.Image(label="Active Image Canvas", type="numpy")
|
| 270 |
+
status = gr.Markdown("✨ Load or capture an image above to start processing across all tabs.")
|
| 271 |
+
global_input.change(set_global_image, global_input, [current_image, global_preview, status])
|
| 272 |
+
|
| 273 |
+
with gr.Tabs():
|
| 274 |
+
with gr.Tab("🎨 Photo Filters & Pipelines"):
|
| 275 |
+
gr.Markdown("Apply built-in visual filters, adjust parameters, or create reusable custom matrix kernels and multi-step JSON pipelines.")
|
| 276 |
+
with gr.Row():
|
| 277 |
+
before = gr.Image(value=None, label="Original Input", type="numpy")
|
| 278 |
+
after = gr.Image(label="Filtered Result", type="numpy")
|
| 279 |
+
current_image.change(lambda x: x, current_image, before)
|
| 280 |
+
|
| 281 |
+
with gr.Group():
|
| 282 |
+
filter_name = gr.Dropdown(list(BUILTIN_FILTERS.keys())[:-1], value="Sepia", label="Built-in Filter Selection")
|
| 283 |
+
with gr.Row():
|
| 284 |
+
blur_method = gr.Radio(["Gaussian", "Median", "Bilateral"], value="Gaussian", label="Blur Method", info="Smoothing algorithm")
|
| 285 |
+
kernel_size = gr.Slider(1, 31, value=7, step=2, label="Kernel Size", info="Must be an odd integer")
|
| 286 |
+
edge_method = gr.Radio(["Canny", "Sobel"], value="Canny", label="Edge Detection Method")
|
| 287 |
+
with gr.Row():
|
| 288 |
+
low = gr.Slider(0, 255, value=80, step=1, label="Canny Low Threshold", info="Lower hysteresis bound")
|
| 289 |
+
high = gr.Slider(0, 255, value=160, step=1, label="Canny High Threshold", info="Upper hysteresis bound")
|
| 290 |
+
brightness = gr.Slider(-100, 100, value=0, step=1, label="Brightness Shift")
|
| 291 |
+
contrast = gr.Slider(0.1, 3.0, value=1.0, step=0.05, label="Contrast Multiplier")
|
| 292 |
+
with gr.Row():
|
| 293 |
+
saturation = gr.Slider(0, 3, value=1, step=0.05, label="Saturation Factor")
|
| 294 |
+
hue_shift = gr.Slider(-90, 90, value=0, step=1, label="Hue Shift Degrees")
|
| 295 |
+
channel = gr.Radio(["R", "G", "B"], value="R", label="Channel Isolation")
|
| 296 |
+
apply_builtin = gr.Button("⚡ Apply Built-in Filter", variant="primary")
|
| 297 |
+
|
| 298 |
+
with gr.Accordion("⚙️ Custom Kernel & Pipeline JSON Editor", open=False):
|
| 299 |
+
with gr.Row():
|
| 300 |
+
with gr.Column():
|
| 301 |
+
kernel_text = gr.Textbox(value="[[0,-1,0],[-1,5,-1],[0,-1,0]]", lines=4, label="Custom 3x3 Matrix Kernel JSON")
|
| 302 |
+
apply_kernel = gr.Button("Preview Custom Kernel")
|
| 303 |
+
with gr.Column():
|
| 304 |
+
pipeline_text = gr.Textbox(value='[{"operation":"Grayscale","params":{}},{"operation":"Sharpen","params":{"amount":1.4}}]', lines=5, label="Multi-Step Pipeline JSON")
|
| 305 |
+
apply_pipeline = gr.Button("Preview Pipeline")
|
| 306 |
+
|
| 307 |
+
with gr.Row():
|
| 308 |
+
save_name = gr.Textbox(label="Filter Name to Save")
|
| 309 |
+
save_btn = gr.Button("💾 Save Current Filter")
|
| 310 |
+
saved_dropdown = gr.Dropdown(choices=names(False), label="Load Saved Filter Preset")
|
| 311 |
+
load_btn = gr.Button("📂 Load Filter")
|
| 312 |
+
delete_btn = gr.Button("🗑️ Delete Filter", variant="stop")
|
| 313 |
+
|
| 314 |
+
filter_msg = gr.Markdown()
|
| 315 |
+
with gr.Accordion("📋 Filter JSON Definition & Python Code Snippet", open=False):
|
| 316 |
+
filter_json = gr.JSON(label="Current Filter Definition JSON")
|
| 317 |
+
dev_code = gr.Code(label="Python Code Snippet for Developers", language="python")
|
| 318 |
+
download_filter = gr.DownloadButton("📥 Download Filtered Result", value=None)
|
| 319 |
+
|
| 320 |
+
apply_builtin.click(run_builtin, [current_image, filter_name, blur_method, kernel_size, edge_method, low, high, brightness, contrast, saturation, hue_shift, channel], [after, current_definition, dev_code]).then(export_image, after, download_filter).then(lambda d: d, current_definition, filter_json)
|
| 321 |
+
apply_kernel.click(preview_kernel, [current_image, kernel_text], [after, current_definition, dev_code]).then(export_image, after, download_filter).then(lambda d: d, current_definition, filter_json)
|
| 322 |
+
apply_pipeline.click(preview_pipeline, [current_image, pipeline_text], [after, current_definition, dev_code]).then(export_image, after, download_filter).then(lambda d: d, current_definition, filter_json)
|
| 323 |
+
save_btn.click(save_current_filter, [save_name, current_definition], [filter_msg, saved_dropdown])
|
| 324 |
+
load_btn.click(load_saved_filter, [current_image, saved_dropdown], [after, current_definition, pipeline_text]).then(export_image, after, download_filter).then(lambda d: d, current_definition, filter_json)
|
| 325 |
+
delete_btn.click(delete_saved, saved_dropdown, [filter_msg, saved_dropdown])
|
| 326 |
+
|
| 327 |
+
with gr.Tab("🔬 Resolution & Color Analysis"):
|
| 328 |
+
gr.Markdown("Compare high/low resolution RGB and grayscale representations, analyze pixel crops, and inspect intensity histograms.")
|
| 329 |
+
with gr.Row():
|
| 330 |
+
percent = gr.Slider(5, 100, value=25, step=5, label="Low Resolution Percent", info="Downsample scale percentage")
|
| 331 |
+
px = gr.Number(value=0, precision=0, label="Pixel Crop X Coordinate")
|
| 332 |
+
py = gr.Number(value=0, precision=0, label="Pixel Crop Y Coordinate")
|
| 333 |
+
analyze_btn = gr.Button("🔬 Run Analysis", variant="primary")
|
| 334 |
+
with gr.Row():
|
| 335 |
+
high_rgb = gr.Image(label="High RGB Original", type="numpy")
|
| 336 |
+
low_rgb = gr.Image(label="Low RGB Upsampled", type="numpy")
|
| 337 |
+
with gr.Row():
|
| 338 |
+
high_gray = gr.Image(label="High Grayscale", type="numpy")
|
| 339 |
+
low_gray = gr.Image(label="Low Grayscale", type="numpy")
|
| 340 |
+
with gr.Row():
|
| 341 |
+
pix_high = gr.Dataframe(label="High RGB Pixel Values", row_count=5)
|
| 342 |
+
pix_low = gr.Dataframe(label="Low RGB Pixel Values", row_count=5)
|
| 343 |
+
hist = gr.Plot(label="Channel Intensity Histograms")
|
| 344 |
+
stat_json = gr.JSON(label="Detailed Image Statistics")
|
| 345 |
+
analyze_btn.click(analyze, [current_image, percent, px, py], [high_rgb, low_rgb, high_gray, low_gray, pix_high, pix_low, hist, stat_json])
|
| 346 |
+
|
| 347 |
+
with gr.Tab("📐 Geometric Transformations"):
|
| 348 |
+
gr.Markdown("Apply affine geometric transformations including translation, rotation, scaling, and reflection while inspecting matrix parameters.")
|
| 349 |
+
op = gr.Radio(["Translation", "Rotation", "Scaling", "Reflection"], value="Rotation", label="Transformation Operation")
|
| 350 |
+
with gr.Group():
|
| 351 |
+
with gr.Row():
|
| 352 |
+
tx = gr.Slider(-300, 300, value=30, step=1, label="Translation X Offset (px)")
|
| 353 |
+
ty = gr.Slider(-300, 300, value=30, step=1, label="Translation Y Offset (px)")
|
| 354 |
+
border = gr.Radio(["constant", "reflect", "replicate"], value="constant", label="Border Extrapolation")
|
| 355 |
+
with gr.Row():
|
| 356 |
+
angle = gr.Slider(0, 360, value=30, step=1, label="Rotation Angle (deg)")
|
| 357 |
+
rot_scale = gr.Slider(0.1, 3, value=1, step=0.05, label="Rotation Scale Factor")
|
| 358 |
+
cx = gr.Slider(0, 1, value=0.5, step=0.05, label="Center X Ratio")
|
| 359 |
+
cy = gr.Slider(0, 1, value=0.5, step=0.05, label="Center Y Ratio")
|
| 360 |
+
expand = gr.Checkbox(value=True, label="Expand Canvas Bounds")
|
| 361 |
+
with gr.Row():
|
| 362 |
+
sx = gr.Slider(0.1, 4, value=1.2, step=0.05, label="Scale X Factor")
|
| 363 |
+
sy = gr.Slider(0.1, 4, value=1.2, step=0.05, label="Scale Y Factor")
|
| 364 |
+
interp = gr.Radio(["nearest", "linear", "cubic", "area"], value="linear", label="Interpolation Mode")
|
| 365 |
+
flip = gr.Radio(["horizontal", "vertical", "both"], value="horizontal", label="Reflection Axis")
|
| 366 |
+
trans_btn = gr.Button("📐 Apply Transformation", variant="primary")
|
| 367 |
+
with gr.Row():
|
| 368 |
+
trans_before = gr.Image(label="Original Canvas", type="numpy")
|
| 369 |
+
trans_after = gr.Image(label="Transformed Canvas", type="numpy")
|
| 370 |
+
matrix = gr.JSON(label="Affine Transformation Matrix 2x3")
|
| 371 |
+
trans_dl = gr.DownloadButton("📥 Download Transformed Result", value=None)
|
| 372 |
+
current_image.change(lambda x: x, current_image, trans_before)
|
| 373 |
+
trans_btn.click(transform, [current_image, op, tx, ty, border, angle, rot_scale, cx, cy, expand, sx, sy, interp, flip], [trans_after, matrix]).then(export_image, trans_after, trans_dl)
|
| 374 |
+
|
| 375 |
+
with gr.Tab("🧪 Morphological Operations"):
|
| 376 |
+
gr.Markdown("Apply thresholding and mathematical morphology operations with custom structuring element kernels.")
|
| 377 |
+
with gr.Group():
|
| 378 |
+
with gr.Row():
|
| 379 |
+
morph_op = gr.Dropdown(["Erosion", "Dilation", "Opening", "Closing", "Gradient", "Top-Hat", "Black-Hat"], value="Opening", label="Morphological Operation")
|
| 380 |
+
thresh_method = gr.Radio(["Otsu", "Manual"], value="Otsu", label="Binarization Threshold Method")
|
| 381 |
+
thresh_value = gr.Slider(0, 255, value=128, step=1, label="Manual Threshold Value")
|
| 382 |
+
with gr.Row():
|
| 383 |
+
shape = gr.Radio(["rect", "ellipse", "cross"], value="rect", label="Kernel Structuring Shape")
|
| 384 |
+
morph_size = gr.Slider(1, 31, value=5, step=2, label="Kernel Size", info="Must be an odd integer")
|
| 385 |
+
iterations = gr.Slider(1, 10, value=1, step=1, label="Iteration Count")
|
| 386 |
+
morph_btn = gr.Button("🧪 Apply Morphology", variant="primary")
|
| 387 |
+
with gr.Row():
|
| 388 |
+
morph_before = gr.Image(label="Original Input", type="numpy")
|
| 389 |
+
morph_after = gr.Image(label="Morphology Output", type="numpy")
|
| 390 |
+
kernel_view = gr.JSON(label="Structuring Element Kernel Matrix")
|
| 391 |
+
morph_dl = gr.DownloadButton("📥 Download Result", value=None)
|
| 392 |
+
current_image.change(lambda x: x, current_image, morph_before)
|
| 393 |
+
morph_btn.click(morph, [current_image, morph_op, thresh_method, thresh_value, shape, morph_size, iterations], [morph_after, kernel_view]).then(export_image, morph_after, morph_dl)
|
| 394 |
+
|
| 395 |
+
with gr.Tab("🌌 Neural Style Transfer"):
|
| 396 |
+
gr.Markdown("Transfer artistic textures from a style reference image to your content image using the TensorFlow Hub Magenta deep neural model.")
|
| 397 |
+
with gr.Row():
|
| 398 |
+
style_upload = gr.Image(label="Custom Style Image Upload", type="numpy")
|
| 399 |
+
style_choice = gr.Dropdown(choices=STYLE_SAMPLES, label="Synthetic Sample Style Presets")
|
| 400 |
+
max_size = gr.Slider(128, 1024, value=512, step=64, label="Inference Resolution Max Size (px)", info="Higher resolution takes longer")
|
| 401 |
+
style_btn = gr.Button("🌌 Run Style Transfer", variant="primary")
|
| 402 |
+
style_out = gr.Image(label="Stylized Result", type="numpy")
|
| 403 |
+
style_msg = gr.Markdown()
|
| 404 |
+
style_dl = gr.DownloadButton("📥 Download Stylized Result", value=None)
|
| 405 |
+
style_btn.click(run_style, [current_image, style_upload, style_choice, max_size], [style_out, style_msg]).then(export_image, style_out, style_dl)
|
| 406 |
+
|
| 407 |
+
with gr.Tab("🎭 Face AR Filters"):
|
| 408 |
+
gr.Markdown("Apply landmark-anchored AR face overlays or design, preview, and save custom JSON AR filters.")
|
| 409 |
+
with gr.Row():
|
| 410 |
+
ar_choice = gr.Dropdown(choices=ar_filter_names(True), value="Glasses", label="AR Filter Presets")
|
| 411 |
+
ar_btn = gr.Button("🎭 Apply AR Filter", variant="primary")
|
| 412 |
+
with gr.Row():
|
| 413 |
+
ar_before = gr.Image(label="Original Face Input", type="numpy")
|
| 414 |
+
ar_out = gr.Image(label="AR Overlay Result", type="numpy")
|
| 415 |
+
current_image.change(lambda x: x, current_image, ar_before)
|
| 416 |
+
|
| 417 |
+
with gr.Accordion("🎨 Custom AR Filter Designer & JSON Editor", open=False):
|
| 418 |
+
ar_custom_text = gr.Textbox(value=DEFAULT_CUSTOM_AR_JSON, lines=8, label="Custom AR Filter JSON Definition")
|
| 419 |
+
ar_preview_btn = gr.Button("👁️ Preview Custom AR Filter")
|
| 420 |
+
with gr.Row():
|
| 421 |
+
ar_save_name = gr.Textbox(label="AR Filter Name to Save")
|
| 422 |
+
ar_save_btn = gr.Button("💾 Save Custom AR Filter")
|
| 423 |
+
ar_saved_dropdown = gr.Dropdown(choices=ar_filter_names(False), label="Load Saved AR Preset")
|
| 424 |
+
ar_load_btn = gr.Button("📂 Load AR Filter")
|
| 425 |
+
ar_delete_btn = gr.Button("🗑️ Delete AR Filter", variant="stop")
|
| 426 |
+
|
| 427 |
+
ar_msg = gr.Markdown()
|
| 428 |
+
ar_dl = gr.DownloadButton("📥 Download AR Result", value=None)
|
| 429 |
+
|
| 430 |
+
ar_btn.click(run_face, [current_image, ar_choice], [ar_out, ar_msg]).then(export_image, ar_out, ar_dl)
|
| 431 |
+
ar_preview_btn.click(preview_ar_custom, [current_image, ar_custom_text], [ar_out, ar_msg]).then(export_image, ar_out, ar_dl)
|
| 432 |
+
ar_save_btn.click(save_custom_ar_filter, [ar_save_name, ar_custom_text], [ar_msg, ar_choice, ar_saved_dropdown])
|
| 433 |
+
ar_load_btn.click(load_saved_ar_filter, [current_image, ar_saved_dropdown], [ar_out, ar_custom_text, ar_msg]).then(export_image, ar_out, ar_dl)
|
| 434 |
+
ar_delete_btn.click(delete_saved_ar_filter, ar_saved_dropdown, [ar_msg, ar_choice, ar_saved_dropdown])
|
| 435 |
+
|
| 436 |
+
with gr.Tab("📦 Batch Dataset Processing"):
|
| 437 |
+
gr.Markdown("Apply built-in or custom filter pipelines to multiple images, folders, or zip dataset archives with a reproducible manifest.")
|
| 438 |
+
batch_files = gr.File(label="Upload Images or Zip Archive", file_count="multiple", type="filepath")
|
| 439 |
+
batch_dir = gr.Textbox(label="Optional Local Dataset Directory Path")
|
| 440 |
+
batch_filter = gr.Dropdown(choices=names(True), value="Grayscale", label="Filter / Pipeline Selection")
|
| 441 |
+
refresh_filters = gr.Button("🔄 Refresh Filter List")
|
| 442 |
+
batch_btn = gr.Button("🚀 Process Batch Dataset", variant="primary")
|
| 443 |
+
batch_zip = gr.File(label="Processed Output Zip + manifest.json")
|
| 444 |
+
refresh_filters.click(lambda: gr.Dropdown(choices=names(True)), None, batch_filter)
|
| 445 |
+
batch_btn.click(run_batch, [batch_files, batch_dir, batch_filter], batch_zip)
|
| 446 |
+
|
| 447 |
+
with gr.Tab("📖 Guide & Documentation"):
|
| 448 |
+
gr.Markdown(
|
| 449 |
+
"""## Persona & Feature Guide
|
| 450 |
+
|
| 451 |
+
### 🎨 Photo Editors
|
| 452 |
+
- Use **Photo Filters & Pipelines** for instant visual edits like Sepia, Vignette, Emboss, Cartoonify, or Channel Isolation.
|
| 453 |
+
- Experiment with **Neural Style Transfer** to stylize portraits or landscapes with synthetic or custom reference artwork.
|
| 454 |
+
- Have fun with **Face AR Filters** for accessories (Glasses, Visors, Crown, Pirate Eyepatch, Dog Ears).
|
| 455 |
+
|
| 456 |
+
### 🔬 Scientists & Researchers
|
| 457 |
+
- Use **Resolution & Color Analysis** to downsample images and evaluate RGB vs Grayscale degradation, intensity histograms, and pixel crop tables.
|
| 458 |
+
- Perform reproducible **Morphological Operations** (Opening, Closing, Top-Hat, Black-Hat) with explicit structuring element kernels.
|
| 459 |
+
- Process large experiment datasets with **Batch Dataset Processing** to export processed images alongside `manifest.json`.
|
| 460 |
+
|
| 461 |
+
### 💻 Developers
|
| 462 |
+
- Use the **Python SDK** (`developer_api.py`) or **CLI** (`cli.py`) for command line processing.
|
| 463 |
+
- Build multi-step JSON filter pipelines or custom AR landmark overlays in the Web UI, save them to disk, and export auto-generated Python code snippets.
|
| 464 |
+
"""
|
| 465 |
+
)
|
| 466 |
+
|
| 467 |
+
|
| 468 |
+
if __name__ == "__main__":
|
| 469 |
+
demo.launch(share=False)
|
| 470 |
+
|
| 471 |
+
|
assets/sample_cells.png
ADDED
|
assets/sample_grid.png
ADDED
|
assets/sample_portrait_placeholder.png
ADDED
|
assets/style_texture_1.png
ADDED
|
assets/style_texture_2.png
ADDED
|
assets/style_texture_3.png
ADDED
|
batch/__init__.py
ADDED
|
File without changes
|
batch/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (148 Bytes). View file
|
|
|
batch/__pycache__/dataset_processor.cpython-312.pyc
ADDED
|
Binary file (4.7 kB). View file
|
|
|
batch/dataset_processor.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Batch dataset processing and reproducibility manifest export."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import shutil
|
| 7 |
+
import tempfile
|
| 8 |
+
import zipfile
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any, Iterable
|
| 11 |
+
|
| 12 |
+
import cv2
|
| 13 |
+
import numpy as np
|
| 14 |
+
from PIL import Image
|
| 15 |
+
|
| 16 |
+
from cv_ops.analysis import stats
|
| 17 |
+
from filters.registry import apply_definition, load_definition
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp"}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _iter_input_files(files: list[str] | None, directory: str | None, workdir: Path) -> Iterable[Path]:
|
| 24 |
+
if directory:
|
| 25 |
+
root = Path(directory).expanduser()
|
| 26 |
+
if root.exists():
|
| 27 |
+
yield from (p for p in root.rglob("*") if p.suffix.lower() in IMAGE_SUFFIXES)
|
| 28 |
+
for file in files or []:
|
| 29 |
+
path = Path(file)
|
| 30 |
+
if path.suffix.lower() == ".zip":
|
| 31 |
+
with zipfile.ZipFile(path) as zf:
|
| 32 |
+
zf.extractall(workdir / path.stem)
|
| 33 |
+
yield from (p for p in (workdir / path.stem).rglob("*") if p.suffix.lower() in IMAGE_SUFFIXES)
|
| 34 |
+
elif path.suffix.lower() in IMAGE_SUFFIXES:
|
| 35 |
+
yield path
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def process_dataset(files: list[str] | None, directory: str | None, filter_name: str, progress: Any = None) -> str:
|
| 39 |
+
definition = load_definition(filter_name)
|
| 40 |
+
temp_root = Path(tempfile.mkdtemp(prefix="cv_lab_batch_"))
|
| 41 |
+
out_dir = temp_root / "processed"
|
| 42 |
+
out_dir.mkdir()
|
| 43 |
+
manifest: list[dict[str, Any]] = []
|
| 44 |
+
inputs = list(_iter_input_files(files, directory, temp_root))
|
| 45 |
+
total = max(1, len(inputs))
|
| 46 |
+
for idx, path in enumerate(inputs):
|
| 47 |
+
if progress:
|
| 48 |
+
progress((idx + 1) / total, desc=f"Processing {path.name}")
|
| 49 |
+
record: dict[str, Any] = {"filename": path.name, "filter": filter_name}
|
| 50 |
+
try:
|
| 51 |
+
img = np.array(Image.open(path).convert("RGB"))
|
| 52 |
+
result = apply_definition(img, definition)
|
| 53 |
+
out_path = out_dir / f"{path.stem}_processed.png"
|
| 54 |
+
cv2.imwrite(str(out_path), cv2.cvtColor(result, cv2.COLOR_RGB2BGR))
|
| 55 |
+
record.update({"status": "ok", "output": out_path.name, "stats": stats(result)})
|
| 56 |
+
except Exception as exc:
|
| 57 |
+
record.update({"status": "error", "error": str(exc)})
|
| 58 |
+
manifest.append(record)
|
| 59 |
+
(out_dir / "manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
| 60 |
+
archive = shutil.make_archive(str(temp_root / "cv_lab_processed"), "zip", out_dir)
|
| 61 |
+
return archive
|
cli.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Command Line Interface (CLI) for CV Lab Camera operations."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
import sys
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
from developer_api import load_image, process_image, save_image
|
| 11 |
+
from batch.dataset_processor import process_dataset
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
parser = argparse.ArgumentParser(description="CV Lab Camera Command Line Interface for developers.")
|
| 16 |
+
subparsers = parser.add_subparsers(dest="command", help="Sub-commands")
|
| 17 |
+
|
| 18 |
+
# Filter subcommand
|
| 19 |
+
filter_parser = subparsers.add_parser("filter", help="Apply built-in or saved image filter")
|
| 20 |
+
filter_parser.add_argument("-i", "--input", required=True, help="Input image file path")
|
| 21 |
+
filter_parser.add_argument("-f", "--filter", default="Sepia", help="Filter name (e.g. Sepia, Grayscale, Blur)")
|
| 22 |
+
filter_parser.add_argument("-p", "--params", help="JSON params string")
|
| 23 |
+
filter_parser.add_argument("-o", "--output", required=True, help="Output image file path")
|
| 24 |
+
|
| 25 |
+
# Transform subcommand
|
| 26 |
+
trans_parser = subparsers.add_parser("transform", help="Apply geometric transformation")
|
| 27 |
+
trans_parser.add_argument("-i", "--input", required=True, help="Input image file path")
|
| 28 |
+
trans_parser.add_argument("--op", default="Rotation", choices=["Translation", "Rotation", "Scaling", "Reflection"], help="Transformation type")
|
| 29 |
+
trans_parser.add_argument("--angle", type=float, default=30.0, help="Rotation angle in degrees")
|
| 30 |
+
trans_parser.add_argument("--tx", type=int, default=30, help="X translation offset")
|
| 31 |
+
trans_parser.add_argument("--ty", type=int, default=30, help="Y translation offset")
|
| 32 |
+
trans_parser.add_argument("-o", "--output", required=True, help="Output image file path")
|
| 33 |
+
|
| 34 |
+
# Morphology subcommand
|
| 35 |
+
morph_parser = subparsers.add_parser("morph", help="Apply morphological operations")
|
| 36 |
+
morph_parser.add_argument("-i", "--input", required=True, help="Input image file path")
|
| 37 |
+
morph_parser.add_argument("--op", default="Opening", help="Morphology operation (e.g. Opening, Erosion, Dilation)")
|
| 38 |
+
morph_parser.add_argument("--size", type=int, default=5, help="Kernel size")
|
| 39 |
+
morph_parser.add_argument("-o", "--output", required=True, help="Output image file path")
|
| 40 |
+
|
| 41 |
+
# AR subcommand
|
| 42 |
+
ar_parser = subparsers.add_parser("ar", help="Apply face AR filter")
|
| 43 |
+
ar_parser.add_argument("-i", "--input", required=True, help="Input face image file path")
|
| 44 |
+
ar_parser.add_argument("-f", "--filter", default="Glasses", help="AR filter name (e.g. Glasses, Cyberpunk Visor, Crown & Star Sparkles)")
|
| 45 |
+
ar_parser.add_argument("-o", "--output", required=True, help="Output image file path")
|
| 46 |
+
|
| 47 |
+
# Style subcommand
|
| 48 |
+
style_parser = subparsers.add_parser("style", help="Apply neural style transfer")
|
| 49 |
+
style_parser.add_argument("-i", "--input", required=True, help="Content image file path")
|
| 50 |
+
style_parser.add_argument("-s", "--style-image", required=True, help="Style image file path")
|
| 51 |
+
style_parser.add_argument("--max-size", type=int, default=512, help="Output max image resolution")
|
| 52 |
+
style_parser.add_argument("-o", "--output", required=True, help="Output image file path")
|
| 53 |
+
|
| 54 |
+
# Batch subcommand
|
| 55 |
+
batch_parser = subparsers.add_parser("batch", help="Process image directory or list")
|
| 56 |
+
batch_parser.add_argument("--files", nargs="*", help="List of input image or zip files")
|
| 57 |
+
batch_parser.add_argument("--dir", help="Input directory")
|
| 58 |
+
batch_parser.add_argument("-f", "--filter", default="Grayscale", help="Filter or pipeline name")
|
| 59 |
+
batch_parser.add_argument("-o", "--output-zip", default="processed.zip", help="Output zip file path")
|
| 60 |
+
|
| 61 |
+
args = parser.parse_args()
|
| 62 |
+
|
| 63 |
+
if not args.command:
|
| 64 |
+
parser.print_help()
|
| 65 |
+
sys.exit(1)
|
| 66 |
+
|
| 67 |
+
if args.command == "filter":
|
| 68 |
+
params = json.loads(args.params) if args.params else {}
|
| 69 |
+
_, meta = process_image(args.input, "filter", operation_name=args.filter, params=params, output_path=args.output)
|
| 70 |
+
print(f"Applied filter '{args.filter}' -> Saved to {args.output}")
|
| 71 |
+
|
| 72 |
+
elif args.command == "transform":
|
| 73 |
+
params = {"angle": args.angle, "tx": args.tx, "ty": args.ty}
|
| 74 |
+
_, meta = process_image(args.input, "transform", operation_name=args.op, params=params, output_path=args.output)
|
| 75 |
+
print(f"Applied transform '{args.op}' -> Saved to {args.output}")
|
| 76 |
+
|
| 77 |
+
elif args.command == "morph":
|
| 78 |
+
params = {"size": args.size}
|
| 79 |
+
_, meta = process_image(args.input, "morphology", operation_name=args.op, params=params, output_path=args.output)
|
| 80 |
+
print(f"Applied morphology '{args.op}' -> Saved to {args.output}")
|
| 81 |
+
|
| 82 |
+
elif args.command == "ar":
|
| 83 |
+
_, meta = process_image(args.input, "ar", operation_name=args.filter, output_path=args.output)
|
| 84 |
+
print(f"Applied AR filter '{args.filter}' ({meta.get('status')}) -> Saved to {args.output}")
|
| 85 |
+
|
| 86 |
+
elif args.command == "style":
|
| 87 |
+
params = {"style_image": args.style_image, "max_size": args.max_size}
|
| 88 |
+
_, meta = process_image(args.input, "style", params=params, output_path=args.output)
|
| 89 |
+
print(f"Applied neural style transfer -> Saved to {args.output}")
|
| 90 |
+
|
| 91 |
+
elif args.command == "batch":
|
| 92 |
+
zip_path = process_dataset(args.files, args.dir, args.filter)
|
| 93 |
+
Path(zip_path).rename(args.output_zip)
|
| 94 |
+
print(f"Batch dataset processed with '{args.filter}' -> Saved archive to {args.output_zip}")
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
main()
|
conftest.py
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
root = Path(__file__).resolve().parent
|
| 5 |
+
if str(root) not in sys.path:
|
| 6 |
+
sys.path.insert(0, str(root))
|
cv_ops/__init__.py
ADDED
|
File without changes
|
cv_ops/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (149 Bytes). View file
|
|
|
cv_ops/__pycache__/analysis.cpython-312.pyc
ADDED
|
Binary file (4.22 kB). View file
|
|
|
cv_ops/__pycache__/morphology.cpython-312.pyc
ADDED
|
Binary file (3.24 kB). View file
|
|
|
cv_ops/__pycache__/transforms.cpython-312.pyc
ADDED
|
Binary file (4 kB). View file
|
|
|
cv_ops/analysis.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Resolution, color-space, statistics, and histogram helpers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import matplotlib.pyplot as plt
|
| 6 |
+
import numpy as np
|
| 7 |
+
import cv2
|
| 8 |
+
|
| 9 |
+
from filters.builtin import ensure_rgb
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def low_resolution_pair(image: np.ndarray, percent: int = 25) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
| 13 |
+
img = ensure_rgb(image)
|
| 14 |
+
h, w = img.shape[:2]
|
| 15 |
+
small = cv2.resize(img, (max(1, w * percent // 100), max(1, h * percent // 100)), interpolation=cv2.INTER_AREA)
|
| 16 |
+
low_rgb = cv2.resize(small, (w, h), interpolation=cv2.INTER_NEAREST)
|
| 17 |
+
high_gray = cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_RGB2GRAY), cv2.COLOR_GRAY2RGB)
|
| 18 |
+
low_gray = cv2.cvtColor(cv2.cvtColor(low_rgb, cv2.COLOR_RGB2GRAY), cv2.COLOR_GRAY2RGB)
|
| 19 |
+
return img, low_rgb, high_gray, low_gray
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def pixel_preview(image: np.ndarray, x: int = 0, y: int = 0, size: int = 6) -> list[list[str]]:
|
| 23 |
+
img = ensure_rgb(image)
|
| 24 |
+
y0, x0 = max(0, y), max(0, x)
|
| 25 |
+
crop = img[y0 : y0 + size, x0 : x0 + size]
|
| 26 |
+
return [[str(tuple(int(v) for v in px)) for px in row] for row in crop]
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def stats(image: np.ndarray) -> dict[str, float | int | list[int]]:
|
| 30 |
+
img = ensure_rgb(image)
|
| 31 |
+
return {"shape": list(img.shape), "mean": float(img.mean()), "std": float(img.std()), "min": int(img.min()), "max": int(img.max())}
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def histogram_figure(high_rgb: np.ndarray, low_rgb: np.ndarray):
|
| 35 |
+
fig, axes = plt.subplots(1, 2, figsize=(10, 4), tight_layout=True)
|
| 36 |
+
for ax, img, title in [(axes[0], high_rgb, "High resolution"), (axes[1], low_rgb, "Low resolution")]:
|
| 37 |
+
for idx, color in enumerate(["red", "green", "blue"]):
|
| 38 |
+
ax.hist(img[..., idx].ravel(), bins=64, range=(0, 255), color=color, alpha=0.35)
|
| 39 |
+
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
| 40 |
+
ax.hist(gray.ravel(), bins=64, range=(0, 255), color="black", alpha=0.35)
|
| 41 |
+
ax.set_title(title)
|
| 42 |
+
ax.set_xlabel("Intensity")
|
| 43 |
+
ax.set_ylabel("Pixels")
|
| 44 |
+
return fig
|
cv_ops/morphology.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Morphological image operations."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import cv2
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
from filters.builtin import ensure_rgb
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
SHAPES = {"rect": cv2.MORPH_RECT, "ellipse": cv2.MORPH_ELLIPSE, "cross": cv2.MORPH_CROSS}
|
| 12 |
+
OPS = {
|
| 13 |
+
"Erosion": cv2.MORPH_ERODE,
|
| 14 |
+
"Dilation": cv2.MORPH_DILATE,
|
| 15 |
+
"Opening": cv2.MORPH_OPEN,
|
| 16 |
+
"Closing": cv2.MORPH_CLOSE,
|
| 17 |
+
"Gradient": cv2.MORPH_GRADIENT,
|
| 18 |
+
"Top-Hat": cv2.MORPH_TOPHAT,
|
| 19 |
+
"Black-Hat": cv2.MORPH_BLACKHAT,
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def make_kernel(shape: str = "rect", size: int = 5) -> np.ndarray:
|
| 24 |
+
size = max(1, int(size))
|
| 25 |
+
if size % 2 == 0:
|
| 26 |
+
size += 1
|
| 27 |
+
return cv2.getStructuringElement(SHAPES.get(shape, cv2.MORPH_RECT), (size, size))
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def threshold_image(image: np.ndarray, method: str = "Otsu", threshold: int = 128) -> np.ndarray:
|
| 31 |
+
gray = cv2.cvtColor(ensure_rgb(image), cv2.COLOR_RGB2GRAY)
|
| 32 |
+
if method == "Otsu":
|
| 33 |
+
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
| 34 |
+
else:
|
| 35 |
+
_, binary = cv2.threshold(gray, int(threshold), 255, cv2.THRESH_BINARY)
|
| 36 |
+
return binary
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def apply_morphology(image: np.ndarray, operation: str = "Opening", shape: str = "rect", size: int = 5, iterations: int = 1, threshold_method: str = "Otsu", threshold: int = 128) -> tuple[np.ndarray, np.ndarray]:
|
| 40 |
+
binary = threshold_image(image, threshold_method, threshold)
|
| 41 |
+
kernel = make_kernel(shape, size)
|
| 42 |
+
op = OPS.get(operation, cv2.MORPH_OPEN)
|
| 43 |
+
if op == cv2.MORPH_ERODE:
|
| 44 |
+
result = cv2.erode(binary, kernel, iterations=int(iterations))
|
| 45 |
+
elif op == cv2.MORPH_DILATE:
|
| 46 |
+
result = cv2.dilate(binary, kernel, iterations=int(iterations))
|
| 47 |
+
else:
|
| 48 |
+
result = cv2.morphologyEx(binary, op, kernel, iterations=int(iterations))
|
| 49 |
+
return cv2.cvtColor(result, cv2.COLOR_GRAY2RGB), kernel
|
cv_ops/transforms.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""2D geometric transformations implemented with OpenCV."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import cv2
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
from filters.builtin import ensure_rgb
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
BORDER_MODES = {"constant": cv2.BORDER_CONSTANT, "reflect": cv2.BORDER_REFLECT, "replicate": cv2.BORDER_REPLICATE}
|
| 12 |
+
INTERPOLATION = {"nearest": cv2.INTER_NEAREST, "linear": cv2.INTER_LINEAR, "cubic": cv2.INTER_CUBIC, "area": cv2.INTER_AREA}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def translate(image: np.ndarray, x: int = 20, y: int = 20, border: str = "constant") -> tuple[np.ndarray, list[list[float]]]:
|
| 16 |
+
img = ensure_rgb(image)
|
| 17 |
+
matrix = np.float32([[1, 0, x], [0, 1, y]])
|
| 18 |
+
out = cv2.warpAffine(img, matrix, (img.shape[1], img.shape[0]), borderMode=BORDER_MODES.get(border, cv2.BORDER_CONSTANT))
|
| 19 |
+
return out, matrix.tolist()
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def rotate(image: np.ndarray, angle: float = 30, scale: float = 1.0, center_x: float = 0.5, center_y: float = 0.5, expand: bool = True) -> tuple[np.ndarray, list[list[float]]]:
|
| 23 |
+
img = ensure_rgb(image)
|
| 24 |
+
h, w = img.shape[:2]
|
| 25 |
+
center = (w * center_x, h * center_y)
|
| 26 |
+
matrix = cv2.getRotationMatrix2D(center, angle, scale)
|
| 27 |
+
out_w, out_h = w, h
|
| 28 |
+
if expand:
|
| 29 |
+
cos, sin = abs(matrix[0, 0]), abs(matrix[0, 1])
|
| 30 |
+
out_w, out_h = int((h * sin) + (w * cos)), int((h * cos) + (w * sin))
|
| 31 |
+
matrix[0, 2] += out_w / 2 - center[0]
|
| 32 |
+
matrix[1, 2] += out_h / 2 - center[1]
|
| 33 |
+
return cv2.warpAffine(img, matrix, (out_w, out_h)), matrix.tolist()
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def scale_image(image: np.ndarray, sx: float = 1.2, sy: float = 1.2, interpolation: str = "linear") -> tuple[np.ndarray, list[list[float]]]:
|
| 37 |
+
img = ensure_rgb(image)
|
| 38 |
+
out = cv2.resize(img, None, fx=sx, fy=sy, interpolation=INTERPOLATION.get(interpolation, cv2.INTER_LINEAR))
|
| 39 |
+
return out, [[sx, 0, 0], [0, sy, 0]]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def reflect(image: np.ndarray, mode: str = "horizontal") -> tuple[np.ndarray, list[list[float]]]:
|
| 43 |
+
img = ensure_rgb(image)
|
| 44 |
+
code = {"horizontal": 1, "vertical": 0, "both": -1}.get(mode, 1)
|
| 45 |
+
matrix = {"horizontal": [[-1, 0, img.shape[1]], [0, 1, 0]], "vertical": [[1, 0, 0], [0, -1, img.shape[0]]], "both": [[-1, 0, img.shape[1]], [0, -1, img.shape[0]]]}.get(mode)
|
| 46 |
+
return cv2.flip(img, code), matrix
|
developer_api.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""High-level Python API / SDK for CV Lab developers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any
|
| 8 |
+
|
| 9 |
+
import numpy as np
|
| 10 |
+
from PIL import Image
|
| 11 |
+
|
| 12 |
+
from batch.dataset_processor import process_dataset
|
| 13 |
+
from cv_ops.analysis import histogram_figure, low_resolution_pair, pixel_preview, stats
|
| 14 |
+
from cv_ops.morphology import apply_morphology
|
| 15 |
+
from cv_ops.transforms import reflect, rotate, scale_image, translate
|
| 16 |
+
from filters.builtin import BUILTIN_FILTERS, ensure_rgb
|
| 17 |
+
from filters.custom import parse_kernel, parse_pipeline
|
| 18 |
+
from filters.registry import apply_definition, apply_step, load_definition, save_filter
|
| 19 |
+
from models.face_filters import apply_face_filter, ar_filter_names, save_ar_filter
|
| 20 |
+
from models.style_transfer import stylize
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def load_image(source: str | Path | np.ndarray) -> np.ndarray:
|
| 24 |
+
"""Load an image from file path or return RGB numpy array."""
|
| 25 |
+
if isinstance(source, (str, Path)):
|
| 26 |
+
img = Image.open(source).convert("RGB")
|
| 27 |
+
return np.array(img, dtype=np.uint8)
|
| 28 |
+
return ensure_rgb(source)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def save_image(image: np.ndarray, output_path: str | Path) -> str:
|
| 32 |
+
"""Save an RGB numpy image array to file."""
|
| 33 |
+
path = Path(output_path)
|
| 34 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 35 |
+
Image.fromarray(image).save(path)
|
| 36 |
+
return str(path)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def generate_python_snippet(operation: str, params: dict[str, Any]) -> str:
|
| 40 |
+
"""Generate reproducible Python code snippet for developers."""
|
| 41 |
+
params_str = json.dumps(params, indent=2)
|
| 42 |
+
return f"""import numpy as np
|
| 43 |
+
from PIL import Image
|
| 44 |
+
from filters.registry import apply_step
|
| 45 |
+
|
| 46 |
+
# Load image
|
| 47 |
+
image = np.array(Image.open("input.jpg").convert("RGB"))
|
| 48 |
+
|
| 49 |
+
# Apply filter
|
| 50 |
+
params = {params_str}
|
| 51 |
+
result = apply_step(image, "{operation}", params)
|
| 52 |
+
|
| 53 |
+
# Save result
|
| 54 |
+
Image.fromarray(result).save("output.jpg")
|
| 55 |
+
"""
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def process_image(
|
| 59 |
+
image_input: str | Path | np.ndarray,
|
| 60 |
+
operation_type: str,
|
| 61 |
+
operation_name: str = "Grayscale",
|
| 62 |
+
params: dict[str, Any] | None = None,
|
| 63 |
+
output_path: str | Path | None = None,
|
| 64 |
+
) -> tuple[np.ndarray, dict[str, Any]]:
|
| 65 |
+
"""Programmatic API to process images with any CV Lab operation."""
|
| 66 |
+
img = load_image(image_input)
|
| 67 |
+
params = params or {}
|
| 68 |
+
|
| 69 |
+
if operation_type == "filter":
|
| 70 |
+
result = apply_step(img, operation_name, params)
|
| 71 |
+
meta = {"operation": operation_name, "params": params}
|
| 72 |
+
elif operation_type == "pipeline":
|
| 73 |
+
definition = parse_pipeline(operation_name) if isinstance(operation_name, str) else operation_name
|
| 74 |
+
result = apply_definition(img, definition)
|
| 75 |
+
meta = {"definition": definition}
|
| 76 |
+
elif operation_type == "transform":
|
| 77 |
+
if operation_name == "Translation":
|
| 78 |
+
result = translate(img, **params)[0]
|
| 79 |
+
elif operation_name == "Rotation":
|
| 80 |
+
result = rotate(img, **params)[0]
|
| 81 |
+
elif operation_name == "Scaling":
|
| 82 |
+
result = scale_image(img, **params)[0]
|
| 83 |
+
else:
|
| 84 |
+
result = reflect(img, **params)[0]
|
| 85 |
+
meta = {"operation": operation_name, "params": params}
|
| 86 |
+
elif operation_type == "morphology":
|
| 87 |
+
result, kernel = apply_morphology(img, operation_name, **params)
|
| 88 |
+
meta = {"operation": operation_name, "kernel_shape": kernel.shape}
|
| 89 |
+
elif operation_type == "ar":
|
| 90 |
+
result, msg = apply_face_filter(img, operation_name, custom_def=params.get("custom_def"))
|
| 91 |
+
meta = {"status": msg}
|
| 92 |
+
elif operation_type == "style":
|
| 93 |
+
style_img = load_image(params["style_image"])
|
| 94 |
+
result, time_sec, msg = stylize(img, style_img, max_size=params.get("max_size", 512))
|
| 95 |
+
meta = {"time_seconds": time_sec, "message": msg}
|
| 96 |
+
else:
|
| 97 |
+
raise ValueError(f"Unknown operation type: {operation_type}")
|
| 98 |
+
|
| 99 |
+
if output_path:
|
| 100 |
+
save_image(result, output_path)
|
| 101 |
+
|
| 102 |
+
return result, meta
|
filters/__init__.py
ADDED
|
File without changes
|
filters/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (150 Bytes). View file
|
|
|
filters/__pycache__/builtin.cpython-312.pyc
ADDED
|
Binary file (10.1 kB). View file
|
|
|
filters/__pycache__/custom.cpython-312.pyc
ADDED
|
Binary file (2.24 kB). View file
|
|
|
filters/__pycache__/registry.cpython-312.pyc
ADDED
|
Binary file (5.58 kB). View file
|
|
|
filters/builtin.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Built-in image filters for CV Lab Camera."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import Any, Callable
|
| 6 |
+
|
| 7 |
+
import cv2
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
FilterFn = Callable[..., np.ndarray]
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def _odd(value: int) -> int:
|
| 15 |
+
value = max(1, int(value))
|
| 16 |
+
return value if value % 2 else value + 1
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def ensure_rgb(image: np.ndarray) -> np.ndarray:
|
| 20 |
+
"""Return an RGB uint8 image with three channels."""
|
| 21 |
+
if image is None:
|
| 22 |
+
raise ValueError("Please provide an image first.")
|
| 23 |
+
arr = np.asarray(image)
|
| 24 |
+
if arr.ndim == 2:
|
| 25 |
+
arr = cv2.cvtColor(arr, cv2.COLOR_GRAY2RGB)
|
| 26 |
+
if arr.shape[-1] == 4:
|
| 27 |
+
arr = arr[..., :3]
|
| 28 |
+
return np.clip(arr, 0, 255).astype(np.uint8)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def grayscale(image: np.ndarray, **_: Any) -> np.ndarray:
|
| 32 |
+
gray = cv2.cvtColor(ensure_rgb(image), cv2.COLOR_RGB2GRAY)
|
| 33 |
+
return cv2.cvtColor(gray, cv2.COLOR_GRAY2RGB)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def sepia(image: np.ndarray, **_: Any) -> np.ndarray:
|
| 37 |
+
img = ensure_rgb(image).astype(np.float32)
|
| 38 |
+
kernel = np.array([[0.393, 0.769, 0.189], [0.349, 0.686, 0.168], [0.272, 0.534, 0.131]])
|
| 39 |
+
return np.clip(img @ kernel.T, 0, 255).astype(np.uint8)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def invert(image: np.ndarray, **_: Any) -> np.ndarray:
|
| 43 |
+
return 255 - ensure_rgb(image)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def blur(image: np.ndarray, method: str = "Gaussian", kernel_size: int = 7, **_: Any) -> np.ndarray:
|
| 47 |
+
img = ensure_rgb(image)
|
| 48 |
+
k = _odd(kernel_size)
|
| 49 |
+
if method == "Median":
|
| 50 |
+
return cv2.medianBlur(img, k)
|
| 51 |
+
if method == "Bilateral":
|
| 52 |
+
return cv2.bilateralFilter(img, k, 75, 75)
|
| 53 |
+
return cv2.GaussianBlur(img, (k, k), 0)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def sharpen(image: np.ndarray, amount: float = 1.0, **_: Any) -> np.ndarray:
|
| 57 |
+
img = ensure_rgb(image)
|
| 58 |
+
kernel = np.array([[0, -1, 0], [-1, 4 + amount, -1], [0, -1, 0]], dtype=np.float32)
|
| 59 |
+
return np.clip(cv2.filter2D(img, -1, kernel), 0, 255).astype(np.uint8)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def edge_detection(image: np.ndarray, method: str = "Canny", low: int = 80, high: int = 160, **_: Any) -> np.ndarray:
|
| 63 |
+
img = ensure_rgb(image)
|
| 64 |
+
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
| 65 |
+
if method == "Sobel":
|
| 66 |
+
sx = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3)
|
| 67 |
+
sy = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3)
|
| 68 |
+
edges = cv2.convertScaleAbs(cv2.magnitude(sx, sy))
|
| 69 |
+
else:
|
| 70 |
+
edges = cv2.Canny(gray, int(low), int(high))
|
| 71 |
+
return cv2.cvtColor(edges, cv2.COLOR_GRAY2RGB)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def brightness_contrast(image: np.ndarray, brightness: int = 0, contrast: float = 1.0, **_: Any) -> np.ndarray:
|
| 75 |
+
img = ensure_rgb(image).astype(np.float32)
|
| 76 |
+
return np.clip(img * float(contrast) + int(brightness), 0, 255).astype(np.uint8)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def saturation_hsv(image: np.ndarray, saturation: float = 1.0, hue_shift: int = 0, **_: Any) -> np.ndarray:
|
| 80 |
+
hsv = cv2.cvtColor(ensure_rgb(image), cv2.COLOR_RGB2HSV).astype(np.float32)
|
| 81 |
+
hsv[..., 0] = (hsv[..., 0] + int(hue_shift)) % 180
|
| 82 |
+
hsv[..., 1] = np.clip(hsv[..., 1] * float(saturation), 0, 255)
|
| 83 |
+
return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2RGB)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def emboss(image: np.ndarray, **_: Any) -> np.ndarray:
|
| 87 |
+
kernel = np.array([[-2, -1, 0], [-1, 1, 1], [0, 1, 2]], dtype=np.float32)
|
| 88 |
+
return np.clip(cv2.filter2D(ensure_rgb(image), -1, kernel) + 128, 0, 255).astype(np.uint8)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def cartoonify(image: np.ndarray, **_: Any) -> np.ndarray:
|
| 92 |
+
img = ensure_rgb(image)
|
| 93 |
+
smooth = cv2.bilateralFilter(img, 9, 120, 120)
|
| 94 |
+
edges = cv2.adaptiveThreshold(cv2.cvtColor(img, cv2.COLOR_RGB2GRAY), 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 9)
|
| 95 |
+
return cv2.bitwise_and(smooth, smooth, mask=edges)
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def vignette(image: np.ndarray, strength: float = 0.65, **_: Any) -> np.ndarray:
|
| 99 |
+
img = ensure_rgb(image).astype(np.float32)
|
| 100 |
+
rows, cols = img.shape[:2]
|
| 101 |
+
x = cv2.getGaussianKernel(cols, cols * float(strength))
|
| 102 |
+
y = cv2.getGaussianKernel(rows, rows * float(strength))
|
| 103 |
+
mask = (y @ x.T)
|
| 104 |
+
mask = mask / mask.max()
|
| 105 |
+
return np.clip(img * mask[..., None], 0, 255).astype(np.uint8)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def isolate_channel(image: np.ndarray, channel: str = "R", **_: Any) -> np.ndarray:
|
| 109 |
+
img = ensure_rgb(image)
|
| 110 |
+
out = np.zeros_like(img)
|
| 111 |
+
idx = {"R": 0, "G": 1, "B": 2}.get(channel, 0)
|
| 112 |
+
out[..., idx] = img[..., idx]
|
| 113 |
+
return out
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def custom_kernel(image: np.ndarray, kernel: list[list[float]] | np.ndarray, **_: Any) -> np.ndarray:
|
| 117 |
+
arr = np.asarray(kernel, dtype=np.float32)
|
| 118 |
+
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
|
| 119 |
+
raise ValueError("Kernel must be a square numeric matrix.")
|
| 120 |
+
return np.clip(cv2.filter2D(ensure_rgb(image), -1, arr), 0, 255).astype(np.uint8)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
BUILTIN_FILTERS: dict[str, FilterFn] = {
|
| 124 |
+
"Grayscale": grayscale,
|
| 125 |
+
"Sepia": sepia,
|
| 126 |
+
"Invert": invert,
|
| 127 |
+
"Blur": blur,
|
| 128 |
+
"Sharpen": sharpen,
|
| 129 |
+
"Edge Detection": edge_detection,
|
| 130 |
+
"Brightness/Contrast": brightness_contrast,
|
| 131 |
+
"Saturation/HSV Shift": saturation_hsv,
|
| 132 |
+
"Emboss": emboss,
|
| 133 |
+
"Cartoonify": cartoonify,
|
| 134 |
+
"Vignette": vignette,
|
| 135 |
+
"Channel Isolation": isolate_channel,
|
| 136 |
+
"Custom Kernel": custom_kernel,
|
| 137 |
+
}
|
filters/custom.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Custom kernel and JSON pipeline helpers."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
from filters.registry import apply_definition
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def parse_kernel(text: str) -> list[list[float]]:
|
| 14 |
+
try:
|
| 15 |
+
kernel = json.loads(text)
|
| 16 |
+
except json.JSONDecodeError as exc:
|
| 17 |
+
raise ValueError("Kernel must be valid JSON, for example [[0,-1,0],[-1,5,-1],[0,-1,0]].") from exc
|
| 18 |
+
arr = np.asarray(kernel, dtype=float)
|
| 19 |
+
if arr.ndim != 2 or arr.shape[0] != arr.shape[1]:
|
| 20 |
+
raise ValueError("Kernel must be a square matrix.")
|
| 21 |
+
return arr.tolist()
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def parse_pipeline(text: str) -> dict[str, Any]:
|
| 25 |
+
try:
|
| 26 |
+
payload = json.loads(text)
|
| 27 |
+
except json.JSONDecodeError as exc:
|
| 28 |
+
raise ValueError("Pipeline must be valid JSON.") from exc
|
| 29 |
+
if isinstance(payload, list):
|
| 30 |
+
payload = {"type": "pipeline", "steps": payload}
|
| 31 |
+
if payload.get("type") != "pipeline" or not isinstance(payload.get("steps"), list):
|
| 32 |
+
raise ValueError('Pipeline must look like {"type":"pipeline","steps":[...]} or a JSON step list.')
|
| 33 |
+
return payload
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def preview_definition(image: np.ndarray, definition: dict[str, Any]) -> np.ndarray:
|
| 37 |
+
return apply_definition(image, definition)
|
filters/registry.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Filter registry and saved-filter persistence."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import uuid
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
from filters.builtin import BUILTIN_FILTERS
|
| 13 |
+
from cv_ops.morphology import apply_morphology
|
| 14 |
+
from cv_ops.transforms import reflect, rotate, scale_image, translate
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 18 |
+
SAVE_DIR = ROOT / "saved_filters"
|
| 19 |
+
REGISTRY_PATH = SAVE_DIR / "filters_registry.json"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _ensure_store() -> None:
|
| 23 |
+
SAVE_DIR.mkdir(exist_ok=True)
|
| 24 |
+
if not REGISTRY_PATH.exists():
|
| 25 |
+
REGISTRY_PATH.write_text("{}", encoding="utf-8")
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def load_registry() -> dict[str, dict[str, Any]]:
|
| 29 |
+
_ensure_store()
|
| 30 |
+
return json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def save_filter(name: str, definition: dict[str, Any]) -> dict[str, Any]:
|
| 34 |
+
if not name.strip():
|
| 35 |
+
raise ValueError("Filter name is required.")
|
| 36 |
+
_ensure_store()
|
| 37 |
+
registry = load_registry()
|
| 38 |
+
filter_id = registry.get(name, {}).get("id", str(uuid.uuid4()))
|
| 39 |
+
payload = {"id": filter_id, "name": name.strip(), **definition}
|
| 40 |
+
path = SAVE_DIR / f"{filter_id}.json"
|
| 41 |
+
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
| 42 |
+
registry[name.strip()] = {"id": filter_id, "path": str(path.relative_to(ROOT)), "definition": payload}
|
| 43 |
+
REGISTRY_PATH.write_text(json.dumps(registry, indent=2), encoding="utf-8")
|
| 44 |
+
return payload
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def delete_filter(name: str) -> None:
|
| 48 |
+
registry = load_registry()
|
| 49 |
+
item = registry.pop(name, None)
|
| 50 |
+
if item:
|
| 51 |
+
path = ROOT / item["path"]
|
| 52 |
+
if path.exists():
|
| 53 |
+
path.unlink()
|
| 54 |
+
REGISTRY_PATH.write_text(json.dumps(registry, indent=2), encoding="utf-8")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def names(include_builtin: bool = True) -> list[str]:
|
| 58 |
+
saved = list(load_registry().keys())
|
| 59 |
+
return (list(BUILTIN_FILTERS.keys()) if include_builtin else []) + saved
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def operation_to_definition(operation: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
| 63 |
+
return {"type": "pipeline", "steps": [{"operation": operation, "params": params or {}}]}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def apply_step(image: np.ndarray, operation: str, params: dict[str, Any] | None = None) -> np.ndarray:
|
| 67 |
+
params = params or {}
|
| 68 |
+
if operation in BUILTIN_FILTERS:
|
| 69 |
+
return BUILTIN_FILTERS[operation](image, **params)
|
| 70 |
+
if operation == "Translate":
|
| 71 |
+
return translate(image, **params)[0]
|
| 72 |
+
if operation == "Rotate":
|
| 73 |
+
return rotate(image, **params)[0]
|
| 74 |
+
if operation == "Scale":
|
| 75 |
+
return scale_image(image, **params)[0]
|
| 76 |
+
if operation == "Reflect":
|
| 77 |
+
return reflect(image, **params)[0]
|
| 78 |
+
if operation == "Morphology":
|
| 79 |
+
return apply_morphology(image, **params)[0]
|
| 80 |
+
raise ValueError(f"Unknown operation: {operation}")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def apply_definition(image: np.ndarray, definition: dict[str, Any]) -> np.ndarray:
|
| 84 |
+
if definition.get("type") == "kernel":
|
| 85 |
+
return BUILTIN_FILTERS["Custom Kernel"](image, kernel=definition["kernel"])
|
| 86 |
+
result = image
|
| 87 |
+
for step in definition.get("steps", []):
|
| 88 |
+
result = apply_step(result, step["operation"], step.get("params", {}))
|
| 89 |
+
return result
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def load_definition(name: str) -> dict[str, Any]:
|
| 93 |
+
if name in BUILTIN_FILTERS:
|
| 94 |
+
return operation_to_definition(name)
|
| 95 |
+
item = load_registry().get(name)
|
| 96 |
+
if not item:
|
| 97 |
+
raise ValueError(f"Saved filter not found: {name}")
|
| 98 |
+
return item["definition"]
|
models/__init__.py
ADDED
|
File without changes
|
models/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (149 Bytes). View file
|
|
|
models/__pycache__/face_filters.cpython-312.pyc
ADDED
|
Binary file (26.9 kB). View file
|
|
|
models/__pycache__/style_transfer.cpython-312.pyc
ADDED
|
Binary file (2.81 kB). View file
|
|
|
models/face_filters.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Robust OpenCV based AR face filters and custom AR filter persistence."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import json
|
| 6 |
+
import math
|
| 7 |
+
import uuid
|
| 8 |
+
from functools import lru_cache
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
import cv2
|
| 13 |
+
import numpy as np
|
| 14 |
+
|
| 15 |
+
from filters.builtin import ensure_rgb
|
| 16 |
+
|
| 17 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 18 |
+
SAVE_AR_DIR = ROOT / "saved_ar_filters"
|
| 19 |
+
REGISTRY_AR_PATH = SAVE_AR_DIR / "ar_filters_registry.json"
|
| 20 |
+
|
| 21 |
+
BUILTIN_AR_FILTERS = [
|
| 22 |
+
"Glasses",
|
| 23 |
+
"Dog/Cat Ears + Nose",
|
| 24 |
+
"Face Mask",
|
| 25 |
+
"Sunglasses & Mustache",
|
| 26 |
+
"Crown & Star Sparkles",
|
| 27 |
+
"Pirate Eyepatch & Hat",
|
| 28 |
+
"Cyberpunk Visor",
|
| 29 |
+
"Party Hat & Horn",
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _ensure_ar_store() -> None:
|
| 34 |
+
SAVE_AR_DIR.mkdir(exist_ok=True)
|
| 35 |
+
if not REGISTRY_AR_PATH.exists():
|
| 36 |
+
REGISTRY_AR_PATH.write_text("{}", encoding="utf-8")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def load_ar_registry() -> dict[str, dict[str, Any]]:
|
| 40 |
+
_ensure_ar_store()
|
| 41 |
+
try:
|
| 42 |
+
return json.loads(REGISTRY_AR_PATH.read_text(encoding="utf-8"))
|
| 43 |
+
except Exception:
|
| 44 |
+
return {}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def save_ar_filter(name: str, definition: dict[str, Any]) -> dict[str, Any]:
|
| 48 |
+
clean_name = (name or "").strip()
|
| 49 |
+
if not clean_name:
|
| 50 |
+
raise ValueError("AR Filter name is required.")
|
| 51 |
+
_ensure_ar_store()
|
| 52 |
+
registry = load_ar_registry()
|
| 53 |
+
filter_id = registry.get(clean_name, {}).get("id", str(uuid.uuid4()))
|
| 54 |
+
payload = {"id": filter_id, "name": clean_name, **definition}
|
| 55 |
+
path = SAVE_AR_DIR / f"{filter_id}.json"
|
| 56 |
+
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
| 57 |
+
registry[clean_name] = {"id": filter_id, "path": str(path.relative_to(ROOT)), "definition": payload}
|
| 58 |
+
REGISTRY_AR_PATH.write_text(json.dumps(registry, indent=2), encoding="utf-8")
|
| 59 |
+
return payload
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def delete_ar_filter(name: str | None) -> None:
|
| 63 |
+
if not name:
|
| 64 |
+
return
|
| 65 |
+
registry = load_ar_registry()
|
| 66 |
+
item = registry.pop(name, None)
|
| 67 |
+
if item:
|
| 68 |
+
path = ROOT / item["path"]
|
| 69 |
+
if path.exists():
|
| 70 |
+
path.unlink()
|
| 71 |
+
REGISTRY_AR_PATH.write_text(json.dumps(registry, indent=2), encoding="utf-8")
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def ar_filter_names(include_builtin: bool = True) -> list[str]:
|
| 75 |
+
saved = list(load_ar_registry().keys())
|
| 76 |
+
return (list(BUILTIN_AR_FILTERS) if include_builtin else []) + saved
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def load_ar_definition(name: str | None) -> dict[str, Any]:
|
| 80 |
+
if not name:
|
| 81 |
+
raise ValueError("No AR filter selected.")
|
| 82 |
+
if name in BUILTIN_AR_FILTERS:
|
| 83 |
+
return {"name": name, "type": "builtin"}
|
| 84 |
+
item = load_ar_registry().get(name)
|
| 85 |
+
if not item:
|
| 86 |
+
raise ValueError(f"Saved AR filter not found: {name}")
|
| 87 |
+
return item.get("definition", {})
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
@lru_cache(maxsize=1)
|
| 91 |
+
def _get_cascades():
|
| 92 |
+
face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_default.xml")
|
| 93 |
+
eye_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_eye.xml")
|
| 94 |
+
return face_cascade, eye_cascade
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _detect_face_landmarks(img: np.ndarray) -> tuple[list[tuple[int, int, int, int, tuple[int, int], tuple[int, int]]], str]:
|
| 98 |
+
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
| 99 |
+
face_cascade, eye_cascade = _get_cascades()
|
| 100 |
+
|
| 101 |
+
faces = face_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=4, minSize=(30, 30))
|
| 102 |
+
if len(faces) == 0:
|
| 103 |
+
alt_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + "haarcascade_frontalface_alt2.xml")
|
| 104 |
+
faces = alt_cascade.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=3, minSize=(30, 30))
|
| 105 |
+
|
| 106 |
+
if len(faces) == 0:
|
| 107 |
+
return [], "No face detected in image."
|
| 108 |
+
|
| 109 |
+
results = []
|
| 110 |
+
for (x, y, w, h) in faces:
|
| 111 |
+
face_roi = gray[y : y + h, x : x + w]
|
| 112 |
+
eyes = eye_cascade.detectMultiScale(face_roi, scaleFactor=1.1, minNeighbors=3, minSize=(15, 15))
|
| 113 |
+
|
| 114 |
+
left_eye = (x + int(w * 0.3), y + int(h * 0.35))
|
| 115 |
+
right_eye = (x + int(w * 0.7), y + int(h * 0.35))
|
| 116 |
+
|
| 117 |
+
if len(eyes) >= 2:
|
| 118 |
+
sorted_eyes = sorted(eyes, key=lambda e: e[0])
|
| 119 |
+
ex1, ey1, ew1, eh1 = sorted_eyes[0]
|
| 120 |
+
ex2, ey2, ew2, eh2 = sorted_eyes[-1]
|
| 121 |
+
left_eye = (x + ex1 + ew1 // 2, y + ey1 + eh1 // 2)
|
| 122 |
+
right_eye = (x + ex2 + ew2 // 2, y + ey2 + eh2 // 2)
|
| 123 |
+
|
| 124 |
+
results.append((x, y, w, h, left_eye, right_eye))
|
| 125 |
+
|
| 126 |
+
return results, f"Detected {len(results)} face(s)."
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _blend_rgba(base: np.ndarray, overlay: np.ndarray, x: int, y: int) -> None:
|
| 130 |
+
h, w = overlay.shape[:2]
|
| 131 |
+
x0, y0 = max(0, x), max(0, y)
|
| 132 |
+
x1, y1 = min(base.shape[1], x + w), min(base.shape[0], y + h)
|
| 133 |
+
if x1 <= x0 or y1 <= y0:
|
| 134 |
+
return
|
| 135 |
+
crop = overlay[y0 - y : y1 - y, x0 - x : x1 - x]
|
| 136 |
+
alpha = crop[..., 3:4].astype(float) / 255.0
|
| 137 |
+
base[y0:y1, x0:x1] = (crop[..., :3] * alpha + base[y0:y1, x0:x1] * (1 - alpha)).astype(np.uint8)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def _draw_element(img: np.ndarray, element: dict[str, Any], landmarks: dict[str, tuple[int, int]], fw: int, fh: int, dist: int, angle: float) -> None:
|
| 141 |
+
if not isinstance(element, dict):
|
| 142 |
+
return
|
| 143 |
+
landmark_name = str(element.get("landmark", "eyes"))
|
| 144 |
+
pos = landmarks.get(landmark_name, landmarks["eyes"])
|
| 145 |
+
dx = int(element.get("offset_x", 0) * fw)
|
| 146 |
+
dy = int(element.get("offset_y", 0) * fh)
|
| 147 |
+
center = (pos[0] + dx, pos[1] + dy)
|
| 148 |
+
|
| 149 |
+
shape = str(element.get("shape", "circle")).lower()
|
| 150 |
+
raw_color = element.get("color", [255, 0, 0])
|
| 151 |
+
if not isinstance(raw_color, (list, tuple)) or len(raw_color) < 3:
|
| 152 |
+
raw_color = [255, 0, 0]
|
| 153 |
+
color = tuple(int(np.clip(c, 0, 255)) for c in raw_color[:3])
|
| 154 |
+
scale = max(0.05, float(element.get("scale", 1.0)))
|
| 155 |
+
|
| 156 |
+
if shape == "crown":
|
| 157 |
+
cw, ch = max(10, int(fw * 0.8 * scale)), max(10, int(fh * 0.4 * scale))
|
| 158 |
+
pts = np.array([
|
| 159 |
+
[center[0] - cw // 2, center[1]],
|
| 160 |
+
[center[0] - cw // 2, center[1] - ch],
|
| 161 |
+
[center[0] - cw // 4, center[1] - ch // 2],
|
| 162 |
+
[center[0], center[1] - ch],
|
| 163 |
+
[center[0] + cw // 4, center[1] - ch // 2],
|
| 164 |
+
[center[0] + cw // 2, center[1] - ch],
|
| 165 |
+
[center[0] + cw // 2, center[1]],
|
| 166 |
+
], np.int32)
|
| 167 |
+
cv2.fillPoly(img, [pts], color)
|
| 168 |
+
cv2.polylines(img, [pts], True, (255, 255, 255), 2)
|
| 169 |
+
cv2.circle(img, (center[0] - cw // 2, center[1] - ch), max(2, int(cw * 0.04)), (220, 30, 30), -1)
|
| 170 |
+
cv2.circle(img, (center[0], center[1] - ch), max(2, int(cw * 0.05)), (30, 220, 30), -1)
|
| 171 |
+
cv2.circle(img, (center[0] + cw // 2, center[1] - ch), max(2, int(cw * 0.04)), (220, 30, 30), -1)
|
| 172 |
+
|
| 173 |
+
elif shape in ("visor", "cyberpunk"):
|
| 174 |
+
vw, vh = max(10, int(dist * 2.4 * scale)), max(6, int(dist * 0.5 * scale))
|
| 175 |
+
overlay = np.zeros((vh, vw, 4), dtype=np.uint8)
|
| 176 |
+
cv2.rectangle(overlay, (0, 0), (vw - 1, vh - 1), (*color, 180), -1)
|
| 177 |
+
cv2.rectangle(overlay, (0, 0), (vw - 1, vh - 1), (255, 255, 255, 255), 2)
|
| 178 |
+
for i in range(10, vw, 20):
|
| 179 |
+
cv2.line(overlay, (i, 0), (i, min(vh, 8)), (255, 255, 255, 220), 1)
|
| 180 |
+
_blend_rgba(img, overlay, center[0] - vw // 2, center[1] - vh // 2)
|
| 181 |
+
|
| 182 |
+
elif shape == "mustache":
|
| 183 |
+
mw, mh = max(10, int(fw * 0.5 * scale)), max(4, int(fh * 0.15 * scale))
|
| 184 |
+
cv2.ellipse(img, (center[0] - mw // 4, center[1]), (mw // 4, mh), 20, 0, 180, color, -1)
|
| 185 |
+
cv2.ellipse(img, (center[0] + mw // 4, center[1]), (mw // 4, mh), -20, 0, 180, color, -1)
|
| 186 |
+
|
| 187 |
+
elif shape == "mask":
|
| 188 |
+
mw, mh = max(10, int(fw * 0.9 * scale)), max(10, int(fh * 0.45 * scale))
|
| 189 |
+
overlay = np.zeros((mh, mw, 4), dtype=np.uint8)
|
| 190 |
+
cv2.rectangle(overlay, (0, 0), (mw - 1, mh - 1), (*color, 220), -1)
|
| 191 |
+
cv2.rectangle(overlay, (0, 0), (mw - 1, mh - 1), (255, 255, 255, 255), 2)
|
| 192 |
+
_blend_rgba(img, overlay, center[0] - mw // 2, center[1] - mh // 2)
|
| 193 |
+
|
| 194 |
+
elif shape == "sparkles":
|
| 195 |
+
sr = max(4, int(fw * 0.08 * scale))
|
| 196 |
+
for offset in [(-int(dist * 0.8), -int(fh * 0.05)), (int(dist * 0.8), -int(fh * 0.05)), (0, -int(fh * 0.15))]:
|
| 197 |
+
sp = (center[0] + offset[0], center[1] + offset[1])
|
| 198 |
+
cv2.line(img, (sp[0] - sr, sp[1]), (sp[0] + sr, sp[1]), color, 2)
|
| 199 |
+
cv2.line(img, (sp[0], sp[1] - sr), (sp[0], sp[1] + sr), color, 2)
|
| 200 |
+
|
| 201 |
+
elif shape == "text":
|
| 202 |
+
txt = str(element.get("text", "AR"))
|
| 203 |
+
font_scale = max(0.5, fw / 150.0 * scale)
|
| 204 |
+
cv2.putText(img, txt, (center[0] - int(fw * 0.2), center[1]), cv2.FONT_HERSHEY_SIMPLEX, font_scale, color, 2, cv2.LINE_AA)
|
| 205 |
+
|
| 206 |
+
elif shape == "star":
|
| 207 |
+
sr = max(6, int(fw * 0.12 * scale))
|
| 208 |
+
pts = []
|
| 209 |
+
for i in range(10):
|
| 210 |
+
r = sr if i % 2 == 0 else sr // 2
|
| 211 |
+
a = i * math.pi / 5 - math.pi / 2
|
| 212 |
+
pts.append([int(center[0] + r * math.cos(a)), int(center[1] + r * math.sin(a))])
|
| 213 |
+
cv2.fillPoly(img, [np.array(pts, np.int32)], color)
|
| 214 |
+
|
| 215 |
+
elif shape == "heart":
|
| 216 |
+
hr = max(6, int(fw * 0.1 * scale))
|
| 217 |
+
cv2.circle(img, (center[0] - hr // 2, center[1] - hr // 2), hr // 2, color, -1)
|
| 218 |
+
cv2.circle(img, (center[0] + hr // 2, center[1] - hr // 2), hr // 2, color, -1)
|
| 219 |
+
pts = np.array([[center[0] - hr, center[1] - hr // 4], [center[0] + hr, center[1] - hr // 4], [center[0], center[1] + hr]], np.int32)
|
| 220 |
+
cv2.fillPoly(img, [pts], color)
|
| 221 |
+
|
| 222 |
+
elif shape == "rectangle":
|
| 223 |
+
rw, rh = max(6, int(fw * 0.3 * scale)), max(6, int(fh * 0.2 * scale))
|
| 224 |
+
cv2.rectangle(img, (center[0] - rw // 2, center[1] - rh // 2), (center[0] + rw // 2, center[1] + rh // 2), color, -1)
|
| 225 |
+
|
| 226 |
+
else:
|
| 227 |
+
r = max(4, int(fw * 0.15 * scale))
|
| 228 |
+
cv2.circle(img, center, r, color, -1)
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
def apply_face_filter(image: np.ndarray, filter_name: str = "Glasses", custom_def: dict[str, Any] | None = None) -> tuple[np.ndarray, str]:
|
| 232 |
+
img = ensure_rgb(image).copy()
|
| 233 |
+
h, w = img.shape[:2]
|
| 234 |
+
|
| 235 |
+
faces, status_msg = _detect_face_landmarks(img)
|
| 236 |
+
|
| 237 |
+
if not faces:
|
| 238 |
+
fx, fy, fw, fh = w // 4, h // 4, w // 2, h // 2
|
| 239 |
+
left_eye = (w // 3, h // 3)
|
| 240 |
+
right_eye = (2 * w // 3, h // 3)
|
| 241 |
+
faces = [(fx, fy, fw, fh, left_eye, right_eye)]
|
| 242 |
+
status_msg = "No face detected; filter centered on canvas."
|
| 243 |
+
else:
|
| 244 |
+
status_msg = f"AR face filter '{filter_name}' applied."
|
| 245 |
+
|
| 246 |
+
for (x, y, fw, fh, left_eye, right_eye) in faces:
|
| 247 |
+
dist = max(20, int(np.linalg.norm(np.array(right_eye) - np.array(left_eye))))
|
| 248 |
+
angle = math.degrees(math.atan2(right_eye[1] - left_eye[1], right_eye[0] - left_eye[0]))
|
| 249 |
+
|
| 250 |
+
landmarks = {
|
| 251 |
+
"head_top": (x + fw // 2, max(0, y - int(fh * 0.15))),
|
| 252 |
+
"forehead": (x + fw // 2, y + int(fh * 0.12)),
|
| 253 |
+
"eyes": ((left_eye[0] + right_eye[0]) // 2, (left_eye[1] + right_eye[1]) // 2),
|
| 254 |
+
"left_eye": left_eye,
|
| 255 |
+
"right_eye": right_eye,
|
| 256 |
+
"nose": (x + fw // 2, y + int(fh * 0.55)),
|
| 257 |
+
"mouth": (x + fw // 2, y + int(fh * 0.75)),
|
| 258 |
+
"chin": (x + fw // 2, y + int(fh * 0.9)),
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
definition = custom_def
|
| 262 |
+
if not definition and filter_name not in BUILTIN_AR_FILTERS:
|
| 263 |
+
try:
|
| 264 |
+
definition = load_ar_definition(filter_name)
|
| 265 |
+
except Exception:
|
| 266 |
+
definition = None
|
| 267 |
+
|
| 268 |
+
if definition and "elements" in definition:
|
| 269 |
+
for elem in definition["elements"]:
|
| 270 |
+
_draw_element(img, elem, landmarks, fw, fh, dist, angle)
|
| 271 |
+
continue
|
| 272 |
+
|
| 273 |
+
if filter_name == "Dog/Cat Ears + Nose":
|
| 274 |
+
ear_w, ear_h = int(fw * 0.35), int(fh * 0.4)
|
| 275 |
+
left_ear_pos = (x + int(fw * 0.05), max(0, y - int(fh * 0.3)))
|
| 276 |
+
right_ear_pos = (x + int(fw * 0.6), max(0, y - int(fh * 0.3)))
|
| 277 |
+
|
| 278 |
+
cv2.ellipse(img, (left_ear_pos[0] + ear_w // 2, left_ear_pos[1] + ear_h // 2), (ear_w // 2, ear_h // 2), -15, 0, 360, (140, 80, 40), -1)
|
| 279 |
+
cv2.ellipse(img, (right_ear_pos[0] + ear_w // 2, right_ear_pos[1] + ear_h // 2), (ear_w // 2, ear_h // 2), 15, 0, 360, (140, 80, 40), -1)
|
| 280 |
+
cv2.ellipse(img, (left_ear_pos[0] + ear_w // 2, left_ear_pos[1] + ear_h // 2), (int(ear_w * 0.3), int(ear_h * 0.3)), -15, 0, 360, (230, 150, 170), -1)
|
| 281 |
+
cv2.ellipse(img, (right_ear_pos[0] + ear_w // 2, right_ear_pos[1] + ear_h // 2), (int(ear_w * 0.3), int(ear_h * 0.3)), 15, 0, 360, (230, 150, 170), -1)
|
| 282 |
+
|
| 283 |
+
nose_pos = landmarks["nose"]
|
| 284 |
+
cv2.ellipse(img, nose_pos, (int(fw * 0.08), int(fh * 0.05)), 0, 0, 360, (40, 30, 30), -1)
|
| 285 |
+
cv2.line(img, (nose_pos[0] - 5, nose_pos[1]), (nose_pos[0] - int(fw * 0.3), nose_pos[1] - 5), (20, 20, 20), 2)
|
| 286 |
+
cv2.line(img, (nose_pos[0] - 5, nose_pos[1] + 5), (nose_pos[0] - int(fw * 0.3), nose_pos[1] + 15), (20, 20, 20), 2)
|
| 287 |
+
cv2.line(img, (nose_pos[0] + 5, nose_pos[1]), (nose_pos[0] + int(fw * 0.3), nose_pos[1] - 5), (20, 20, 20), 2)
|
| 288 |
+
cv2.line(img, (nose_pos[0] + 5, nose_pos[1] + 5), (nose_pos[0] + int(fw * 0.3), nose_pos[1] + 15), (20, 20, 20), 2)
|
| 289 |
+
|
| 290 |
+
elif filter_name == "Face Mask":
|
| 291 |
+
_draw_element(img, {"landmark": "mouth", "shape": "mask", "color": [70, 180, 220], "scale": 1.0}, landmarks, fw, fh, dist, angle)
|
| 292 |
+
|
| 293 |
+
elif filter_name == "Sunglasses & Mustache":
|
| 294 |
+
gw, gh = int(dist * 2.3), int(dist * 0.7)
|
| 295 |
+
overlay = np.zeros((gh, gw, 4), dtype=np.uint8)
|
| 296 |
+
cv2.ellipse(overlay, (int(gw * 0.28), gh // 2), (gh // 2, int(gh * 0.45)), 0, 0, 360, (20, 20, 20, 240), -1)
|
| 297 |
+
cv2.ellipse(overlay, (int(gw * 0.72), gh // 2), (gh // 2, int(gh * 0.45)), 0, 0, 360, (20, 20, 20, 240), -1)
|
| 298 |
+
cv2.line(overlay, (int(gw * 0.28), int(gh * 0.2)), (int(gw * 0.72), int(gh * 0.2)), (220, 180, 50, 255), 3)
|
| 299 |
+
_blend_rgba(img, overlay, landmarks["eyes"][0] - gw // 2, landmarks["eyes"][1] - gh // 2)
|
| 300 |
+
_draw_element(img, {"landmark": "mouth", "shape": "mustache", "color": [30, 20, 20], "scale": 1.0, "offset_y": -0.05}, landmarks, fw, fh, dist, angle)
|
| 301 |
+
|
| 302 |
+
elif filter_name == "Crown & Star Sparkles":
|
| 303 |
+
_draw_element(img, {"landmark": "forehead", "shape": "crown", "color": [255, 215, 0], "scale": 1.0, "offset_y": -0.15}, landmarks, fw, fh, dist, angle)
|
| 304 |
+
_draw_element(img, {"landmark": "eyes", "shape": "sparkles", "color": [255, 240, 100], "scale": 1.0}, landmarks, fw, fh, dist, angle)
|
| 305 |
+
|
| 306 |
+
elif filter_name == "Pirate Eyepatch & Hat":
|
| 307 |
+
hw, hh = int(fw * 1.1), int(fh * 0.5)
|
| 308 |
+
hat_pts = np.array([
|
| 309 |
+
[landmarks["head_top"][0] - hw // 2, landmarks["head_top"][1]],
|
| 310 |
+
[landmarks["head_top"][0], landmarks["head_top"][1] - hh],
|
| 311 |
+
[landmarks["head_top"][0] + hw // 2, landmarks["head_top"][1]],
|
| 312 |
+
], np.int32)
|
| 313 |
+
cv2.fillPoly(img, [hat_pts], (20, 20, 20))
|
| 314 |
+
cv2.polylines(img, [hat_pts], True, (200, 170, 40), 3)
|
| 315 |
+
|
| 316 |
+
ep = landmarks["left_eye"]
|
| 317 |
+
cv2.circle(img, ep, int(dist * 0.35), (15, 15, 15), -1)
|
| 318 |
+
cv2.circle(img, ep, int(dist * 0.35), (200, 200, 200), 2)
|
| 319 |
+
cv2.line(img, (0, max(0, ep[1] - int(dist * 0.2))), (w, ep[1] + int(dist * 0.2)), (15, 15, 15), 2)
|
| 320 |
+
|
| 321 |
+
elif filter_name == "Cyberpunk Visor":
|
| 322 |
+
_draw_element(img, {"landmark": "eyes", "shape": "visor", "color": [0, 240, 255], "scale": 1.1}, landmarks, fw, fh, dist, angle)
|
| 323 |
+
|
| 324 |
+
elif filter_name == "Party Hat & Horn":
|
| 325 |
+
hw, hh = int(fw * 0.5), int(fh * 0.6)
|
| 326 |
+
hat_pts = np.array([
|
| 327 |
+
[landmarks["head_top"][0] - hw // 2, landmarks["head_top"][1]],
|
| 328 |
+
[landmarks["head_top"][0], landmarks["head_top"][1] - hh],
|
| 329 |
+
[landmarks["head_top"][0] + hw // 2, landmarks["head_top"][1]],
|
| 330 |
+
], np.int32)
|
| 331 |
+
cv2.fillPoly(img, [hat_pts], (240, 60, 120))
|
| 332 |
+
cv2.circle(img, (landmarks["head_top"][0], landmarks["head_top"][1] - hh), 8, (255, 230, 80), -1)
|
| 333 |
+
|
| 334 |
+
else:
|
| 335 |
+
gw, gh = int(dist * 2.2), int(dist * 0.65)
|
| 336 |
+
overlay = np.zeros((gh, gw, 4), dtype=np.uint8)
|
| 337 |
+
|
| 338 |
+
cv2.circle(overlay, (int(gw * 0.28), gh // 2), gh // 2 - 2, (30, 30, 30, 255), 4)
|
| 339 |
+
cv2.circle(overlay, (int(gw * 0.28), gh // 2), gh // 2 - 5, (20, 50, 80, 140), -1)
|
| 340 |
+
|
| 341 |
+
cv2.circle(overlay, (int(gw * 0.72), gh // 2), gh // 2 - 2, (30, 30, 30, 255), 4)
|
| 342 |
+
cv2.circle(overlay, (int(gw * 0.72), gh // 2), gh // 2 - 5, (20, 50, 80, 140), -1)
|
| 343 |
+
|
| 344 |
+
cv2.line(overlay, (int(gw * 0.45), gh // 2), (int(gw * 0.55), gh // 2), (30, 30, 30, 255), 4)
|
| 345 |
+
|
| 346 |
+
if abs(angle) > 1:
|
| 347 |
+
matrix = cv2.getRotationMatrix2D((gw / 2, gh / 2), angle, 1)
|
| 348 |
+
overlay = cv2.warpAffine(overlay, matrix, (gw, gh), borderMode=cv2.BORDER_TRANSPARENT)
|
| 349 |
+
|
| 350 |
+
_blend_rgba(img, overlay, landmarks["eyes"][0] - gw // 2, landmarks["eyes"][1] - gh // 2)
|
| 351 |
+
|
| 352 |
+
return img, status_msg
|
models/style_transfer.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Lazy TensorFlow Hub arbitrary style transfer wrapper."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import time
|
| 7 |
+
from functools import lru_cache
|
| 8 |
+
|
| 9 |
+
import cv2
|
| 10 |
+
import numpy as np
|
| 11 |
+
|
| 12 |
+
os.environ.setdefault("TFHUB_MODEL_LOAD_FORMAT", "COMPRESSED")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@lru_cache(maxsize=1)
|
| 16 |
+
def _load_model():
|
| 17 |
+
import tensorflow_hub as hub
|
| 18 |
+
|
| 19 |
+
return hub.load("https://tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/2")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _to_tensor(image: np.ndarray, max_size: int):
|
| 23 |
+
import tensorflow as tf
|
| 24 |
+
|
| 25 |
+
img = np.asarray(image).astype(np.float32) / 255.0
|
| 26 |
+
h, w = img.shape[:2]
|
| 27 |
+
scale = min(1.0, max_size / max(h, w))
|
| 28 |
+
if scale < 1:
|
| 29 |
+
img = cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)
|
| 30 |
+
return tf.constant(img[None, ...])
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def stylize(content: np.ndarray, style: np.ndarray, max_size: int = 512) -> tuple[np.ndarray, float, str]:
|
| 34 |
+
try:
|
| 35 |
+
start = time.perf_counter()
|
| 36 |
+
model = _load_model()
|
| 37 |
+
output = model(_to_tensor(content, max_size), _to_tensor(style, 256))[0]
|
| 38 |
+
arr = np.clip(np.array(output[0]) * 255, 0, 255).astype(np.uint8)
|
| 39 |
+
return arr, time.perf_counter() - start, "Style transfer complete."
|
| 40 |
+
except Exception as exc:
|
| 41 |
+
return np.asarray(content).astype(np.uint8), 0.0, f"Style transfer unavailable: {exc}"
|
pytest.ini
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[pytest]
|
| 2 |
+
pythonpath = .
|
| 3 |
+
filterwarnings =
|
| 4 |
+
ignore::DeprecationWarning
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.44,<6
|
| 2 |
+
opencv-python>=4.9,<5
|
| 3 |
+
numpy>=1.26,<3
|
| 4 |
+
pillow>=10,<12
|
| 5 |
+
matplotlib>=3.8,<4
|
| 6 |
+
tensorflow>=2.15,<2.18
|
| 7 |
+
tensorflow-hub>=0.16,<1
|
| 8 |
+
mediapipe>=0.10,<1
|
| 9 |
+
pytest>=8,<9
|
| 10 |
+
setuptools<70
|
saved_ar_filters/ar_filters_registry.json
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"tiger": {
|
| 3 |
+
"id": "fa0c305e-44b6-459a-ae32-a1256dc2ecc2",
|
| 4 |
+
"path": "saved_ar_filters\\fa0c305e-44b6-459a-ae32-a1256dc2ecc2.json",
|
| 5 |
+
"definition": {
|
| 6 |
+
"id": "fa0c305e-44b6-459a-ae32-a1256dc2ecc2",
|
| 7 |
+
"name": "tiger",
|
| 8 |
+
"filter": {
|
| 9 |
+
"name": "Royal Tiger",
|
| 10 |
+
"version": "2.0",
|
| 11 |
+
"type": "face_mask",
|
| 12 |
+
"tracking": {
|
| 13 |
+
"mode": "face_mesh",
|
| 14 |
+
"landmarks": 468,
|
| 15 |
+
"smooth_factor": 0.85,
|
| 16 |
+
"mirror": true
|
| 17 |
+
},
|
| 18 |
+
"elements": [
|
| 19 |
+
{
|
| 20 |
+
"landmark": "full_face",
|
| 21 |
+
"shape": "tiger_fur",
|
| 22 |
+
"color": [
|
| 23 |
+
230,
|
| 24 |
+
120,
|
| 25 |
+
20
|
| 26 |
+
],
|
| 27 |
+
"scale": 1.12,
|
| 28 |
+
"opacity": 0.92
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"landmark": "forehead",
|
| 32 |
+
"shape": "tiger_stripes",
|
| 33 |
+
"color": [
|
| 34 |
+
20,
|
| 35 |
+
12,
|
| 36 |
+
8
|
| 37 |
+
],
|
| 38 |
+
"scale": 1.05,
|
| 39 |
+
"opacity": 0.95,
|
| 40 |
+
"pattern": "vertical"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"landmark": "left_temple",
|
| 44 |
+
"shape": "tiger_stripes",
|
| 45 |
+
"color": [
|
| 46 |
+
15,
|
| 47 |
+
10,
|
| 48 |
+
5
|
| 49 |
+
],
|
| 50 |
+
"scale": 0.9,
|
| 51 |
+
"rotation": -18
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"landmark": "right_temple",
|
| 55 |
+
"shape": "tiger_stripes",
|
| 56 |
+
"color": [
|
| 57 |
+
15,
|
| 58 |
+
10,
|
| 59 |
+
5
|
| 60 |
+
],
|
| 61 |
+
"scale": 0.9,
|
| 62 |
+
"rotation": 18
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"landmark": "left_cheek",
|
| 66 |
+
"shape": "tiger_stripes",
|
| 67 |
+
"color": [
|
| 68 |
+
20,
|
| 69 |
+
12,
|
| 70 |
+
6
|
| 71 |
+
],
|
| 72 |
+
"scale": 0.95,
|
| 73 |
+
"rotation": -25
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"landmark": "right_cheek",
|
| 77 |
+
"shape": "tiger_stripes",
|
| 78 |
+
"color": [
|
| 79 |
+
20,
|
| 80 |
+
12,
|
| 81 |
+
6
|
| 82 |
+
],
|
| 83 |
+
"scale": 0.95,
|
| 84 |
+
"rotation": 25
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"landmark": "forehead",
|
| 88 |
+
"shape": "tiger_crown",
|
| 89 |
+
"color": [
|
| 90 |
+
255,
|
| 91 |
+
215,
|
| 92 |
+
0
|
| 93 |
+
],
|
| 94 |
+
"scale": 0.85,
|
| 95 |
+
"offset_y": -0.18,
|
| 96 |
+
"glow": true
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"landmark": "eyes",
|
| 100 |
+
"shape": "tiger_eyes",
|
| 101 |
+
"color": [
|
| 102 |
+
255,
|
| 103 |
+
180,
|
| 104 |
+
30
|
| 105 |
+
],
|
| 106 |
+
"scale": 1.08,
|
| 107 |
+
"glow": true,
|
| 108 |
+
"pupil": "vertical_slit"
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"landmark": "eyes",
|
| 112 |
+
"shape": "cyan_eye_outline",
|
| 113 |
+
"color": [
|
| 114 |
+
0,
|
| 115 |
+
255,
|
| 116 |
+
255
|
| 117 |
+
],
|
| 118 |
+
"scale": 1.02,
|
| 119 |
+
"opacity": 0.7,
|
| 120 |
+
"glow": true
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"landmark": "nose",
|
| 124 |
+
"shape": "tiger_nose",
|
| 125 |
+
"color": [
|
| 126 |
+
35,
|
| 127 |
+
18,
|
| 128 |
+
12
|
| 129 |
+
],
|
| 130 |
+
"scale": 1.0,
|
| 131 |
+
"opacity": 1.0
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"landmark": "mouth",
|
| 135 |
+
"shape": "tiger_muzzle",
|
| 136 |
+
"color": [
|
| 137 |
+
245,
|
| 138 |
+
220,
|
| 139 |
+
190
|
| 140 |
+
],
|
| 141 |
+
"scale": 1.05,
|
| 142 |
+
"opacity": 0.9
|
| 143 |
+
},
|
| 144 |
+
{
|
| 145 |
+
"landmark": "mouth",
|
| 146 |
+
"shape": "tiger_mouth",
|
| 147 |
+
"color": [
|
| 148 |
+
25,
|
| 149 |
+
12,
|
| 150 |
+
10
|
| 151 |
+
],
|
| 152 |
+
"scale": 0.85,
|
| 153 |
+
"offset_y": 0.02
|
| 154 |
+
},
|
| 155 |
+
{
|
| 156 |
+
"landmark": "cheeks",
|
| 157 |
+
"shape": "whisker_dots",
|
| 158 |
+
"color": [
|
| 159 |
+
30,
|
| 160 |
+
20,
|
| 161 |
+
15
|
| 162 |
+
],
|
| 163 |
+
"scale": 0.9,
|
| 164 |
+
"opacity": 0.9
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"landmark": "left_cheek",
|
| 168 |
+
"shape": "whiskers",
|
| 169 |
+
"color": [
|
| 170 |
+
255,
|
| 171 |
+
245,
|
| 172 |
+
225
|
| 173 |
+
],
|
| 174 |
+
"scale": 1.0,
|
| 175 |
+
"rotation": -8
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"landmark": "right_cheek",
|
| 179 |
+
"shape": "whiskers",
|
| 180 |
+
"color": [
|
| 181 |
+
255,
|
| 182 |
+
245,
|
| 183 |
+
225
|
| 184 |
+
],
|
| 185 |
+
"scale": 1.0,
|
| 186 |
+
"rotation": 8
|
| 187 |
+
},
|
| 188 |
+
{
|
| 189 |
+
"landmark": "ears",
|
| 190 |
+
"shape": "tiger_ears",
|
| 191 |
+
"color": [
|
| 192 |
+
210,
|
| 193 |
+
100,
|
| 194 |
+
20
|
| 195 |
+
],
|
| 196 |
+
"scale": 1.15,
|
| 197 |
+
"opacity": 0.95
|
| 198 |
+
}
|
| 199 |
+
],
|
| 200 |
+
"effects": {
|
| 201 |
+
"face_lighting": true,
|
| 202 |
+
"fur_texture": true,
|
| 203 |
+
"stripe_blending": 0.85,
|
| 204 |
+
"eye_glow_intensity": 0.65,
|
| 205 |
+
"motion_smoothing": 0.9
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
}
|
saved_ar_filters/fa0c305e-44b6-459a-ae32-a1256dc2ecc2.json
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "fa0c305e-44b6-459a-ae32-a1256dc2ecc2",
|
| 3 |
+
"name": "tiger",
|
| 4 |
+
"filter": {
|
| 5 |
+
"name": "Royal Tiger",
|
| 6 |
+
"version": "2.0",
|
| 7 |
+
"type": "face_mask",
|
| 8 |
+
"tracking": {
|
| 9 |
+
"mode": "face_mesh",
|
| 10 |
+
"landmarks": 468,
|
| 11 |
+
"smooth_factor": 0.85,
|
| 12 |
+
"mirror": true
|
| 13 |
+
},
|
| 14 |
+
"elements": [
|
| 15 |
+
{
|
| 16 |
+
"landmark": "full_face",
|
| 17 |
+
"shape": "tiger_fur",
|
| 18 |
+
"color": [
|
| 19 |
+
230,
|
| 20 |
+
120,
|
| 21 |
+
20
|
| 22 |
+
],
|
| 23 |
+
"scale": 1.12,
|
| 24 |
+
"opacity": 0.92
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"landmark": "forehead",
|
| 28 |
+
"shape": "tiger_stripes",
|
| 29 |
+
"color": [
|
| 30 |
+
20,
|
| 31 |
+
12,
|
| 32 |
+
8
|
| 33 |
+
],
|
| 34 |
+
"scale": 1.05,
|
| 35 |
+
"opacity": 0.95,
|
| 36 |
+
"pattern": "vertical"
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"landmark": "left_temple",
|
| 40 |
+
"shape": "tiger_stripes",
|
| 41 |
+
"color": [
|
| 42 |
+
15,
|
| 43 |
+
10,
|
| 44 |
+
5
|
| 45 |
+
],
|
| 46 |
+
"scale": 0.9,
|
| 47 |
+
"rotation": -18
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"landmark": "right_temple",
|
| 51 |
+
"shape": "tiger_stripes",
|
| 52 |
+
"color": [
|
| 53 |
+
15,
|
| 54 |
+
10,
|
| 55 |
+
5
|
| 56 |
+
],
|
| 57 |
+
"scale": 0.9,
|
| 58 |
+
"rotation": 18
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"landmark": "left_cheek",
|
| 62 |
+
"shape": "tiger_stripes",
|
| 63 |
+
"color": [
|
| 64 |
+
20,
|
| 65 |
+
12,
|
| 66 |
+
6
|
| 67 |
+
],
|
| 68 |
+
"scale": 0.95,
|
| 69 |
+
"rotation": -25
|
| 70 |
+
},
|
| 71 |
+
{
|
| 72 |
+
"landmark": "right_cheek",
|
| 73 |
+
"shape": "tiger_stripes",
|
| 74 |
+
"color": [
|
| 75 |
+
20,
|
| 76 |
+
12,
|
| 77 |
+
6
|
| 78 |
+
],
|
| 79 |
+
"scale": 0.95,
|
| 80 |
+
"rotation": 25
|
| 81 |
+
},
|
| 82 |
+
{
|
| 83 |
+
"landmark": "forehead",
|
| 84 |
+
"shape": "tiger_crown",
|
| 85 |
+
"color": [
|
| 86 |
+
255,
|
| 87 |
+
215,
|
| 88 |
+
0
|
| 89 |
+
],
|
| 90 |
+
"scale": 0.85,
|
| 91 |
+
"offset_y": -0.18,
|
| 92 |
+
"glow": true
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"landmark": "eyes",
|
| 96 |
+
"shape": "tiger_eyes",
|
| 97 |
+
"color": [
|
| 98 |
+
255,
|
| 99 |
+
180,
|
| 100 |
+
30
|
| 101 |
+
],
|
| 102 |
+
"scale": 1.08,
|
| 103 |
+
"glow": true,
|
| 104 |
+
"pupil": "vertical_slit"
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"landmark": "eyes",
|
| 108 |
+
"shape": "cyan_eye_outline",
|
| 109 |
+
"color": [
|
| 110 |
+
0,
|
| 111 |
+
255,
|
| 112 |
+
255
|
| 113 |
+
],
|
| 114 |
+
"scale": 1.02,
|
| 115 |
+
"opacity": 0.7,
|
| 116 |
+
"glow": true
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"landmark": "nose",
|
| 120 |
+
"shape": "tiger_nose",
|
| 121 |
+
"color": [
|
| 122 |
+
35,
|
| 123 |
+
18,
|
| 124 |
+
12
|
| 125 |
+
],
|
| 126 |
+
"scale": 1.0,
|
| 127 |
+
"opacity": 1.0
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"landmark": "mouth",
|
| 131 |
+
"shape": "tiger_muzzle",
|
| 132 |
+
"color": [
|
| 133 |
+
245,
|
| 134 |
+
220,
|
| 135 |
+
190
|
| 136 |
+
],
|
| 137 |
+
"scale": 1.05,
|
| 138 |
+
"opacity": 0.9
|
| 139 |
+
},
|
| 140 |
+
{
|
| 141 |
+
"landmark": "mouth",
|
| 142 |
+
"shape": "tiger_mouth",
|
| 143 |
+
"color": [
|
| 144 |
+
25,
|
| 145 |
+
12,
|
| 146 |
+
10
|
| 147 |
+
],
|
| 148 |
+
"scale": 0.85,
|
| 149 |
+
"offset_y": 0.02
|
| 150 |
+
},
|
| 151 |
+
{
|
| 152 |
+
"landmark": "cheeks",
|
| 153 |
+
"shape": "whisker_dots",
|
| 154 |
+
"color": [
|
| 155 |
+
30,
|
| 156 |
+
20,
|
| 157 |
+
15
|
| 158 |
+
],
|
| 159 |
+
"scale": 0.9,
|
| 160 |
+
"opacity": 0.9
|
| 161 |
+
},
|
| 162 |
+
{
|
| 163 |
+
"landmark": "left_cheek",
|
| 164 |
+
"shape": "whiskers",
|
| 165 |
+
"color": [
|
| 166 |
+
255,
|
| 167 |
+
245,
|
| 168 |
+
225
|
| 169 |
+
],
|
| 170 |
+
"scale": 1.0,
|
| 171 |
+
"rotation": -8
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"landmark": "right_cheek",
|
| 175 |
+
"shape": "whiskers",
|
| 176 |
+
"color": [
|
| 177 |
+
255,
|
| 178 |
+
245,
|
| 179 |
+
225
|
| 180 |
+
],
|
| 181 |
+
"scale": 1.0,
|
| 182 |
+
"rotation": 8
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
"landmark": "ears",
|
| 186 |
+
"shape": "tiger_ears",
|
| 187 |
+
"color": [
|
| 188 |
+
210,
|
| 189 |
+
100,
|
| 190 |
+
20
|
| 191 |
+
],
|
| 192 |
+
"scale": 1.15,
|
| 193 |
+
"opacity": 0.95
|
| 194 |
+
}
|
| 195 |
+
],
|
| 196 |
+
"effects": {
|
| 197 |
+
"face_lighting": true,
|
| 198 |
+
"fur_texture": true,
|
| 199 |
+
"stripe_blending": 0.85,
|
| 200 |
+
"eye_glow_intensity": 0.65,
|
| 201 |
+
"motion_smoothing": 0.9
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
}
|
saved_filters/filters_registry.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{}
|
tests/__pycache__/test_cv_ops.cpython-312-pytest-8.4.2.pyc
ADDED
|
Binary file (19.2 kB). View file
|
|
|
tests/__pycache__/test_developer_api.cpython-312-pytest-8.4.2.pyc
ADDED
|
Binary file (8.83 kB). View file
|
|
|
tests/test_cv_ops.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
from cv_ops.morphology import apply_morphology
|
| 4 |
+
from cv_ops.transforms import reflect, rotate, scale_image, translate
|
| 5 |
+
from filters.builtin import BUILTIN_FILTERS
|
| 6 |
+
from filters.registry import apply_definition
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def sample_image():
|
| 10 |
+
img = np.zeros((20, 30, 3), dtype=np.uint8)
|
| 11 |
+
img[5:15, 10:20] = [200, 100, 50]
|
| 12 |
+
return img
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_builtin_filter_shapes():
|
| 16 |
+
img = sample_image()
|
| 17 |
+
for name in ["Grayscale", "Sepia", "Invert", "Blur", "Sharpen", "Edge Detection"]:
|
| 18 |
+
out = BUILTIN_FILTERS[name](img)
|
| 19 |
+
assert out.shape == img.shape
|
| 20 |
+
assert out.dtype == np.uint8
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def test_transforms_return_matrices():
|
| 24 |
+
img = sample_image()
|
| 25 |
+
out, matrix = translate(img, 2, 3)
|
| 26 |
+
assert out.shape == img.shape
|
| 27 |
+
assert matrix == [[1.0, 0.0, 2.0], [0.0, 1.0, 3.0]]
|
| 28 |
+
assert rotate(img, 15, expand=False)[0].shape == img.shape
|
| 29 |
+
assert scale_image(img, 2, 2)[0].shape[:2] == (40, 60)
|
| 30 |
+
assert reflect(img, "both")[0].shape == img.shape
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def test_morphology_binary_output():
|
| 34 |
+
out, kernel = apply_morphology(sample_image(), "Opening", size=3)
|
| 35 |
+
assert out.shape == sample_image().shape
|
| 36 |
+
assert kernel.shape == (3, 3)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def test_pipeline_definition():
|
| 40 |
+
img = sample_image()
|
| 41 |
+
definition = {"type": "pipeline", "steps": [{"operation": "Invert", "params": {}}, {"operation": "Grayscale", "params": {}}]}
|
| 42 |
+
out = apply_definition(img, definition)
|
| 43 |
+
assert out.shape == img.shape
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def test_export_image():
|
| 47 |
+
from os.path import exists
|
| 48 |
+
from app import export_image
|
| 49 |
+
|
| 50 |
+
assert export_image(None) is None
|
| 51 |
+
|
| 52 |
+
img = sample_image()
|
| 53 |
+
filepath = export_image(img)
|
| 54 |
+
assert filepath is not None
|
| 55 |
+
assert isinstance(filepath, str)
|
| 56 |
+
assert exists(filepath)
|
| 57 |
+
assert filepath.endswith(".png")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def test_face_filters():
|
| 61 |
+
from models.face_filters import BUILTIN_AR_FILTERS, apply_face_filter
|
| 62 |
+
|
| 63 |
+
img = np.zeros((100, 100, 3), dtype=np.uint8)
|
| 64 |
+
for filter_name in BUILTIN_AR_FILTERS:
|
| 65 |
+
res, msg = apply_face_filter(img, filter_name)
|
| 66 |
+
assert res.shape == img.shape
|
| 67 |
+
assert res.dtype == np.uint8
|
| 68 |
+
assert isinstance(msg, str)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def test_custom_ar_filters():
|
| 72 |
+
from models.face_filters import apply_face_filter, ar_filter_names, delete_ar_filter, save_ar_filter
|
| 73 |
+
|
| 74 |
+
custom_def = {
|
| 75 |
+
"elements": [
|
| 76 |
+
{"landmark": "forehead", "shape": "crown", "color": [255, 215, 0], "scale": 1.0},
|
| 77 |
+
{"landmark": "eyes", "shape": "visor", "color": [0, 255, 255], "scale": 1.0},
|
| 78 |
+
]
|
| 79 |
+
}
|
| 80 |
+
saved = save_ar_filter("Test Crown Visor", custom_def)
|
| 81 |
+
assert saved["name"] == "Test Crown Visor"
|
| 82 |
+
assert "Test Crown Visor" in ar_filter_names(True)
|
| 83 |
+
|
| 84 |
+
img = np.zeros((100, 100, 3), dtype=np.uint8)
|
| 85 |
+
res, msg = apply_face_filter(img, "Test Crown Visor")
|
| 86 |
+
assert res.shape == img.shape
|
| 87 |
+
|
| 88 |
+
delete_ar_filter("Test Crown Visor")
|
| 89 |
+
assert "Test Crown Visor" not in ar_filter_names(False)
|
| 90 |
+
|
tests/test_developer_api.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tempfile
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import numpy as np
|
| 4 |
+
from developer_api import load_image, process_image, save_image, generate_python_snippet
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def sample_img():
|
| 8 |
+
arr = np.zeros((40, 50, 3), dtype=np.uint8)
|
| 9 |
+
arr[10:30, 10:30] = [100, 150, 200]
|
| 10 |
+
return arr
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def test_developer_api_process_image():
|
| 14 |
+
img = sample_img()
|
| 15 |
+
out, meta = process_image(img, "filter", operation_name="Sepia")
|
| 16 |
+
assert out.shape == img.shape
|
| 17 |
+
assert meta["operation"] == "Sepia"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def test_developer_api_transform_and_morph():
|
| 21 |
+
img = sample_img()
|
| 22 |
+
out_t, meta_t = process_image(img, "transform", operation_name="Rotation", params={"angle": 15})
|
| 23 |
+
assert out_t.ndim == 3
|
| 24 |
+
assert out_t.shape[2] == 3
|
| 25 |
+
|
| 26 |
+
out_m, meta_m = process_image(img, "morphology", operation_name="Opening", params={"size": 3})
|
| 27 |
+
assert out_m.shape == img.shape
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def test_developer_api_ar():
|
| 32 |
+
img = sample_img()
|
| 33 |
+
out_ar, meta_ar = process_image(img, "ar", operation_name="Glasses")
|
| 34 |
+
assert out_ar.shape == img.shape
|
| 35 |
+
assert "status" in meta_ar
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def test_generate_python_snippet():
|
| 39 |
+
snippet = generate_python_snippet("Sepia", {"contrast": 1.2})
|
| 40 |
+
assert "apply_step" in snippet
|
| 41 |
+
assert "Sepia" in snippet
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_save_and_load_image():
|
| 45 |
+
img = sample_img()
|
| 46 |
+
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
|
| 47 |
+
tmp_path = f.name
|
| 48 |
+
save_image(img, tmp_path)
|
| 49 |
+
loaded = load_image(tmp_path)
|
| 50 |
+
assert loaded.shape == img.shape
|
| 51 |
+
Path(tmp_path).unlink(missing_ok=True)
|