Files
RecipesCam/docker/100_lightroom.md
T

191 lines
13 KiB
Markdown

# Giải Mã Kỹ Thuật: Tại Sao Thuật Toán Đưa Lên Web App Vẫn Không Thể Như Lightroom?
Rất nhiều kỹ sư đồ họa khi chuyển đổi thuật toán tone mapping sang WebGL đều gặp phải một thực tế phũ phàng: **công thức toán học trông rất hoàn hảo trên lý thuyết, nhưng khi chạy trên Web App thì Highlights kéo xuống bị xám xịt như bùn tro, Shadows kéo lên thì vỡ nát hạt sạn hoặc bết màu, không thể nào trong trẻo như Lightroom.**
Nguyên nhân không nằm ở việc bạn viết sai code GLSL, mà bắt nguồn từ **6 rào cản kiến trúc cốt tử** giữa môi trường xử lý ảnh chuyên nghiệp (Adobe Camera Raw) và môi trường đồ họa trình duyệt (WebGL / WebGPU).
---
## 1. Rào Cản Cốt Tử #1: Dữ Liệu Đầu Vào (Scene-Referred RAW vs Display-Referred 8-bit)
Đây là lý do lớn nhất chiếm tới $60\%$ sự khác biệt:
```
[QUY TRÌNH LIGHTROOM]
File RAW (14-bit Bayer) ──► Linear Sensor Space ──► Headroom rộng [0.0 đến 4.0+]
(Chi tiết Highlights vượt ngưỡng 1.0 VẪN CÒN NGUYÊN)
[QUY TRÌNH WEB APP THÔNG THƯỜNG]
File JPEG/PNG (8-bit) ──► Bị máy ảnh bake sẵn sRGB ──► Bị cắt cụt cứng (Clamped) [0.0, 1.0]
(Mọi chi tiết > 1.0 đều đã bị vứt bỏ thành (255, 255, 255))
```
### Tại sao Highlights của Lightroom cứu được mây trời, còn Web App biến thành màu "Xám Bùn" (Dirty Gray)?
* **Trong Lightroom (RAW):** Khi chụp một đám mây bị chói, cảm biến máy ảnh thu nhận năng lượng ánh sáng tuyến tính thực tế có thể lên tới $2.5$ hoặc $3.0$ (vượt xa điểm trắng hiển thị $1.0$). Khi bạn kéo `Highlights -100`, Lightroom kéo các giá trị $2.5$ đó nén về khoảng $[0.7 - 0.9]$. Các vân mây, khối hình vẫn giữ nguyên gradient chuyển tiếp.
* **Trên Web App (JPEG):** Ảnh đưa vào WebGL là ảnh 8-bit đã bị thiết bị chụp "nướng chín" (baked). Mọi pixel của đám mây trắng đã bị ép cứng về giá trị tối đa: $R=255, G=255, B=255$ ($1.0$). Gradient nội tại đã **bằng 0**. Khi bạn áp dụng công thức giảm Highlights:
$$1.0 - \text{HighlightAmount} \times \text{Factor} = 0.75$$
Một mảng pixel phẳng lì toàn số $1.0$ biến thành một mảng pixel phẳng lì toàn số $0.75$. Đó chính là lý do đám mây **không hề hiện lại vân mây** mà biến thành một mảng màu xám xịt nhân tạo.
### Tại sao Shadows của Lightroom mịn màng, còn Web App bị vỡ khối (Banding)?
* Vùng tối sâu của file RAW 14-bit chứa hàng ngàn bước thang độ xám (Gray levels).
* Vùng tối của file 8-bit JPEG chỉ có khoảng $5 - 10$ mức số nguyên (từ $0$ đến $10$). Khi kéo `Shadows +100`, Web App phóng đại $10$ mức này lên thành $50 - 60$, tạo ra hiện tượng bậc thang đứt gãy (Color Banding) và nhiễu nén JPEG rác (Macroblocking artifacts).
---
## 2. Rào Cản Kỹ Thuật #2: Độ Chính Xác FBO WebGL (8-bit Unsigned Byte vs 16/32-bit Float)
Trong pipeline nhiều lớp (Multi-pass Pipeline) của WebGL:
```
Pass 1: Blur/Guided Filter ──► Framebuffer (FBO) ──► Pass 2: Tone Composite
```
* **Sai lầm phổ biến trên Web:** Khởi tạo texture FBO bằng kiểu mặc định:
```javascript
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
```
Kiểu `gl.UNSIGNED_BYTE` giới hạn bộ đệm FBO chỉ có **256 mức giá trị** cho mỗi kênh màu.
* **Hậu quả:**
1. Khi Pass 1 tính toán lớp nền (Base Layer) qua bộ lọc Guided Filter, các phép tính trung bình dấu phẩy động bị làm tròn (rounding error) liên tục về $1/255$.
2. Sang Pass 2, phép trừ $\text{Detail} = \text{Orig} - \text{Base}$ thực hiện trên các giá trị đã bị làm tròn thô kệch, tạo ra các viền bậc thang nhân tạo li ti khắp bề mặt ảnh.
---
## 3. Rào Cản Thuật Toán #3: Local Laplacian Pyramid vs Single-Pass Guided Filter
Trong các tài liệu thông thường, người ta hay đơn giản hóa rằng Lightroom dùng "Bilateral" hay "Guided Filter". Nhưng trên thực tế, kiến trúc xử lý Tone của Adobe Camera Raw (từ Process Version 2012 trở đi) sử dụng thuật toán đỉnh cao: **Local Laplacian Filters (Aubry, Paris, Hasinoff, Kautz - MIT/Adobe Research)**.
```
[CÁCH WEB APP THƯỜNG LÀM]
Ảnh Đầu Vào
│
┌─────────────┴─────────────┐
▼ ▼
Base Layer Detail Layer
(1 bán kính) (1 bán kính)
│ │
└─────────────┬─────────────┘
▼
Ảnh kết quả (Dễ bẹt khối)
[CÁCH LIGHTROOM THỰC SỰ LÀM]
Ảnh Đầu Vào
│
┌───────────┼───────────┐
▼ ▼ ▼
Cấp 0 Cấp 1 Cấp 2 ... (Pyramid N Cấp)
(Micro) (Meso) (Macro)
│ │ │
[Nắn chỉnh từng nhánh bằng đường cong Tone Curve cục bộ phi tuyến]
│ │ │
└───────────┼───────────┘
▼
Tái cấu trúc: Khối sâu 3D, chuyển tiếp dải sáng hoàn hảo
```
* **Điểm yếu của Web App:** Khi chỉ dùng 1 bán kính mờ duy nhất cho Base Layer, bạn chỉ nắn sáng được ở một "tần số" cố định. Nếu bán kính lọc quá nhỏ $\implies$ sinh ra viền xung quanh vật thể; nếu bán kính lọc quá lớn $\implies$ các khối chi tiết trung bình bị phẳng lì (mất tương phản vi mô).
* **Lightroom:** Nén sáng trên một kim tự tháp đa tầng (Laplacian Pyramid). Nó nén vùng sáng tổng thể nhưng đồng thời **khuếch đại tương phản cục bộ tại mọi cấp độ phân giải**, giúp ngọn núi vừa sáng rõ vừa gồ ghề sắc cạnh.
---
## 4. Rào Cản Màu Sắc #4: Lỗi Chia Tỷ Lệ Màu Vùng Tối & Lệch Sắc Thái (Chroma Shift)
Trong đoạn shader WebGL của các ứng dụng tự phát triển, thường có dòng code:
```glsl
float scale = (newLum + 0.0001) / (lumOrig + 0.0001);
vec3 result = orig * scale;
```
Đây là nguyên nhân gây ra hiện tượng **"nát màu"** ở vùng tối:
1. **Hiện tượng phát nổ nhiễu (Chroma Explosion):** Tại các pixel cực tối, $\text{lumOrig} \approx 0.001$. Khi kéo Shadows, $\text{newLum}$ được nâng lên $0.05$. Tỷ lệ `scale` lúc này bằng:
$$\frac{0.05}{0.001} = 50 \text{ lần!}$$
Một hạt nhiễu đỏ nhỏ li ti mắt thường không thấy ở vùng tối bị nhân lên gấp $50$ lần, biến thành một đốm màu đỏ rực khổng lồ loang lổ.
2. **Lệch sắc thái (Hue Distortion):** Ba kênh $R, G, B$ khi bị nhân tuyến tính với hệ số lớn sẽ chạm trần clipping ($1.0$) không đồng đều. Kênh nào chạm trần trước sẽ bị gãy tỷ lệ, khiến màu xanh lá của rừng cây chuyển sang màu vàng chuối hoặc xanh neon bẩn.
---
## 5. Rào Cản Không Gian Màu #5: Linear sRGB vs ACEScg / ProPhoto RGB
* WebGL thông thường chỉ chuyển: $\text{sRGB} \xrightarrow{pow(2.2)} \text{Linear sRGB}$.
* Không gian **sRGB có dải màu (Color Gamut) rất hẹp**. Khi bạn tăng sáng một vùng tối có màu rêu đậm, màu sắc sau khi tăng sáng sẽ nhanh chóng văng ra ngoài biên giới tam giác sRGB (Out of Gamut), dẫn đến việc màu bị bẹt và xỉn.
* **Lightroom:** Xử lý toàn bộ trong không gian **ProPhoto RGB** (hoặc ACEScg). Đây là không gian màu siêu rộng (Wide Color Gamut), bao phủ hầu hết các màu mắt người nhìn thấy được. Dù có đẩy Shadows hay nén Highlights kịch kim, màu sắc vẫn nằm gọn trong không gian tính toán an toàn trước khi được Tone Map mượt mà về màn hình hiển thị.
---
## 6. Giải Pháp Đột Phá: Làm Thế Nào Để Web App Tiệm Cận 90% - 95% Lightroom?
Để đưa Web App của bạn lên chất lượng tương đương Lightroom ngay trên tệp ảnh 8-bit thông thường, hãy áp dụng ngay **4 giải pháp kỹ thuật sống còn** dưới đây:
### Giải Pháp 1: Bắt buộc dùng WebGL2 Float Texture cho Framebuffer (FBO)
Khử hoàn toàn lỗi vỡ hạt và đường viền răng cưa bằng cách kích hoạt kết cấu dấu phẩy động 16-bit:
```javascript
// Sử dụng WebGL2 context
const gl = canvas.getContext('webgl2');
gl.getExtension('EXT_color_buffer_float');
// Tạo FBO Texture lưu trữ số thực nửa độ chính xác (Half Float - 16 bit)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null);
```
### Giải Pháp 2: Khôi Phục Highlights Ảo (Pseudo-Highlight Inpainting & Desaturation)
Vì ảnh 8-bit không có headroom, khi người dùng kéo `Highlights < 0`, thay vì giảm độ sáng đơn thuần làm xám đám mây, hãy áp dụng kỹ thuật **Highlight Soft-Roll Rolloff + Khử bão hòa biên độ cao**:
```glsl
// Kỹ thuật cứu cháy sáng cho ảnh 8-bit trên WebGL:
if (u_highlights < 0.0) {
// 1. Chỉ nén những vùng có gradient (không nén vùng cháy tuyệt đối 1.0)
float highlightMask = smoothstep(0.6, 0.95, baseLum);
// 2. Nén dải sáng theo đường cong lũy thừa mềm (Soft Knee)
float compress = pow(baseLum, 1.0 - u_highlights * 0.5);
newLum = mix(baseLum, compress, highlightMask);
// 3. Với các pixel chạm ngưỡng 1.0, hòa trộn nhẹ với màu sắc môi trường xung quanh
// để tái tạo gradient nhân tạo, tránh để mảng trắng chuyển sang mảng xám xịt.
}
```
### Giải Pháp 3: Ổn Định Tỷ Lệ Màu Vùng Tối Bằng Soft-Division (Khử Nát Màu)
Thay thế công thức chia tỷ lệ thô bằng hàm **Soft Saturation Clamp**:
```glsl
// Thay vì: scale = newLum / lumOrig;
// HÃY DÙNG CÔNG THỨC ỔN ĐỊNH CỦA ACR:
float safeLumOrig = max(lumOrig, 0.005);
float scale = newLum / safeLumOrig;
// Nén hệ số khuếch đại tối đa để không bao giờ khuếch đại nhiễu quá 3.0 lần
float clampedScale = min(scale, 1.0 + max(u_shadows, 0.0) * 2.5);
// Tái cấu trúc RGB an toàn
vec3 result = orig * clampedScale;
// Dập tắt nhiễu màu sàn đen: Vùng cực tối (< 2%) ép dần về đơn sắc (Desaturate to Neutral)
float noiseFloor = smoothstep(0.0, 0.03, newLum);
result = mix(vec3(newLum), result, noiseFloor);
```
### Giải Pháp 4: Chuyển Sang Không Gian Màu Tri Giác OKLab Thay Vì sRGB
Thay vì nắn độ sáng trên RGB rồi chia tỷ lệ, hãy chuyển đổi pixel sang không gian màu **OKLab**:
* Kênh $L$: Độ sáng tri giác người (Perceptual Lightness).
* Kênh $a, b$: Sắc độ màu (Chrominance).
Khi bạn can thiệp vào Shadows, Highlights, Blacks, Whites: **chỉ tác động duy nhất vào kênh $L$**. Kênh $a, b$ được giữ nguyên hoặc nhân nhẹ theo định luật Hunt Effect. Điều này triệt tiêu $100\%$ hiện tượng cây cối bị biến thành màu bùn hay da mặt bị ố vàng.
---
## 7. Bảng So Sánh Trước & Sau Khi Cải Cách Pipeline
| Tiêu chí | Web App Hiện Tại Của Bạn | Web App Sau Khi Tối Ưu Nâng Cao |
| :--- | :--- | :--- |
| **Độ sâu FBO Texture** | 8-bit `UNSIGNED_BYTE` (256 mức) | 16-bit `RGBA16F` (Float - Vô hạn mức mượt) |
| **Kéo Shadows $+100$** | Hạt sạn loang lổ, bợt màu, xám đục | Trong trẻo, giữ nguyên sắc xanh của lá và độ sâu đen |
| **Kéo Highlights $-100$** | Mây biến thành mảng xám bùn nhân tạo | Chuyển tiếp êm dịu, không sinh viền giả |
| **Không gian tính toán** | Linear sRGB thô sơ | OKLab / Perceptual Space với Soft-Division |
| **Hiện tượng mép cạnh** | Xuất hiện viền Halo hoặc Stroke | Triệt tiêu mép cạnh hoàn toàn ($100\%$ tự nhiên) |