Files
RecipesCam/docker/fix_blacks.md
T

152 lines
8.1 KiB
Markdown

# Phân Tích Kỹ Thuật & Thuật Toán Tối Ưu Thanh Trượt "Blacks" (Lightroom vs Web UI)
---
## 1. So Sánh Bản Chất: "Shadows" Khác Gì "Blacks"?
Rất nhiều nhà phát triển nhầm lẫn giữa hai thanh trượt **Shadows** và **Blacks**:
```
[0% Luminance] ─────────────────────────────────────────── [100% Luminance]
|◄───────── BLACKS (0% - 15%) ─────────►|
|◄──────────── SHADOWS (10% - 45%) ────────────►|
|◄── MIDTONES / HIGHLIGHTS ──►|
```
| Tiêu chí | Thanh trượt Shadows | Thanh trượt Blacks (Chủ đề bài viết) |
| :--- | :--- | :--- |
| **Vùng tác động** | Vùng bóng đổ rộng ($10\% - 45\%$ Luminance). | Chóp chân đồ thị histogram ($0\% - 15\%$ Luminance). |
| **Bản chất xử lý** | **Local Tone Mapping** (Cần lọc cạnh Guided/Bilateral filter để giữ chi tiết vi mô). | **Global Tone Curve / Endpoint Remapping** (Tái cấu trúc điểm chân histogram & độ dốc Toe). |
| **Mục đích** | Cứu chi tiết mảng tối của chủ thể (tán cây, vách đá). | Thiết lập chiều sâu tuyệt đối (Depth), độ trong trẻo (Clarity) và điểm neo đen của ảnh. |
---
## 2. Chẩn Đoán Lỗi Kỹ Thuật Ở Ảnh 2 (Ứng Dụng Tự Code)
Quan sát Ảnh 2, có thể nhận diện ngay 3 sai sót cốt lõi trong công thức toán học:
### A. Lỗi nâng sàn tuyệt đối (Pedestal / Offset Lift)
* **Hiện tượng:** Toàn bộ bức ảnh như bị phủ một lớp sương mù màu xám đục (Milky / Faded look).
* **Nguyên nhân:** Thuật toán hiện tại đang cộng trực tiếp một hằng số vào kênh màu:
$$\text{Color}_{out} = \text{Color}_{in} + \text{BlackOffset}$$
Phép tính này biến điểm đen tuyệt đối ($0.0$) thành màu xám ($0.15 - 0.25$), làm mất hoàn toàn độ sâu trường ảnh.
### B. Lan sang dải Midtones quá mức (Overshoot Falloff)
* **Hiện tượng:** Mặt nước, thân thác nước và các vùng trung tính (Midtones) bị bạc màu và mất tương phản rõ rệt.
* **Nguyên nhân:** Mặt nạ trọng số không dốc đủ mạnh hoặc dùng hàm suy giảm tuyến tính, khiến các giá trị độ sáng tới $40\% - 50\%$ vẫn bị đội lên.
### C. Triệt tiêu độ bão hòa do thao tác sai không gian màu (Saturation Collapse)
* Khi tăng Luminance của vùng cực tối mà không bù trừ tỷ lệ Chroma trong không gian màu tri giác, màu xanh của rêu đá và cây cối bị biến thành màu xám bùn.
---
## 3. Bí Quyết Của Lightroom Khi Tăng "Blacks +100"
Ở Ảnh 3 (Lightroom), dù Blacks đã kéo kịch trần $+100$:
1. **Không hề có màng sương xám:** Lightroom giữ nguyên điểm $0.0$ tuyệt đối hoặc mở rộng dải chân (Toe Expansion) theo hàm mũ mượt mà, điểm đen được kéo giãn có kiểm soát chứ không bị nhấc bổng lên như một tấm màng phẳng.
2. **Bảo tồn tương phản vi mô (Micro-Contrast):** Dải chuyển giữa vùng tối sâu ($0\% - 5\%$) lên vùng tối vừa ($10\% - 20\%$) được làm dốc mềm (Soft Knee Curve), giữ được độ nổi khối của vách núi và mặt nước.
3. **Giới hạn trần nghiêm ngặt:** Tại mốc $\text{Luminance} \approx 0.2$, tác động của thanh trượt Blacks giảm hoàn toàn về $0.0$. Vùng thác nước trắng và bầu trời không đổi 1 giá trị số nào.
---
## 4. Mô Hình Toán Học Đề Xuất Cho Web UI
Thuật toán chuẩn gồm 3 bước xử lý trên không gian ánh sáng tuyến tính (Linear Space):
### Bước 1: Hàm trọng số Toe (Toe Weighting Function)
Sử dụng đường cong Hermite suy giảm bậc cao để khóa chặt vùng tác động dưới $20\%$:
$$W(L) = \text{clamp}\left(1.0 - \frac{L}{L_{threshold}},\, 0.0,\, 1.0\right)^3 \quad (\text{với } L_{threshold} \approx 0.18)$$
### Bước 2: Kéo giãn dải tối không phá hủy điểm 0 (Non-linear Toe Lift)
Thay vì phép cộng $\text{Color} + \Delta$, ta sử dụng phép biến đổi lũy thừa đối xứng (Power Curve Remapping):
$$L_{lifted} = L + \text{Amount} \times W(L) \times \left(\sqrt{L} - L\right)$$
* Khi $L = 0.0 \implies \sqrt{0} - 0 = 0$ (Điểm đen tuyệt đối không bị biến thành màu xám đục).
* Với $0 < L < 0.15 \implies \sqrt{L} > L$ (Vùng bóng tối sâu được giải phóng chi tiết cực kỳ tự nhiên).
### Bước 3: Giữ gìn tỷ lệ màu sắc (Chroma Preservation)
Áp dụng tỷ lệ tăng sáng của độ chói lên trực tiếp các kênh RGB:
$$\text{Scale} = \frac{L_{lifted} + \epsilon}{L + \epsilon} \implies \text{RGB}_{out} = \text{RGB}_{in} \times \text{Scale}$$
---
## 5. Mã Nguồn WebGL Shader Mẫu (Fragment Shader GLSL)
Bạn có thể thay thế Shader hiện tại bằng đoạn mã sau. Shader này chạy trực tiếp trong 1 Single Pass, tối ưu hóa $60\text{ fps}$ cho WebGL:
```glsl
precision highp float;
uniform sampler2D u_image;
uniform float u_black_amount; // Giá trị từ -1.0 đến +1.0 (-100 đến +100 trên UI)
varying vec2 v_texCoord;
// 1. Chuyển đổi sRGB sang Linear
vec3 sRGBToLinear(vec3 c) {
return pow(c, vec3(2.2));
}
// 2. Chuyển đổi Linear sang sRGB
vec3 linearTosRGB(vec3 c) {
return pow(c, vec3(1.0 / 2.2));
}
// 3. Tính Perceptual Luminance chuẩn Rec.709
float getLuminance(vec3 c) {
return dot(c, vec3(0.2126, 0.7152, 0.0722));
}
void main() {
vec4 texColor = texture2D(u_image, v_texCoord);
vec3 linearColor = sRGBToLinear(texColor.rgb);
float lum = getLuminance(linearColor);
// Ngưỡng tối đa vùng Blacks tác động (khoảng 18% ánh sáng tuyến tính)
const float BLACKS_THRESHOLD = 0.18;
// Trọng số suy giảm mượt (Bậc 3 Hermite Falloff)
float t = clamp(lum / BLACKS_THRESHOLD, 0.0, 1.0);
float weight = (1.0 - t) * (1.0 - t) * (1.0 - t);
vec3 result = linearColor;
if (u_black_amount > 0.0) {
// --- TRƯỜNG HỢP NÂNG BLACKS (+0 ĐẾN +100) ---
// Sử dụng hàm căn bậc hai có trọng số để nâng dải tối mà KHÔNG làm xám điểm đen
float expansion = sqrt(lum) - lum;
// Điều chỉnh hệ số nâng dựa trên slider
float liftedLum = lum + (u_black_amount * 0.7) * weight * expansion;
// Khử nhiễu sàn: Giữ tỷ lệ RGB theo độ chói mới để tránh lệch sắc thái
float scale = (liftedLum + 0.0001) / (lum + 0.0001);
result = linearColor * scale;
// Bù nhẹ độ bão hòa (Saturation Recovery) theo lượng Black được mở
float satBoost = 1.0 + (weight * u_black_amount * 0.25);
result = mix(vec3(liftedLum), result, satBoost);
} else if (u_black_amount < 0.0) {
// --- TRƯỜNG HỢP ÉP BLACKS (-0 ĐẾN -100: Làm sâu thêm điểm tối) ---
float crushFactor = 1.0 + (u_black_amount * weight * 0.85);
result = linearColor * max(crushFactor, 0.0);
}
// Bảo vệ dải giá trị an toàn [0.0, 1.0]
result = clamp(result, 0.0, 1.0);
gl_FragColor = vec4(linearTosRGB(result), texColor.a);
}
```
---
## 6. Bảng Tự Kiểm Soát (Self-Check Checklist)
1. **Kiểm tra độ chói điểm $0.0$:** Nhập một pixel đen tuyền $(0, 0, 0)$ qua hàm, giá trị đầu ra **bắt buộc** phải là $(0, 0, 0)$. Nếu ra $(0.1, 0.1, 0.1)$ tức là thuật toán vẫn đang bị lỗi Offset.
2. **Không áp dụng phép cộng trực tiếp:** Mọi thao tác nâng vùng tối đều phải là hàm tỷ lệ nhân (`linearColor * scale`) thay vì cộng `linearColor + delta`.
3. **Thao tác trên dải Linear Light:** Đảm bảo toàn bộ phép tính lũy thừa và căn bậc hai diễn ra sau khi đã giải mã Gamma $2.2$.