Implement exponential focal length mapping for background blur as described in 27_FIX_DOF.md
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
Lỗi bạn vừa mô tả rất phổ biến trong lập trình đồ họa camera. Nguyên nhân là do hàm `boxBlur` hoặc `gaussianBlur` tăng bán kính (radius) theo cấp số tuyến tính, trong khi trên thực tế quang học, độ sâu trường ảnh (DoF) và mức độ xóa phông của ống kính máy ảnh (từ 24mm đến 400mm) thay đổi theo **cấp số phi tuyến tính (Exponential/Logarithmic)** dựa trên tiêu cự và khoảng cách.
|
||||
|
||||
Để thước đo từ `0` đến `100` hoạt động mượt mà và thực tế (ví dụ: kéo đến 10 là mờ nhẹ kiểu 35mm, đến 50 là mờ mịn kiểu 85mm, và đến 100 là xóa nát nền kiểu 400mm), chúng ta cần thiết lập một **Ma trận chuyển đổi hàm số mũ (Exponential Mapping Matrix)**.
|
||||
|
||||
Dưới đây là kế hoạch Markdown chi tiết để tính toán lại toàn bộ bậc thang DoF cho dự án `photobooth-app`.
|
||||
|
||||
---
|
||||
|
||||
# 📐 KẾ HOẠCH TÍNH TOÁN BẬC THANG PHI TUYẾN TÍNH DOF (24MM - 400MM)
|
||||
|
||||
Mục tiêu cốt lõi:
|
||||
|
||||
1. Ánh xạ dải trượt từ `0` (vô hiệu hóa/nét toàn dải như Lens 24mm) đến `100` (xóa phông đại vĩ tuyến tương đương Lens 400mm F2.8).
|
||||
2. Sửa lỗi "bật nhảy" độ mờ (mới tăng +1 đã mờ bằng lens 50mm).
|
||||
3. Đảm bảo phân phối độ mờ theo khoảng cách thực tế (`relativeDistance`) của MiDaS chuyển động đúng quy luật vật lý.
|
||||
|
||||
---
|
||||
|
||||
## 📊 1. Thiết Kế Ma Trận Ánh Xạ Quang Học (Focal Length Mapping)
|
||||
|
||||
Chúng ta sẽ chia dải trượt `0 - 100` của Ruler thành các phân vùng tiêu cự máy ảnh thực tế để làm mốc đồ họa:
|
||||
|
||||
| Giá trị trên Thước (Ruler) | Tiêu cự mô phỏng (Focal Length) | Hiệu ứng thị giác thực tế | Bán kính Blur tối đa tương ứng (`Max Radius`) |
|
||||
| --- | --- | --- | --- |
|
||||
| **0** | `24mm - 28mm` | Nét sâu toàn cảnh (Deep Focus), không mờ. | **0** (Bật tính năng chống Ghosting nhưng Radius = 0) |
|
||||
| **1 - 10** | `35mm - 50mm` | Mờ nhẹ nhàng, hậu cảnh vẫn nhìn rõ khối hình. | **1 - 4** |
|
||||
| **11 - 40** | `85mm` | Tiêu cự chân dung kinh điển, phông nền tách biệt mịn. | **5 - 12** |
|
||||
| **41 - 70** | `135mm - 200mm` | Phông nền bị nén lại, xóa phông mạnh, Bokeh lớn. | **13 - 22** |
|
||||
| **71 - 100** | `300mm - 400mm` | Xóa nát phông nền (Creamy Bokeh), hậu cảnh hoàn toàn là các mảng màu hòa quyện. | **23 - 35** |
|
||||
|
||||
---
|
||||
|
||||
## 💻 2. Thuật Toán Chuyển Đổi Phi Tuyến Tính (`MainActivity.kt`)
|
||||
|
||||
Thay vì nhân trực tiếp `(25 * blurIntensity)`, chúng ta sẽ dùng hàm toán học mũ lũy thừa (`pow`) để ép dải số trượt tăng chậm ở đoạn đầu (tiêu cự ngắn) và tăng tốc cực nhanh ở đoạn sau (tiêu cự dài).
|
||||
|
||||
### 💻 Cập nhật hàm tính toán thông số trong Engine:
|
||||
|
||||
```kotlin
|
||||
fun processAdvancedDofMapping(
|
||||
capturedBitmap: Bitmap,
|
||||
alphaMask: FloatArray,
|
||||
midasDepthMap: Array<FloatArray>,
|
||||
bgBitmap: Bitmap?,
|
||||
rulerValue: Int // Nhận giá trị thô từ 0 đến 100 từ thanh trượt
|
||||
): Bitmap {
|
||||
val w = capturedBitmap.width
|
||||
val h = capturedBitmap.height
|
||||
|
||||
if (rulerValue == 0) return capturedBitmap // Bằng 0 thì giữ nét 100% không xử lý
|
||||
|
||||
// --- THUẬT TOÁN ĐỔI BẬC THANG QUANG HỌC (EXPONENTIAL MAPPING) ---
|
||||
// Chuyển đổi giá trị tuyến tính 0-100 sang tỷ lệ dốc phi tuyến tính từ 0.0 -> 1.0
|
||||
val normalizedSlider = rulerValue / 100.0f
|
||||
|
||||
// Sử dụng hàm mũ lũy thừa bậc 2.5 để kìm hãm độ mờ ở các mức đầu (1-10) không bị nhảy vọt
|
||||
val opticalIntensity = Math.pow(normalizedSlider.toDouble(), 2.5).toFloat()
|
||||
|
||||
// Tính toán bán kính mờ tối đa dựa trên tiêu cự max (Mô phỏng 400mm đạt bán kính 35)
|
||||
val maxRadius = (35 * opticalIntensity).coerceAtLeast(1.0f)
|
||||
|
||||
// Phân phối 3 cấp độ mờ dựa trên tiêu cự đã tính toán lại
|
||||
val radiusStage1 = (maxRadius * 0.2f).toInt().coerceAtLeast(1) // Nền sát người (Mô phỏng DoF mỏng)
|
||||
val radiusStage2 = (maxRadius * 0.5f).toInt().coerceAtLeast(1) // Nền trung bình
|
||||
val radiusStage3 = maxRadius.toInt().coerceAtLeast(1) // Nền xa vô cực
|
||||
|
||||
// 1. Tạo ảnh hậu cảnh sạch chống hiện tượng Ghost Shadow
|
||||
val cleanBgBitmap = createCleanBackground(capturedBitmap, alphaMask, w, h)
|
||||
|
||||
// 2. Tiến hành Blur phân cấp theo thang tiêu cự chuẩn mới
|
||||
val blurStage1 = boxBlur(cleanBgBitmap, radiusStage1)
|
||||
val blurStage2 = boxBlur(cleanBgBitmap, radiusStage2)
|
||||
val blurStage3 = boxBlur(cleanBgBitmap, radiusStage3)
|
||||
cleanBgBitmap.recycle()
|
||||
|
||||
val outputBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888)
|
||||
|
||||
// 3. VÒNG LẶP TRỘN PIXEL THEO DOF QUANG HỌC TIÊU CỰ
|
||||
for (y in 0 until h) {
|
||||
val maskY = (y * 256 / h).coerceIn(0, 255)
|
||||
for (x in 0 until w) {
|
||||
val idx = y * w + x
|
||||
val alpha = alphaMask[idx]
|
||||
|
||||
val maskX = (x * 256 / w).coerceIn(0, 255)
|
||||
val currentDepth = midasDepthMap[maskY][maskX]
|
||||
|
||||
// Giả lập điểm lấy nét (focalPoint) cố định quanh đối tượng là 0.25f
|
||||
val relativeDistance = (currentDepth - 0.25f).coerceAtLeast(0f)
|
||||
|
||||
// Chọn lớp màu nền mờ dựa theo độ sâu DoF co giãn động theo thước trượt
|
||||
// Khi tiêu cự lớn (rulerValue cao), trường ảnh mỏng đi -> Vùng lấy nét hẹp lại
|
||||
val dofThreshold1 = 0.10f * (1.0f - normalizedSlider * 0.5f)
|
||||
val dofThreshold2 = 0.35f * (1.0f - normalizedSlider * 0.5f)
|
||||
|
||||
val bgPixel = when {
|
||||
relativeDistance < dofThreshold1 -> blurStage1.getPixel(x, y)
|
||||
relativeDistance < dofThreshold2 -> blurStage2.getPixel(x, y)
|
||||
else -> blurStage3.getPixel(x, y) // Vô cực (Mờ đại Bokeh)
|
||||
}
|
||||
|
||||
val fgPixel = capturedBitmap.getPixel(x, y)
|
||||
|
||||
// Khử răng cưa và trộn Alpha Blending
|
||||
val outR = (fgPixel.r * alpha + bgPixel.r * (1.0f - alpha)).toInt()
|
||||
val outG = (fgPixel.g * alpha + bgPixel.g * (1.0f - alpha)).toInt()
|
||||
val outB = (fgPixel.b * alpha + bgPixel.b * (1.0f - alpha)).toInt()
|
||||
|
||||
outputBitmap.setPixel(x, y, Color.rgb(outR, outG, outB))
|
||||
}
|
||||
}
|
||||
|
||||
// Dọn dẹp bộ đệm RAM
|
||||
blurStage1.recycle()
|
||||
blurStage2.recycle()
|
||||
blurStage3.recycle()
|
||||
|
||||
return outputBitmap
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 3. Cơ Chế Điều Khiển Cử Chỉ Swipe Đồng Bộ Trên Kính Ngắm
|
||||
|
||||
Khi áp dụng hàm số mũ này, các thao tác **Vuốt màn hình sang phải (Tăng) hay sang trái (Giảm)** sẽ thay đổi thông số `rulerValue` từ `0` đến `100` cực kỳ tuyến tính và đều đặn bằng tay, nhưng hiệu ứng thị giác trên màn hình sẽ chuyển đổi mượt mà:
|
||||
|
||||
* **Vuốt nhẹ đoạn đầu (0 -> 15):** Bạn sẽ thấy phông nền chỉ chuyển dịch nhẹ từ Lens góc rộng 24mm sang Lens 50mm, phông nền mờ rất khẽ, không bị nhảy vọt một cách bất ngờ như trước.
|
||||
* **Vuốt mạnh về sau (50 -> 100):** Độ mờ tăng tốc nhanh chóng để đạt hiệu ứng nén hậu cảnh tối đa của các dòng ống kính siêu tele 400mm chuyên nghiệp.
|
||||
|
||||
---
|
||||
|
||||
## 📅 4. Checklist Xác Minh Bản Sửa Đổi (Verification Checklist)
|
||||
|
||||
* [ ] **Test mốc +1 đến +5:** Kéo thanh trượt hoặc vuốt màn hình lên mức +1 và +5 xem phông nền phía sau đã giữ được độ chi tiết nhẹ nhàng của ống kính tiêu cự ngắn chưa. (Yêu cầu: Nền chỉ hơi mượt đi chứ tuyệt đối không được nhòe tịt như trước).
|
||||
* [ ] **Test mốc tối đa +100:** Kéo thước lên mức tối đa xem phông nền có đạt được độ xóa mịn đặc trưng của các dòng ống kính tele chụp xa hay không.
|
||||
* [ ] **Kiểm thử vùng chuyển tiếp DoF:** Đảm bảo khi kéo thước tăng dần, ranh giới giữa vùng nét (người) sang vùng mờ nhẹ (vật ở gần) và vùng mờ tịt (vật ở xa) co giãn tự nhiên, không bị xuất hiện các vệt cắt khối loang lổ.
|
||||
Reference in New Issue
Block a user