# Phân Tích Kỹ Thuật & Giải Pháp Khắc Phục Lỗi "Halo" Khi Chỉnh Highlight, Shadow, White, Black (Tích Hợp Cân Bằng Saturation & Vibrance) --- ## 1. Hiện Tượng Halo Artifact Khi Chỉnh 4 Thông Số Ánh Sáng (Highlight, Shadow, White, Black) Khi người dùng điều chỉnh 4 thanh trượt ánh sáng (**Highlights**, **Shadows**, **Whites**, **Blacks**) — đặc biệt là kéo mạnh $+100$ hoặc $-100$ trên các tệp **ảnh RAW** (`.NEF`, `.RAF`, `.DNG`, `.RW2`): * Tại các đường biên tương phản cao (Step Edges) như: ngọn núi/tán cây trên nền trời sáng, đường viền chủ thể ngược sáng, khung cửa sổ sáng trên mảng tường tối. * Xuất hiện **đường viền phát sáng rực (Bright Halo)** hoặc **vệt viền đen sẫm (Dark Halo/Undershoot)** ôm theo mép cạnh. * **Màu sắc tại vùng viền bị bếch hoặc rực lên bất thường (Saturation & Fringing Artifacts)** khi kết hợp với thanh trượt **Color (Saturation)** hoặc **Vibrance**. ``` MẶT CẮT ĐỘ SÁNG TẠI ĐƯỜNG BIÊN TƯƠNG PHẢN CAO (Tán cây vs Bầu trời): Độ Sáng (Luminance L) ▲ 1.0 ┤ [Overshoot: Viền sáng rực (Bright Halo)] │ ┌───┐ 0.9 ├────────────────────────────┘ └──────────────────────── (Bầu trời) │ │ 0.1 ├────────────────────────────┐ ┌──────────────────────── (Tán cây) │ └───┘ 0.0 ┴────────────────────────────────────────────────────────► Khoảng cách (X) [Undershoot: Viền đen sẫm (Dark Halo)] ``` --- ## 2. Nguyên Nhân Kỹ Thuật Cốt Lõi ### A. Sự Rò Rỉ Không Gian Của Bộ Lọc Nền (Gaussian Neighborhood Leakage) 1. Để bảo tồn tương phản cục bộ (Local Contrast), thuật toán tách độ sáng ảnh thành 2 lớp: $$\text{Image} = \text{Base Layer (Khối nền tổng thể)} + \text{Detail Layer (Chi tiết vi mô)}$$ 2. Lớp `Base Layer` được trích xuất bằng bộ lọc làm mờ. Nếu bộ lọc làm mờ là **Gaussian Blur** thông thường: * Về phía bầu trời sáng ($t \approx 0.95$) gần ngọn núi tối: Gaussian Blur gom cả pixel tối vào $\implies$ `base` bị giảm xuống (ví dụ `base = 0.60`). * Phép tính target độ sáng: $\text{target} = \text{toneCurve}(\text{base}) + (t - \text{base})$. * Vì `base = 0.60` thấp hơn thực tế, khi nén Highlight ($-Highlight$), hàm `toneCurve(0.60)` không nén đủ sâu $\implies$ Độ sáng tại mép trời bị đẩy vọt lên cao hơn vùng trời xa biên $\implies$ **Tạo viền trắng sáng rực (Bright Halo)**. * Ngược lại, phía ngọn núi tối ($t \approx 0.05$) gần bầu trời sáng: `base` bị kéo lên cao $\implies$ Khi nâng Shadow ($+Shadow$), hàm `toneCurve` nâng không đủ $\implies$ **Tạo viền đen đậm (Dark Halo)**. ### B. Tại Sao Ảnh RAW Bị Ảnh Hưởng Nghiêm Trọng Hơn Ảnh JPEG? * Ảnh JPEG 8-bit đã qua nén tương phản và kẹp dải sáng ($0.0 - 1.0$), chênh lệch bước nhảy $t - \text{base}$ ở mức vừa phải ($0.05 - 0.15$). * Ảnh **RAW** lưu trữ năng lượng ánh sáng tuyến tính (Linear Scene-Referred Light) với Dải động cực rộng (Dynamic Range 12 - 14 stops). Khi giải mã qua Raw Develop: * Khoảng chênh lệch giữa bầu trời chưa cháy và bóng râm tối sâu là cực lớn. * Bước nhảy $|t - \text{base}|$ tại đường biên tương phản đạt tới $0.30 - 0.75$. * Phép tính Base + Detail với Gaussian Blur thiếu Edge-Gating sẽ tạo ra vùng Overshoot/Undershoot khổng lồ, khiến ảnh RAW bị viền Halo dày cộp và sắc nét. ### C. Tác Động Dây Chuyền Đến Color (Saturation) Và Vibrance 1. **Lệch Sắc Tố (Hunt Effect Fringing):** Thuật toán bù bão hòa màu tự động (Hunt Effect) áp dụng cho dải tối dựa trên `base`. Khi `base` bị lem qua đường biên sáng/tối, lượng Chroma Boost `boost` bị tính toán sai vị trí, tạo ra **viền loang màu (Color Fringing)** xung quanh Halo. 2. **Khuếch Đại Bởi Vibrance & Saturation:** Thanh trượt **Vibrance** tác động khuếch đại màu sắc trên vùng ít bão hòa dựa trên `(1 - chroma)` và Luminance `l2`. Khi Luminance bị biến dạng bởi Halo, phép biến đổi màu sắc nhân tỷ lệ `lightMove` sẽ khuếch đại cả sắc độ tại viền Halo, khiến đường viền vừa bị lóa sáng vừa rực màu bất nhiên. --- ## 3. Giải Pháp Thuật Toán: Edge-Aware Range-Gated Tone Mapping Để triệt tiêu hoàn toàn viền Halo trên cả ảnh JPEG và ảnh RAW mà vẫn giữ nguyên $100\%$ độ nổi khối chi tiết dải trung tính, thuật toán áp dụng cơ chế **Phân hoạch biên độ theo độ chênh lệch ánh sáng (Edge-Aware Range Gating)**: ``` [Luminance Pixel (t) & Gaussian Base (base)] │ ▼ [Tính độ lệch: diff = |t - base|] │ ▼ [Mặt nạ kiểm soát cạnh: edgeW = 1.0 - smoothstep(0.08, 0.35, diff)] │ ┌─────────────────┴─────────────────┐ ▼ (diff nhỏ < 0.08) ▼ (diff lớn > 0.35 - Bước nhảy cạnh cứng) [edgeW ≈ 1.0: Môi trường bề mặt] [edgeW ≈ 0.0: Đường biên tương phản cao] • effBase = base • effBase = t • Target = toneCurve(base)+(t-base) • Target = toneCurve(t) • Giữ 100% chi tiết vân khối 3D. • Chuyển mượt về đường cong toàn cục. • TRIỆT TIÊU HOÀN TOÀN HALO! ``` ### Công Thức Toán Học Cho WebGL / SkSL Shader: ```glsl // 1. Độ chênh lệch giữa luma điểm ảnh và luma môi trường lân cận float diff = abs(t - base); // 2. Mặt nạ suy giảm mượt (Range Falloff Window) // Với diff < 0.08: edgeW = 1.0 (vùng vân mịn / dải chuyển mượt) // Với diff > 0.35: edgeW = 0.0 (đường biên tương phản cứng, đặc biệt trên ảnh RAW) float edgeW = 1.0 - smoothstep(0.08, 0.35, diff); // 3. Luma môi trường hiệu dụng (Effective Base Luminance) float effBase = mix(t, base, edgeW); // 4. Áp dụng toneCurve trên effBase float o = toneCurve(effBase, clamp(bl + dr * 0.12, -1.0, 1.0), clamp(sh + dr * 0.06, -1.0, 1.0), clamp(hl - dr * 0.09, -1.0, 1.0), clamp(wh - dr * 0.18, -1.0, 1.0)); // 5. Tái cấu trúc luma đích không bị vệt halo float target = clamp(o + (t - effBase), 0.0, 1.0); vec3 res = lightMove(c, t, target); // 6. Chroma Boost & Vibrance dựa trên effBase an toàn không lem viền float shMask = smoothstep(0.02, 0.12, effBase) * (1.0 - smoothstep(0.25, 0.55, effBase)); float uBl = clamp(1.0 - effBase / TONE_BLACK_EDGE, 0.0, 1.0); ``` --- ## 4. Cân Chỉnh Sự Tương Tác Với Color (Saturation) & Vibrance 1. **Khóa viền màu theo effBase:** `shMask` và `blMask` sử dụng `effBase` thay cho `base` thô. Khi quét qua đường biên của ảnh RAW, `effBase` tự động tiệm cận `t`, ngăn chặn hoàn toàn việc bơm Saturation nhầm vào đường biên. 2. **Bảo toàn Chroma qua lightMove:** Hàm `lightMove` duy trì tỉ lệ chênh lệch giữa 3 kênh RGB theo Luminance đích `target`. Nhờ `target` không bị nhô (Overshoot) hay lõm (Undershoot), sắc độ của màu sắc (Hue & Saturation) được giữ nguyên vẹn. 3. **Vibrance thích ứng:** Nhờ độ chói `l2` được tính toán trên kết quả `toneRamp` đã sạch viền, phép biến đổi Vibrance `kv = 1.0 + vib * 0.75 * (1.0 - chroma)` hoạt động chính xác trên dải màu trung tính mà không gây rực màu vùng viền. --- ## 5. Mã Nguồn Đã Cập Nhật Trong Codebase 1. **Shader Core (`frontend/shared/utils/toneShader.ts`):** * Đã tích hợp `edgeW` và `effBase` trực tiếp vào hàm `toneRamp` trong `TONE_SKSL`. 2. **Bộ Test Tự Động (`frontend/scripts/`):** * `highlight-knee-check.mjs`: Đã cập nhật chuỗi khớp mẫu shader và kiểm tra toàn vẹn không bị nén đè. * `tone-base-check.mjs`: Đã cập nhật mô hình tính toán dự đoán `effBase`, kiểm tra điểm ảnh $128$ trên nền $76$ cho kết quả chuẩn xác $151/255$. * Đã kiểm tra `npm run typecheck` và 100% các script check trong `scripts/` đều **PASSED**. --- ## 6. Bảng Kiểm Tra Chất Lượng (Quality Assurance Checklist) | Tiêu chí | Hành vi chuẩn (Sau khi fix) | Dấu hiệu lỗi (Cần kiểm tra lại) | | :--- | :--- | :--- | | **Chỉnh Highlight -100 trên RAW** | Bầu trời sáng hiện rõ mây, ranh giới ngọn núi đanh gọn, không có viền sáng lóa xung quanh. | Xuất hiện dải viền trắng phát sáng chạy dọc mép núi. | | **Chỉnh Shadow +100 trên RAW** | Chi tiết bóng râm vách đá nâng lên rõ nét, không có đường viền đen đậm viền ngoài. | Có viền đen/tối sẫm bao quanh các tán cây ngược sáng. | | **Tăng Vibrance +10 & Color +10** | Màu sắc bầu trời, da người và rêu đá rực rỡ tự nhiên, dải biên không bị lem màu neon. | Đường viền núi bị nổi màu xanh/đỏ rực rỡ như nét tô viền (Color Fringing). |