Aditya7864 commited on
Commit
8096125
·
1 Parent(s): b0c1552

Add application file

Browse files
Files changed (49) hide show
  1. README.md +122 -12
  2. __pycache__/app.cpython-312.pyc +0 -0
  3. __pycache__/conftest.cpython-312-pytest-8.4.2.pyc +0 -0
  4. __pycache__/developer_api.cpython-312.pyc +0 -0
  5. app.py +471 -0
  6. assets/sample_cells.png +0 -0
  7. assets/sample_grid.png +0 -0
  8. assets/sample_portrait_placeholder.png +0 -0
  9. assets/style_texture_1.png +0 -0
  10. assets/style_texture_2.png +0 -0
  11. assets/style_texture_3.png +0 -0
  12. batch/__init__.py +0 -0
  13. batch/__pycache__/__init__.cpython-312.pyc +0 -0
  14. batch/__pycache__/dataset_processor.cpython-312.pyc +0 -0
  15. batch/dataset_processor.py +61 -0
  16. cli.py +98 -0
  17. conftest.py +6 -0
  18. cv_ops/__init__.py +0 -0
  19. cv_ops/__pycache__/__init__.cpython-312.pyc +0 -0
  20. cv_ops/__pycache__/analysis.cpython-312.pyc +0 -0
  21. cv_ops/__pycache__/morphology.cpython-312.pyc +0 -0
  22. cv_ops/__pycache__/transforms.cpython-312.pyc +0 -0
  23. cv_ops/analysis.py +44 -0
  24. cv_ops/morphology.py +49 -0
  25. cv_ops/transforms.py +46 -0
  26. developer_api.py +102 -0
  27. filters/__init__.py +0 -0
  28. filters/__pycache__/__init__.cpython-312.pyc +0 -0
  29. filters/__pycache__/builtin.cpython-312.pyc +0 -0
  30. filters/__pycache__/custom.cpython-312.pyc +0 -0
  31. filters/__pycache__/registry.cpython-312.pyc +0 -0
  32. filters/builtin.py +137 -0
  33. filters/custom.py +37 -0
  34. filters/registry.py +98 -0
  35. models/__init__.py +0 -0
  36. models/__pycache__/__init__.cpython-312.pyc +0 -0
  37. models/__pycache__/face_filters.cpython-312.pyc +0 -0
  38. models/__pycache__/style_transfer.cpython-312.pyc +0 -0
  39. models/face_filters.py +352 -0
  40. models/style_transfer.py +41 -0
  41. pytest.ini +4 -0
  42. requirements.txt +10 -0
  43. saved_ar_filters/ar_filters_registry.json +210 -0
  44. saved_ar_filters/fa0c305e-44b6-459a-ae32-a1256dc2ecc2.json +204 -0
  45. saved_filters/filters_registry.json +1 -0
  46. tests/__pycache__/test_cv_ops.cpython-312-pytest-8.4.2.pyc +0 -0
  47. tests/__pycache__/test_developer_api.cpython-312-pytest-8.4.2.pyc +0 -0
  48. tests/test_cv_ops.py +90 -0
  49. tests/test_developer_api.py +51 -0
README.md CHANGED
@@ -1,15 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: AI Lab CAM
3
- emoji: 😻
4
- colorFrom: indigo
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 6.23.1
8
- python_version: '3.12'
9
- app_file: app.py
10
- pinned: false
11
- license: apache-2.0
12
- short_description: AI Lab CAM
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  ---
14
 
15
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
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)