mcp-relight-harmonize 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/mcp-relight-harmonize/SKILL.md +47 -35
- package/.agents/skills/mcp-relight-harmonize/references/diffusion_guide.md +10 -8
- package/.agents/skills/mcp-relight-harmonize/scripts/extract_layers.py +412 -0
- package/MASTER.md +139 -83
- package/README.md +24 -10
- package/dist/contracts/schemas.js +2 -2
- package/dist/contracts/types.d.ts +2 -2
- package/dist/domain/layer_extractor.d.ts +42 -0
- package/dist/domain/layer_extractor.js +352 -0
- package/dist/domain/prompt_synthesizer.js +10 -6
- package/dist/index.js +1 -1
- package/dist/resources/presets.js +2 -2
- package/dist/server.js +35 -20
- package/dist/tools/analyze_optical.d.ts +1 -1
- package/dist/tools/analyze_optical.js +25 -6
- package/dist/tools/generate_relight.js +1 -1
- package/dist/tools/harmonize.js +1 -1
- package/dist/tools/synthesize_prompt.js +1 -1
- package/docs/architecture.md +2 -2
- package/package.json +3 -3
|
@@ -1,40 +1,60 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp-relight-harmonize
|
|
3
3
|
description: >-
|
|
4
|
-
Execute end-to-end optical image profiling, physically-grounded relighting variations, contact-aware composite harmonization,
|
|
5
|
-
and precision diffusion prompt synthesis
|
|
4
|
+
Execute end-to-end 6-layer optical image profiling, physically-grounded relighting variations, contact-aware composite harmonization,
|
|
5
|
+
and precision diffusion prompt synthesis compatible with Any Image Generator Model (optimized for GEMINI Nano Banana in Antigravity). Use when inspecting lighting angles or color temperature,
|
|
6
6
|
generating alternative illumination schemes (Ambient, Dramatic, Rim, Mood), blending foreground cutouts into backgrounds,
|
|
7
7
|
or drafting photorealistic prompts.
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# `mcp-relight-harmonize` Skill Specification
|
|
11
11
|
|
|
12
|
-
Created by **MarwanDevSpace**, this skill empowers the agent to execute physical lighting analysis, generate relighted variations, perform seamless composite harmonization, and craft production diffusion prompts tailored for **
|
|
12
|
+
Created by **MarwanDevSpace**, this skill empowers the agent to execute physical lighting analysis, decompose images into 6 visual layers in `Layers/`, generate relighted variations, perform seamless composite harmonization, and craft production diffusion prompts tailored for **Any Image Generator Model** (specifically optimized for **GEMINI Nano Banana** in **Google Antigravity**).
|
|
13
|
+
|
|
14
|
+
> [!NOTE]
|
|
15
|
+
> **Antigravity Recommended**: This skill is optimized to work with **any Image Generator**, and is **strongly preferred inside Google Antigravity** where native desktop image generation (`generate_image`) with **GEMINI Nano Banana** can immediately execute the desired modifications.
|
|
13
16
|
|
|
14
17
|
---
|
|
15
18
|
|
|
16
19
|
## 1. When to Activate This Skill
|
|
17
20
|
|
|
18
21
|
Activate this skill whenever the user asks to:
|
|
19
|
-
1. **Analyze an image's lighting:**
|
|
20
|
-
2. **Relight an image:** Produce
|
|
22
|
+
1. **Decompose & Analyze an image's lighting:** Generate 6 analytical layers into `Layers/` and determine CCT (Kelvin), lighting direction, azimuth/elevation angles, contrast ratio, or surface roughness.
|
|
23
|
+
2. **Relight an image:** Produce physically-grounded lighting variants (`Ambient`, `Dramatic`, `Rim`, `Mood`).
|
|
21
24
|
3. **Composite/Harmonize an image:** Place a cutout foreground onto a background scene, matching ambient color statistics and adding physically grounded contact shadows.
|
|
22
|
-
4. **Generate `/prompt` for diffusion:** Create exact physical lighting and detail enhancement prompts for **
|
|
25
|
+
4. **Generate `/prompt` for diffusion:** Create exact physical lighting and detail enhancement prompts for **Any Image Generator** / **GEMINI Nano Banana** with calibrated denoising strength (`0.35 - 0.45`).
|
|
23
26
|
|
|
24
27
|
---
|
|
25
28
|
|
|
26
29
|
## 2. Core Workflows & Execution Procedures
|
|
27
30
|
|
|
28
|
-
### Workflow A: Optical Profiling
|
|
31
|
+
### Workflow A: Optical Profiling & 6-Layer Decomposition
|
|
29
32
|
Before modifying or inpainting an image, extract its optical geometry:
|
|
30
|
-
1. Call tool `analyze_optical_profile({ image_path: "<path_to_image>" })
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
-
|
|
33
|
+
1. Call tool `analyze_optical_profile({ image_path: "<path_to_image>", extract_layers: true })` or run:
|
|
34
|
+
`python scripts/extract_layers.py --image <path> [--intent "<intent>"]`
|
|
35
|
+
2. Python decomposes the image into 6 visual layers inside `Layers/`:
|
|
36
|
+
- `01_highlights.png` (Highlights / Specular Zones)
|
|
37
|
+
- `02_shadows.png` (Shadows / Low-Key Zones)
|
|
38
|
+
- `03_ambient_occlusion.png` (Deep Ambient Occlusion & Ground Contact)
|
|
39
|
+
- `04_edges.png` (Sobel High-Frequency Contours)
|
|
40
|
+
- `05_depth_normals.png` (3D Surface Normal Gradient Field)
|
|
41
|
+
- `06_chroma_saturation.png` (Chrominance & Saturation Distribution)
|
|
42
|
+
3. Review the resulting `OpticalProfileReport` and dynamic `Layer.md`:
|
|
43
|
+
- **`colorTemperatureKelvin`**: e.g., 3200K tungsten vs 5500K daylight.
|
|
44
|
+
- **`lightingAngles`**: `azimuthDeg` and `elevationDeg`.
|
|
34
45
|
- **`luminanceDynamics.contrastRatio`**: Key-to-fill ratio.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
- **`aoCoveragePct`**: Contact shadow presence vs floating subject.
|
|
47
|
+
4. Store findings in memory / CoT to guide subsequent generation.
|
|
48
|
+
|
|
49
|
+
### Workflow B: Interactive Decision & Direct Image Generation
|
|
50
|
+
1. Address the user with the mandatory prompt:
|
|
51
|
+
**"ماذا تريد من تعديل؟"**
|
|
52
|
+
accompanied by tailored recommendations derived strictly from the image's detected optical profile.
|
|
53
|
+
2. Upon user selection or intent:
|
|
54
|
+
- Synthesize the photorealistic prompt for the **Image Generator**.
|
|
55
|
+
- **Inside Google Antigravity**: Directly trigger `generate_image` using the synthesized prompt to produce the modified image on the spot!
|
|
56
|
+
|
|
57
|
+
### Workflow C: Physical Relighting Variations
|
|
38
58
|
To produce visual lighting alternatives on disk:
|
|
39
59
|
1. Call tool `generate_relight_variations({ image_path: "<path_to_image>", target_lighting: "All" })`.
|
|
40
60
|
2. The tool produces 4 files in `OUTPUT_CACHE_DIR`:
|
|
@@ -42,44 +62,36 @@ To produce visual lighting alternatives on disk:
|
|
|
42
62
|
- `*_relight_dramatic.png` (-1.5 EV shadow crush, top-left directional chiaroscuro).
|
|
43
63
|
- `*_relight_rim.png` (+1.2 EV high-pass edge halo, cool cyan perimeter).
|
|
44
64
|
- `*_relight_mood.png` (3200K tungsten amber shift, highlight bloom diffusion).
|
|
45
|
-
3. Present the resulting artifact paths to the user with their mathematical adjustment logs.
|
|
46
65
|
|
|
47
|
-
### Workflow
|
|
66
|
+
### Workflow D: Composite Harmonization
|
|
48
67
|
To blend a subject or product into a new background:
|
|
49
68
|
1. Call tool `harmonize_composite({ foreground_path: "<fg>", background_path: "<bg>", blend_mode: "seamless" })`.
|
|
50
|
-
2. The engine executes
|
|
51
|
-
- **Reinhard Color Statistics Transfer:** Normalizes foreground means and deviations to match background in Ruderman $l\alpha\beta$ space.
|
|
52
|
-
- **Contact Shadow Synthesis:** Projects an elliptical Gaussian shadow beneath the subject's lowest contact plane.
|
|
53
|
-
- **Smooth Blending:** Eliminates boundary halo artifacts.
|
|
54
|
-
3. Return the composite image file path.
|
|
69
|
+
2. The engine executes Reinhard Color Statistics Transfer in Ruderman $l\alpha\beta$ space, applies Ground Contact Shadows, and smooths boundaries.
|
|
55
70
|
|
|
56
|
-
### Workflow
|
|
71
|
+
### Workflow E: Diffusion Prompt Synthesis (`/prompt`)
|
|
57
72
|
To craft targeted generative prompts:
|
|
58
|
-
1. Call tool `synthesize_diffusion_prompt({ image_path: "<path>", user_intent: "<intent>", target_model: "
|
|
59
|
-
2. Extract the
|
|
60
|
-
- **`enhancementPrompt`**:
|
|
61
|
-
- **`relightingPrompt`**:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
### Workflow E: Output Cache Inspection
|
|
66
|
-
To inspect or discover previously generated relight variations or composite outputs:
|
|
67
|
-
1. Call tool `list_cached_variations({ cache_dir?: "<path>" })`.
|
|
68
|
-
2. Inspect `files` list containing filename, full path, byte size, and last modified timestamp for all output artifacts.
|
|
73
|
+
1. Call tool `synthesize_diffusion_prompt({ image_path: "<path>", user_intent: "<intent>", target_model: "universal" | "nano_banana" })`.
|
|
74
|
+
2. Extract the specialized prompts:
|
|
75
|
+
- **`enhancementPrompt`**: Micro-surface textures, pore fidelity, lens sharpness, and subsurface scattering.
|
|
76
|
+
- **`relightingPrompt`**: Exact lighting angles, Kelvin temperature, volumetric dust rays, and contact shadows.
|
|
77
|
+
|
|
78
|
+
### Workflow F: Output Cache Inspection
|
|
79
|
+
1. Call tool `list_cached_variations({ cache_dir?: "<path>" })` to inspect generated artifacts.
|
|
69
80
|
|
|
70
81
|
---
|
|
71
82
|
|
|
72
83
|
## 3. Reference Documentation
|
|
73
84
|
|
|
74
|
-
For in-depth mathematical formulations and model guides:
|
|
75
85
|
- [Optical Mathematics & Formulations](./references/optical_math.md)
|
|
76
86
|
- [Tool Orchestration & Chaining Standards](./references/tool_orchestration.md)
|
|
77
|
-
- [
|
|
87
|
+
- [Image Generator & GEMINI Nano Banana Guide](./references/diffusion_guide.md)
|
|
78
88
|
|
|
79
89
|
---
|
|
80
90
|
|
|
81
91
|
## 4. Executable Helper Scripts
|
|
82
92
|
|
|
93
|
+
- Execute 6-layer optical extraction:
|
|
94
|
+
`python scripts/extract_layers.py --image <image_path> [--intent "<intent>"]`
|
|
83
95
|
- Execute full end-to-end analysis & prompt synthesis from CLI:
|
|
84
96
|
`node .agents/skills/mcp-relight-harmonize/scripts/run_pipeline.js <image_path>`
|
|
85
97
|
- Verify server health and tool registration:
|
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Image Generator Prompt Engineering Guide: Universal & GEMINI Nano Banana
|
|
2
2
|
|
|
3
|
-
This document maps physical optical metrics into specialized prompt directives
|
|
3
|
+
This document maps physical optical metrics into specialized prompt directives compatible with **Any Image Generator Model**, with specific optimization for **GEMINI Nano Banana** in **Google Antigravity**.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## 1. Target Engine
|
|
7
|
+
## 1. Target Engine Paradigms
|
|
8
8
|
|
|
9
|
-
| Parameter |
|
|
9
|
+
| Parameter | Universal Image Generator (Natural Studio) | GEMINI Nano Banana (Optical Shaders) |
|
|
10
10
|
|---|---|---|
|
|
11
|
-
| **Prompt Paradigm** | Natural language, photographic directives | Dense tokenized optical descriptors |
|
|
11
|
+
| **Prompt Paradigm** | Natural language, photographic studio directives | Dense tokenized optical descriptors & shaders |
|
|
12
12
|
| **Optics Formulation** | 85mm prime lens f/2.0, authentic falloff | Micro-surface specular roughness, normal angle |
|
|
13
13
|
| **Contact Shadows** | "physically-based contact shadows at base" | "ground contact shadow, ambient occlusion caster" |
|
|
14
14
|
| **Volumetrics** | "subtle volumetric dust rays in air" | "volumetric raytraced bounce" |
|
|
15
15
|
| **Denoising Default** | `0.38` | `0.38` (subtle) to `0.42` (dramatic) |
|
|
16
|
+
| **Recommended Environment** | Any diffusion generator | **Google Antigravity (`generate_image`)** |
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
19
|
-
## 2.
|
|
20
|
+
## 2. Universal Image Generator Prompt Architecture
|
|
20
21
|
|
|
21
22
|
### Enhancement Prompt Template:
|
|
22
23
|
```text
|
|
@@ -34,7 +35,7 @@ authentic photometric falloff, [User Intent].
|
|
|
34
35
|
|
|
35
36
|
---
|
|
36
37
|
|
|
37
|
-
## 3. Nano Banana Prompt Architecture
|
|
38
|
+
## 3. GEMINI Nano Banana Prompt Architecture
|
|
38
39
|
|
|
39
40
|
### Enhancement Prompt Template:
|
|
40
41
|
```text
|
|
@@ -50,7 +51,8 @@ denoising 0.38, light_azimuth_[Azimuth]deg, [User Intent]
|
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
### Recommended Parameters:
|
|
53
|
-
- `model`: `"nano-banana
|
|
54
|
+
- `model`: `"gemini-nano-banana"`
|
|
55
|
+
- `target_environment`: `"Google Antigravity"`
|
|
54
56
|
- `denoising_strength`: `0.38`
|
|
55
57
|
- `guidance_scale`: `4.5`
|
|
56
58
|
- `steps`: `32`
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MarwanDevSpace - Dynamic 6-Layer Optical Decomposition & Intelligence Engine
|
|
4
|
+
Extracts 6 analytical visual layers into Layers/ directory:
|
|
5
|
+
1. 01_highlights.png - طبقة الألوان الفاتحة (Highlights / Specular Zones)
|
|
6
|
+
2. 02_shadows.png - طبقة الألوان الغامقة (Shadows / Low-Key Zones)
|
|
7
|
+
3. 03_ambient_occlusion.png - طبقة الظل العالي (Deep Ambient Occlusion & Ground Contact)
|
|
8
|
+
4. 04_edges.png - طبقة الحواف (Sobel High-Frequency Contours)
|
|
9
|
+
5. 05_depth_normals.png - طبقة العمق (3D Surface Normal Gradient Field)
|
|
10
|
+
6. 06_chroma_saturation.png - طبقة الألوان والتشبع والبكسلات (Chroma & Saturation Distribution)
|
|
11
|
+
|
|
12
|
+
Generates dynamic, on-demand Layer.md reports and physical generative prompts
|
|
13
|
+
specifically targeting Universal Image Generator and Nano Banana without static boilerplate.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import sys
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
from typing import Optional, Dict, Any, List
|
|
21
|
+
import numpy as np
|
|
22
|
+
from PIL import Image
|
|
23
|
+
|
|
24
|
+
def calculate_cct(r_mean: float, g_mean: float, b_mean: float) -> float:
|
|
25
|
+
"""Calculates Correlated Color Temperature (Kelvin) using CIE 1931 xy and McCamy formulation."""
|
|
26
|
+
r_norm = min(max(r_mean / 255.0, 0.0), 1.0)
|
|
27
|
+
g_norm = min(max(g_mean / 255.0, 0.0), 1.0)
|
|
28
|
+
b_norm = min(max(b_mean / 255.0, 0.0), 1.0)
|
|
29
|
+
|
|
30
|
+
# Gamma linearization (sRGB -> linear RGB)
|
|
31
|
+
r_lin = ((r_norm + 0.055) / 1.055) ** 2.4 if r_norm > 0.04045 else r_norm / 12.92
|
|
32
|
+
g_lin = ((g_norm + 0.055) / 1.055) ** 2.4 if g_norm > 0.04045 else g_norm / 12.92
|
|
33
|
+
b_lin = ((b_norm + 0.055) / 1.055) ** 2.4 if b_norm > 0.04045 else b_norm / 12.92
|
|
34
|
+
|
|
35
|
+
# CIE XYZ matrix transformation
|
|
36
|
+
X = 0.4124564 * r_lin + 0.3575761 * g_lin + 0.1804375 * b_lin
|
|
37
|
+
Y = 0.2126729 * r_lin + 0.7151522 * g_lin + 0.0721750 * b_lin
|
|
38
|
+
Z = 0.0193339 * r_lin + 0.1191920 * g_lin + 0.9503041 * b_lin
|
|
39
|
+
|
|
40
|
+
total = X + Y + Z
|
|
41
|
+
if total <= 1e-7:
|
|
42
|
+
return 6500.0
|
|
43
|
+
|
|
44
|
+
x = X / total
|
|
45
|
+
y = Y / total
|
|
46
|
+
denom = 0.1858 - y
|
|
47
|
+
if abs(denom) < 1e-6:
|
|
48
|
+
denom = 1e-6 if denom >= 0 else -1e-6
|
|
49
|
+
|
|
50
|
+
n = (x - 0.3320) / denom
|
|
51
|
+
cct = 449.0 * (n ** 3) + 3525.0 * (n ** 2) + 6823.3 * n + 5520.33
|
|
52
|
+
return float(np.clip(cct, 1500.0, 20000.0))
|
|
53
|
+
|
|
54
|
+
def build_dynamic_diagnostics(
|
|
55
|
+
cct_kelvin: float,
|
|
56
|
+
azimuth_deg: float,
|
|
57
|
+
elevation_deg: float,
|
|
58
|
+
hl_coverage_pct: float,
|
|
59
|
+
shadow_coverage_pct: float,
|
|
60
|
+
ao_coverage_pct: float,
|
|
61
|
+
mean_gradient: float,
|
|
62
|
+
mean_saturation: float,
|
|
63
|
+
user_intent: Optional[str] = None
|
|
64
|
+
) -> Dict[str, Any]:
|
|
65
|
+
"""Dynamically analyzes layer metrics to produce targeted findings, customized suggestions, and diffusion prompts."""
|
|
66
|
+
findings: List[str] = []
|
|
67
|
+
tailored_options: List[str] = []
|
|
68
|
+
|
|
69
|
+
# 1. Thermal & Chromatic Diagnostics
|
|
70
|
+
if cct_kelvin < 3800:
|
|
71
|
+
thermal_desc = f"طيف لوني دافئ جداً ({round(cct_kelvin)}K - Tungsten/Golden Hour)"
|
|
72
|
+
findings.append(f"🔥 **انحياز طيفي دافئ**: الصورة مسجلة بحرارة `{round(cct_kelvin)}K` مع غلبة للأطياف البرتقالية والصفراء في القنوات اللونية.")
|
|
73
|
+
tailored_options.append(f"موازنة حرارة الألوان من `{round(cct_kelvin)}K` إلى 5500K نهارية محايدة لإزالة المسحة الكهرمانية.")
|
|
74
|
+
elif cct_kelvin > 6500:
|
|
75
|
+
thermal_desc = f"طيف لوني بارد ({round(cct_kelvin)}K - Cool Atmospheric Skylight)"
|
|
76
|
+
findings.append(f"❄️ **انحياز طيفي بارد**: الصورة مسجلة بحرارة `{round(cct_kelvin)}K` مع غلبة للزرقة والارتداد السماوي البارد.")
|
|
77
|
+
tailored_options.append(f"تدفئة المشهد الضوئي برفع حرارة الألوان إلى 3200K (Mood Tungsten) أو 4500K سينمائي.")
|
|
78
|
+
else:
|
|
79
|
+
thermal_desc = f"طيف نهاري متوازن ({round(cct_kelvin)}K - Balanced Daylight)"
|
|
80
|
+
findings.append(f"☀️ **حرارة ألوان متوازنة**: حرارة الألوان عند `{round(cct_kelvin)}K` تماثل الإضاءة الاستوديوية المحايدة.")
|
|
81
|
+
|
|
82
|
+
# 2. Lighting Angles & Dynamic Contrast
|
|
83
|
+
light_dir = "يمين" if 0 <= azimuth_deg < 90 or 270 <= azimuth_deg <= 360 else "يسار"
|
|
84
|
+
light_vert = "علوية" if elevation_deg > 50 else "أفقية جانبية"
|
|
85
|
+
findings.append(f"💡 **توجيه الإضاءة الرئيسية**: زاوية السمت `{round(azimuth_deg)}°` (إضاءة من جهة ال{light_dir}) مع زاوية ارتفاع `{round(elevation_deg)}°` ({light_vert}).")
|
|
86
|
+
|
|
87
|
+
if shadow_coverage_pct > 35.0 and hl_coverage_pct > 15.0:
|
|
88
|
+
findings.append(f"⚡ **تباين درامي عالي (Chiaroscuro)**: كثافة الظلال `{round(shadow_coverage_pct, 1)}%` مع سعة مناطق ساطعة `{round(hl_coverage_pct, 1)}%` تدل على تباين قوي بين المفتاح الضوئي والملء.")
|
|
89
|
+
tailored_options.append("تنعيم التباين ورفع تفاصيل الظلال المغلقة عبر وضع الإضاءة المحيطية (Ambient Fill +0.8 EV).")
|
|
90
|
+
elif shadow_coverage_pct < 12.0 and hl_coverage_pct < 12.0:
|
|
91
|
+
findings.append("🌫️ **إضاءة منبسطة ناعمة (Flat Diffuse)**: تباين منخفض وغياب للمناطق الساطعة الحادة، ما يعطي مظهراً هادئاً.")
|
|
92
|
+
tailored_options.append("إضافة عمق درامي وزيادة التباين بنمط Chiaroscuro عالي التحديد مع إضاءة اتجاهية بارزة.")
|
|
93
|
+
|
|
94
|
+
# 3. Grounding & Ambient Occlusion
|
|
95
|
+
if ao_coverage_pct < 2.0:
|
|
96
|
+
findings.append(f"⚠️ **فقدان الارتكاز الأرضي (Lack of Grounding)**: نسبة الظلال التلامسية العميقة `{round(ao_coverage_pct, 2)}%` تكاد تنعدم، مما قد يظهر العنصر كأنه طافٍ في الفراغ.")
|
|
97
|
+
tailored_options.append(f"بناء وتثبيت ظل تلامسي أرضي فيزيائي (Contact Shadow Footprint) أسفل أدنى نقطة ارتكاز.")
|
|
98
|
+
else:
|
|
99
|
+
findings.append(f"⚓ **ارتكاز أرضي متماسك**: نسبة ظلال التلامس والانغلاق الموضعي `{round(ao_coverage_pct, 1)}%` تؤمن التصاقاً بصرياً طبيعياً بالأرضية.")
|
|
100
|
+
|
|
101
|
+
# 4. Surface Micro-textures & Edges
|
|
102
|
+
if mean_gradient > 0.03:
|
|
103
|
+
findings.append(f"🔍 **تفاصيل سطحية دقيقة وحواف حادة**: متوسط تباين سوبل `{round(mean_gradient, 4)}` يشير إلى وفرة في التفاصيل المجهرية والأنسجة السطحية الواضحة.")
|
|
104
|
+
else:
|
|
105
|
+
findings.append(f"🎨 **حواف ناعمة وسطح انسيابي**: متوسط تدرج سوبل `{round(mean_gradient, 4)}` يشير إلى مساحات ناعمة متصلة ومناسبة للتنعيم وإعادة التوزيع.")
|
|
106
|
+
|
|
107
|
+
# 5. Chroma / Saturation
|
|
108
|
+
if mean_saturation > 0.40:
|
|
109
|
+
findings.append(f"🌈 **تشبع لوني مكثف**: متوسط التشبع `{round(mean_saturation, 3)}` يظهر كثافة لونية حيوية في البكسلات.")
|
|
110
|
+
else:
|
|
111
|
+
findings.append(f"🔘 **لوحة لونية هادئة أو معتدلة**: متوسط التشبع `{round(mean_saturation, 3)}` يعكس تدرجات رصينة غير مبالغ بها.")
|
|
112
|
+
|
|
113
|
+
# Add custom intent option if provided
|
|
114
|
+
if user_intent:
|
|
115
|
+
tailored_options.insert(0, f"تطبيق طلبك المخصص فوراً: \"{user_intent}\" بتطابق فيزيائي دقيق مع بيانات الطبقات الست.")
|
|
116
|
+
else:
|
|
117
|
+
tailored_options.append("إعادة إضاءة بنمط هالة الحواف (Rim Light Halo +1.2 EV) لإبراز حدود المجسم وعزله عن الخلفية.")
|
|
118
|
+
tailored_options.append("دمج العنصر في خلفية جديدة مع مطابقة إحصائيات الألوان ودرجة الحرارة وظلال الارتكاز.")
|
|
119
|
+
|
|
120
|
+
# Dynamic Generative Prompts tailored to exact metrics
|
|
121
|
+
intent_clause = f", {user_intent}" if user_intent else ""
|
|
122
|
+
universal_prompt = (
|
|
123
|
+
f"A master-quality studio photograph{intent_clause}, calibrated optical lighting at {round(azimuth_deg)}° azimuth "
|
|
124
|
+
f"and {round(elevation_deg)}° elevation, authentic {round(cct_kelvin)}K color temperature balance, "
|
|
125
|
+
f"physically-grounded ambient occlusion contact shadows firmly anchoring the base plane, "
|
|
126
|
+
f"crisp micro-surface geometry (Sobel roughness index {round(mean_gradient, 3)}), "
|
|
127
|
+
f"smooth luminance falloff and authentic subsurface scattering, 85mm prime lens f/2.0."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
nano_banana_prompt = (
|
|
131
|
+
f"optics relight, cct {round(cct_kelvin)}K, light vector azimuth {round(azimuth_deg)} deg elevation {round(elevation_deg)} deg, "
|
|
132
|
+
f"surface roughness {round(mean_gradient, 4)}, saturation index {round(mean_saturation, 3)}, "
|
|
133
|
+
f"ground contact occlusion caster, volumetric photon bounce, high-key rim highlight accents, "
|
|
134
|
+
f"denoising strength 0.38{intent_clause}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
"thermalDescription": thermal_desc,
|
|
139
|
+
"findings": findings,
|
|
140
|
+
"tailoredOptions": tailored_options,
|
|
141
|
+
"universalImagePrompt": universal_prompt,
|
|
142
|
+
"nanoBananaPrompt": nano_banana_prompt,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
def extract_layers(image_path: str, output_dir: str = "Layers", user_intent: Optional[str] = None) -> Dict[str, Any]:
|
|
146
|
+
abs_image_path = os.path.abspath(image_path)
|
|
147
|
+
if not os.path.exists(abs_image_path):
|
|
148
|
+
raise FileNotFoundError(f"Source image not found: {abs_image_path}")
|
|
149
|
+
|
|
150
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
151
|
+
abs_output_dir = os.path.abspath(output_dir)
|
|
152
|
+
|
|
153
|
+
img = Image.open(abs_image_path).convert("RGB")
|
|
154
|
+
rgb = np.array(img, dtype=np.float32)
|
|
155
|
+
height, width, _ = rgb.shape
|
|
156
|
+
num_pixels = width * height
|
|
157
|
+
|
|
158
|
+
# Luminance channel Y
|
|
159
|
+
lum = 0.2126 * rgb[:, :, 0] + 0.7152 * rgb[:, :, 1] + 0.0722 * rgb[:, :, 2]
|
|
160
|
+
norm_lum = lum / 255.0
|
|
161
|
+
|
|
162
|
+
# 1. طبقة الألوان الفاتحة (Highlights / Specular Zones)
|
|
163
|
+
hl_mask = np.clip((lum - 170.0) / 75.0, 0.0, 1.0)
|
|
164
|
+
hl_layer = rgb * hl_mask[:, :, np.newaxis]
|
|
165
|
+
hl_coverage_pct = float((np.sum(lum > 170.0) / num_pixels) * 100.0)
|
|
166
|
+
hl_path = os.path.join(abs_output_dir, "01_highlights.png")
|
|
167
|
+
Image.fromarray(np.uint8(np.clip(hl_layer, 0, 255))).save(hl_path)
|
|
168
|
+
|
|
169
|
+
# 2. طبقة الألوان الغامقة (Shadows / Low-Key Zones)
|
|
170
|
+
shadow_mask = np.clip((95.0 - lum) / 80.0, 0.0, 1.0)
|
|
171
|
+
shadow_layer = rgb * shadow_mask[:, :, np.newaxis]
|
|
172
|
+
shadow_coverage_pct = float((np.sum(lum < 85.0) / num_pixels) * 100.0)
|
|
173
|
+
shadow_path = os.path.join(abs_output_dir, "02_shadows.png")
|
|
174
|
+
Image.fromarray(np.uint8(np.clip(shadow_layer, 0, 255))).save(shadow_path)
|
|
175
|
+
|
|
176
|
+
# 3. طبقة الظل العالي (Deep Ambient Occlusion & Contact Umbra)
|
|
177
|
+
ao_mask = np.clip((40.0 - lum) / 40.0, 0.0, 1.0)
|
|
178
|
+
ao_visual = np.zeros((height, width, 3), dtype=np.float32)
|
|
179
|
+
ao_visual[:, :, 0] = (1.0 - ao_mask) * 230.0
|
|
180
|
+
ao_visual[:, :, 1] = (1.0 - ao_mask) * 240.0 + (ao_mask * 15.0)
|
|
181
|
+
ao_visual[:, :, 2] = (1.0 - ao_mask) * 255.0 + (ao_mask * 30.0)
|
|
182
|
+
ao_coverage_pct = float((np.sum(lum < 35.0) / num_pixels) * 100.0)
|
|
183
|
+
ao_path = os.path.join(abs_output_dir, "03_ambient_occlusion.png")
|
|
184
|
+
Image.fromarray(np.uint8(np.clip(ao_visual, 0, 255))).save(ao_path)
|
|
185
|
+
|
|
186
|
+
# 4. طبقة الحواف (Sobel Gradient Contours)
|
|
187
|
+
sobel_x = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], dtype=np.float32)
|
|
188
|
+
sobel_y = np.array([[-1, -2, -1], [0, 0, 0], [1, 2, 1]], dtype=np.float32)
|
|
189
|
+
padded_lum = np.pad(norm_lum, 1, mode="edge")
|
|
190
|
+
gx = np.zeros((height, width), dtype=np.float32)
|
|
191
|
+
gy = np.zeros((height, width), dtype=np.float32)
|
|
192
|
+
for i in range(3):
|
|
193
|
+
for j in range(3):
|
|
194
|
+
gx += sobel_x[i, j] * padded_lum[i : i + height, j : j + width]
|
|
195
|
+
gy += sobel_y[i, j] * padded_lum[i : i + height, j : j + width]
|
|
196
|
+
|
|
197
|
+
edge_mag = np.sqrt(gx ** 2 + gy ** 2)
|
|
198
|
+
edge_vis = np.clip(edge_mag * 4.0 * 255.0, 0, 255)
|
|
199
|
+
mean_gradient = float(np.mean(edge_mag))
|
|
200
|
+
edge_path = os.path.join(abs_output_dir, "04_edges.png")
|
|
201
|
+
Image.fromarray(np.uint8(edge_vis)).save(edge_path)
|
|
202
|
+
|
|
203
|
+
# 5. طبقة العمق (Depth / 3D Normal Vector Field)
|
|
204
|
+
scale = 6.0
|
|
205
|
+
nx = -gx * scale
|
|
206
|
+
ny = -gy * scale
|
|
207
|
+
nz = np.ones((height, width), dtype=np.float32)
|
|
208
|
+
mag = np.sqrt(nx ** 2 + ny ** 2 + nz ** 2) + 1e-7
|
|
209
|
+
nx_norm = nx / mag
|
|
210
|
+
ny_norm = ny / mag
|
|
211
|
+
nz_norm = nz / mag
|
|
212
|
+
|
|
213
|
+
normal_rgb = np.zeros((height, width, 3), dtype=np.float32)
|
|
214
|
+
normal_rgb[:, :, 0] = (nx_norm * 0.5 + 0.5) * 255.0
|
|
215
|
+
normal_rgb[:, :, 1] = (ny_norm * 0.5 + 0.5) * 255.0
|
|
216
|
+
normal_rgb[:, :, 2] = (nz_norm * 0.5 + 0.5) * 255.0
|
|
217
|
+
depth_path = os.path.join(abs_output_dir, "05_depth_normals.png")
|
|
218
|
+
Image.fromarray(np.uint8(np.clip(normal_rgb, 0, 255))).save(depth_path)
|
|
219
|
+
|
|
220
|
+
lum_excess = np.maximum(lum - np.mean(lum), 0.0) ** 2
|
|
221
|
+
sum_w = np.sum(lum_excess) + 1e-7
|
|
222
|
+
avg_lx = np.sum(nx_norm * lum_excess) / sum_w
|
|
223
|
+
avg_ly = np.sum(ny_norm * lum_excess) / sum_w
|
|
224
|
+
avg_lz = np.sum(nz_norm * lum_excess) / sum_w
|
|
225
|
+
l_mag = np.sqrt(avg_lx**2 + avg_ly**2 + avg_lz**2) + 1e-7
|
|
226
|
+
avg_lx, avg_ly, avg_lz = avg_lx / l_mag, avg_ly / l_mag, avg_lz / l_mag
|
|
227
|
+
azimuth_deg = (np.degrees(np.arctan2(-avg_ly, avg_lx)) + 360.0) % 360.0
|
|
228
|
+
elevation_deg = np.clip(np.degrees(np.arcsin(np.clip(avg_lz, -1.0, 1.0))), 0.0, 90.0)
|
|
229
|
+
|
|
230
|
+
# 6. طبقة الألوان والتشبع (Chrominance / Saturation Distribution)
|
|
231
|
+
max_c = np.maximum(np.maximum(rgb[:, :, 0], rgb[:, :, 1]), rgb[:, :, 2])
|
|
232
|
+
min_c = np.minimum(np.minimum(rgb[:, :, 0], rgb[:, :, 1]), rgb[:, :, 2])
|
|
233
|
+
chroma = max_c - min_c
|
|
234
|
+
sat = np.zeros_like(chroma)
|
|
235
|
+
non_zero = max_c > 1e-4
|
|
236
|
+
sat[non_zero] = chroma[non_zero] / max_c[non_zero]
|
|
237
|
+
|
|
238
|
+
sat_vis = np.zeros((height, width, 3), dtype=np.float32)
|
|
239
|
+
sat_vis[:, :, 0] = np.clip(sat * 255.0 * 1.5, 0, 255)
|
|
240
|
+
sat_vis[:, :, 1] = np.clip((1.0 - np.abs(sat - 0.5) * 2.0) * 220.0, 0, 255)
|
|
241
|
+
sat_vis[:, :, 2] = np.clip((1.0 - sat) * 200.0, 0, 255)
|
|
242
|
+
mean_saturation = float(np.mean(sat))
|
|
243
|
+
chroma_path = os.path.join(abs_output_dir, "06_chroma_saturation.png")
|
|
244
|
+
Image.fromarray(np.uint8(sat_vis)).save(chroma_path)
|
|
245
|
+
|
|
246
|
+
# Correlated Color Temperature
|
|
247
|
+
mean_r = float(np.mean(rgb[:, :, 0]))
|
|
248
|
+
mean_g = float(np.mean(rgb[:, :, 1]))
|
|
249
|
+
mean_b = float(np.mean(rgb[:, :, 2]))
|
|
250
|
+
cct_kelvin = calculate_cct(mean_r, mean_g, mean_b)
|
|
251
|
+
|
|
252
|
+
# Build Dynamic Diagnostics based purely on calculated data
|
|
253
|
+
diag = build_dynamic_diagnostics(
|
|
254
|
+
cct_kelvin=cct_kelvin,
|
|
255
|
+
azimuth_deg=azimuth_deg,
|
|
256
|
+
elevation_deg=elevation_deg,
|
|
257
|
+
hl_coverage_pct=hl_coverage_pct,
|
|
258
|
+
shadow_coverage_pct=shadow_coverage_pct,
|
|
259
|
+
ao_coverage_pct=ao_coverage_pct,
|
|
260
|
+
mean_gradient=mean_gradient,
|
|
261
|
+
mean_saturation=mean_saturation,
|
|
262
|
+
user_intent=user_intent
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
report = {
|
|
266
|
+
"sourceImage": abs_image_path,
|
|
267
|
+
"layersDirectory": abs_output_dir,
|
|
268
|
+
"dimensions": [width, height],
|
|
269
|
+
"opticalMetrics": {
|
|
270
|
+
"cctKelvin": round(cct_kelvin, 1),
|
|
271
|
+
"dominantLightVector": [round(float(avg_lx), 4), round(float(avg_ly), 4), round(float(avg_lz), 4)],
|
|
272
|
+
"lightingAngles": {
|
|
273
|
+
"azimuthDeg": round(float(azimuth_deg), 1),
|
|
274
|
+
"elevationDeg": round(float(elevation_deg), 1),
|
|
275
|
+
},
|
|
276
|
+
"meanLuminance": round(float(np.mean(lum)), 2),
|
|
277
|
+
"meanSaturation": round(mean_saturation, 3),
|
|
278
|
+
"edgeRoughness": round(mean_gradient, 4),
|
|
279
|
+
},
|
|
280
|
+
"diagnostics": diag,
|
|
281
|
+
"layers": {
|
|
282
|
+
"01_highlights": {
|
|
283
|
+
"file": "01_highlights.png",
|
|
284
|
+
"path": hl_path,
|
|
285
|
+
"description": "طبقة الألوان الفاتحة - High-key Luminance & Specular Zones",
|
|
286
|
+
"coveragePct": round(hl_coverage_pct, 2),
|
|
287
|
+
},
|
|
288
|
+
"02_shadows": {
|
|
289
|
+
"file": "02_shadows.png",
|
|
290
|
+
"path": shadow_path,
|
|
291
|
+
"description": "طبقة الألوان الغامقة - Low-key & Shadow Zones",
|
|
292
|
+
"coveragePct": round(shadow_coverage_pct, 2),
|
|
293
|
+
},
|
|
294
|
+
"03_ambient_occlusion": {
|
|
295
|
+
"file": "03_ambient_occlusion.png",
|
|
296
|
+
"path": ao_path,
|
|
297
|
+
"description": "طبقة الظل العالي - High Contact Shadows & Ground Occlusion",
|
|
298
|
+
"coveragePct": round(ao_coverage_pct, 2),
|
|
299
|
+
},
|
|
300
|
+
"04_edges": {
|
|
301
|
+
"file": "04_edges.png",
|
|
302
|
+
"path": edge_path,
|
|
303
|
+
"description": "طبقة الحواف - Sobel High-Frequency Gradient Contours",
|
|
304
|
+
"meanGradient": round(mean_gradient, 4),
|
|
305
|
+
},
|
|
306
|
+
"05_depth_normals": {
|
|
307
|
+
"file": "05_depth_normals.png",
|
|
308
|
+
"path": depth_path,
|
|
309
|
+
"description": "طبقة العمق - 3D Surface Normal Gradient Field (RGB Tangent Map)",
|
|
310
|
+
"dominantAzimuth": round(float(azimuth_deg), 1),
|
|
311
|
+
"dominantElevation": round(float(elevation_deg), 1),
|
|
312
|
+
},
|
|
313
|
+
"06_chroma_saturation": {
|
|
314
|
+
"file": "06_chroma_saturation.png",
|
|
315
|
+
"path": chroma_path,
|
|
316
|
+
"description": "طبقة الألوان والتشبع والبكسلات - Chrominance & Saturation Distribution",
|
|
317
|
+
"meanSaturation": round(mean_saturation, 3),
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
# Format findings and tailored suggestions dynamically
|
|
323
|
+
findings_rendered = "\n\n".join([f"{item}" for item in diag["findings"]])
|
|
324
|
+
options_rendered = "\n".join([f"- **خيار {idx + 1}**: {opt}" for idx, opt in enumerate(diag["tailoredOptions"])])
|
|
325
|
+
|
|
326
|
+
user_intent_header = f"\n**الهدف المخصص المطلوب**: `{user_intent}`\n" if user_intent else ""
|
|
327
|
+
|
|
328
|
+
layer_md_path = os.path.join(abs_output_dir, "Layer.md")
|
|
329
|
+
layer_md_content = f"""# تقرير الطبقات التحليلية الست (Layer.md)
|
|
330
|
+
**مصدر الصورة**: `{abs_image_path}`
|
|
331
|
+
**أبعاد الصورة**: `{width}x{height}` بكسل
|
|
332
|
+
**التصنيف الضوئي المكتشف**: `{diag['thermalDescription']}`
|
|
333
|
+
**زاوية الإضاءة المسجلة**: سمت `{round(azimuth_deg)}°` | ارتفاع `{round(elevation_deg)}°`{user_intent_header}
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## 1. فهرس الطبقات التحليلية الست (The 6 Extracted Layers)
|
|
338
|
+
|
|
339
|
+
| # | اسم الطبقة | الملف المولد | القراءة الفيزيائية المكتشفة |
|
|
340
|
+
|---|---|---|---|
|
|
341
|
+
| **1** | **طبقة الألوان الفاتحة (Highlights)** | [`01_highlights.png`](file:///{hl_path.replace(os.sep, '/')}) | نسبة تغطية الأضواء الساطعة: `{round(hl_coverage_pct, 2)}%` |
|
|
342
|
+
| **2** | **طبقة الألوان الغامقة (Shadows)** | [`02_shadows.png`](file:///{shadow_path.replace(os.sep, '/')}) | مساحة الظلال والمناطق الداكنة: `{round(shadow_coverage_pct, 2)}%` |
|
|
343
|
+
| **3** | **طبقة الظل العالي (Ambient Occlusion)** | [`03_ambient_occlusion.png`](file:///{ao_path.replace(os.sep, '/')}) | كثافة الارتكاز الأرضي وظلال التلامس: `{round(ao_coverage_pct, 2)}%` |
|
|
344
|
+
| **4** | **طبقة الحواف (Edges & Contours)** | [`04_edges.png`](file:///{edge_path.replace(os.sep, '/')}) | تباين الحواف المجهرية لسوبل: `{round(mean_gradient, 4)}` |
|
|
345
|
+
| **5** | **طبقة العمق (3D Depth Normals)** | [`05_depth_normals.png`](file:///{depth_path.replace(os.sep, '/')}) | توجيه التنسور المتجهي ثلاثي الأبعاد: سمت `{round(azimuth_deg)}°` |
|
|
346
|
+
| **6** | **طبقة الألوان والتشبع (Chroma / Saturation)** | [`06_chroma_saturation.png`](file:///{chroma_path.replace(os.sep, '/')}) | متوسط تشبع وتوزيع نقاء الألوان: `{round(mean_saturation, 3)}` |
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 2. النتائج البصرية الديناميكية المستخلصة من الصورة (Dynamic Findings)
|
|
351
|
+
|
|
352
|
+
{findings_rendered}
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
## 3. التوجيه التوليدي المخصص (Calibrated Prompts for Image Generators)
|
|
357
|
+
|
|
358
|
+
### أ. النمط الوصفي الاستوديوي (Any Image Generator Model):
|
|
359
|
+
> "{diag['universalImagePrompt']}"
|
|
360
|
+
|
|
361
|
+
### ب. نمط الشيدر الضوئي المكثف (GEMINI Nano Banana - Antigravity):
|
|
362
|
+
> "{diag['nanoBananaPrompt']}"
|
|
363
|
+
|
|
364
|
+
> 💡 **ملاحظة**: هذا التوجيه متوافق مع **أي Image Generator Model**، ويُفضل ويُوصى بشدة باستخدامه داخل **Google Antigravity** لتطبيق التعديل المباشر عبر أداة `generate_image` ومحرك **GEMINI Nano Banana**!
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## 4. الخطوة التفاعلية الموجهة بالبيانات (Dynamic Interactive Decision)
|
|
369
|
+
|
|
370
|
+
**ماذا تريد من تعديل؟**
|
|
371
|
+
بناءً على المعطيات الضوئية المكتشفة في هذه الصورة تحديداً، إليك خيارات التعديل المقترحة:
|
|
372
|
+
|
|
373
|
+
{options_rendered}
|
|
374
|
+
|
|
375
|
+
💬 *يمكنك أيضاً طلب أي تعديل مخصص بحرية تامة، وسيتم فوراً توليد برومبت دقيق للغاية أو إنشاء الصورة المعدلة بالكامل!*
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
with open(layer_md_path, "w", encoding="utf-8") as f:
|
|
379
|
+
f.write(layer_md_content)
|
|
380
|
+
|
|
381
|
+
report["layerMarkdownPath"] = layer_md_path
|
|
382
|
+
return report
|
|
383
|
+
|
|
384
|
+
def main():
|
|
385
|
+
parser = argparse.ArgumentParser(description="MarwanDevSpace Dynamic 6-Layer Optical Decomposition")
|
|
386
|
+
parser.add_argument("--image", "-i", required=True, help="Path to input image")
|
|
387
|
+
parser.add_argument("--output-dir", "-o", default="Layers", help="Output directory for the 6 layers (default: Layers)")
|
|
388
|
+
parser.add_argument("--intent", "-t", default=None, help="Optional user modification intent to tailor report and prompts")
|
|
389
|
+
parser.add_argument("--json", action="store_true", help="Print JSON report to stdout")
|
|
390
|
+
args = parser.parse_args()
|
|
391
|
+
|
|
392
|
+
try:
|
|
393
|
+
report = extract_layers(args.image, args.output_dir, user_intent=args.intent)
|
|
394
|
+
if args.json:
|
|
395
|
+
print(json.dumps(report, indent=2))
|
|
396
|
+
else:
|
|
397
|
+
diag = report["diagnostics"]
|
|
398
|
+
print(f"[MarwanDevSpace] 6 Analytical layers successfully extracted to: {report['layersDirectory']}")
|
|
399
|
+
print(f"[MarwanDevSpace] Dynamic Layer.md report created: {report['layerMarkdownPath']}")
|
|
400
|
+
print(f"\nOptical Profile: {diag['thermalDescription']} | Azimuth: {report['opticalMetrics']['lightingAngles']['azimuthDeg']}°")
|
|
401
|
+
print("\nKey Dynamic Findings:")
|
|
402
|
+
for f in diag["findings"][:3]:
|
|
403
|
+
print(f" • {f}")
|
|
404
|
+
print("\n[Interactive Decision Point]: ماذا تريد من تعديل؟")
|
|
405
|
+
for opt in diag["tailoredOptions"]:
|
|
406
|
+
print(f" - {opt}")
|
|
407
|
+
except Exception as e:
|
|
408
|
+
print(f"[Error] Failed to extract layers: {e}", file=sys.stderr)
|
|
409
|
+
sys.exit(1)
|
|
410
|
+
|
|
411
|
+
if __name__ == "__main__":
|
|
412
|
+
main()
|