Files
photobooth-app/16_REPLACE_BACKGROUND.md
T

230 lines
11 KiB
Markdown

Dưới đây là kế hoạch Markdown tổng hợp toàn diện, kết hợp từ hạ tầng giao diện, kiến trúc luồng dữ liệu cho đến thuật toán xử lý pixel nâng cao để triển khai tính năng **Chân dung xóa phông (Portrait Blur)****Thay thế phông nền AI (Background Replacement)**, đảm bảo loại bỏ hoàn toàn hiện tượng răng cưa và lộ vết cắt ở các chi tiết phức tạp như tóc của người hoặc vật.
---
# 📊 KẾ HOẠCH TOÀN DIỆN: TÍNH NĂNG CHÂN DUNG XÓA/THAY NỀN VÀ KHỬ LỖI CẮT VIỀN TÓC CHUYÊN NGHIỆP
Mục tiêu cốt lõi:
1. Cung cấp bộ công cụ tối giản ở thanh trượt Ruler dưới đáy màn hình để chọn chế độ nền.
2. Tích hợp mô hình AI On-Device để bóc tách thực thể người/vật.
3. Sử dụng các bộ toán học xử lý pixel chuyên sâu nhằm xử lý vùng biên, khử lem màu nền cũ và hòa trộn hạt màu mềm mại.
4. Tích hợp mượt mà vào chuỗi xử lý (Chụp ➔ Can thiệp AI ➔ Tinh chỉnh màu Sliders ➔ Ép khung Instax ➔ Lưu Gallery).
---
## 📦 1. Bổ Sung & Cấu Hình Thư Viện (`pubspec.yaml`)
Thêm bộ thư viện AI phân đoạn hình ảnh của Google và công cụ ma trận điểm ảnh vào tệp cấu hình:
```yaml
dependencies:
flutter:
sdk: flutter
camera: ^0.10.6
google_mlkit_selfie_segmentation: ^0.7.0 # AI tách nền chạy offline
image: ^4.3.0 # Xử lý pixel, Blur và Blend màu
gal: ^2.4.0 # Lưu ảnh trực tiếp vào thư viện máy
flutter:
assets:
- assets/frames/instaxframe.png
- assets/backgrounds/background-01.png # Ảnh nền phong cảnh mẫu
- assets/backgrounds/background-02.png
- assets/backgrounds/background-03.png
```
---
## 📐 2. Thiết Kế Giao Diện Điều Khiển (UI/UX Matrix)
Tại Tầng 2 của Editor (Thanh cuộn ngang 14 nút thuộc tính), ta bổ sung 2 công cụ điều khiển:
* **Icon Portrait (Xóa phông):** Khi chọn, thước đo Ruler sẽ cho phép tăng giảm độ mịn của nền từ `0` đến `100` (tương ứng giá trị blur từ `0.0` đến `1.0`).
* **Icon BG Replace (Thay nền):** Khi nhấn chọn, một dải menu con cuộn ngang sẽ hiển thị danh sách các ảnh nền mẫu để người dùng áp dụng.
### Quản lý trạng thái hệ thống:
```dart
enum BackgroundMode { none, blur, replace }
BackgroundMode _selectedBgMode = BackgroundMode.none;
double _blurIntensity = 0.0; // Độ mờ phông nền (0.0 -> 1.0)
String? _selectedBgAssetPath; // Đường dẫn ảnh nền mới được chọn
bool _isFrameApplied = false; // Trạng thái ép khung Instax
```
---
## 💻 3. Thuật Toán Lõi: Xử Lý Viền Tóc & Chống Răng Cưa (Core Engine)
Để tóc của nhân vật hoặc các chi tiết nhỏ của vật thể không bị lộ vết cắt thô bạo, thuật toán áp dụng 3 kỹ thuật song song: **Feathering (Làm mềm biên)**, **Color Decontamination (Khử lem màu nền cũ)****Alpha Blending (Hòa trộn ma trận màu)**.
Hàm xử lý dưới đây sẽ chạy độc lập để trả ra một bức ảnh sạch tuyệt đối:
```dart
import 'dart:io';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:image/image.dart' as img;
import 'package:google_mlkit_selfie_segmentation/google_mlkit_selfie_segmentation.dart';
Future<img.Image> _processAdvancedBackgroundAi({
required img.Image capturedImage,
required String rawPhotoPath,
}) async {
// 1. Khởi chạy AI để trích xuất Mặt nạ phân đoạn (Segmentation Mask)
final segmenter = SelfieSegmenter(mode: SegmenterMode.single, enableRawSizeMask: true);
final inputImage = InputImage.fromFilePath(rawPhotoPath);
final mask = await segmenter.processImage(inputImage);
if (mask == null) return capturedImage;
final w = mask.width;
final h = mask.height;
final confidences = mask.confidences; // Mảng chứa độ tin cậy thực thể (0.0 -> 1.0)
// Đưa ảnh gốc về cùng kích thước ma trận AI để tính toán đồng bộ từng điểm ảnh
img.Image fg = img.copyResize(capturedImage, width: w, height: h);
img.Image bg = img.Image(width: w, height: h);
img.Image output = img.Image(width: w, height: h);
// 2. Chuẩn bị phông nền phụ thuộc vào chế độ người dùng chọn
if (_selectedBgMode == BackgroundMode.blur) {
// CHẾ ĐỘ XÓA PHÔNG: Nhân bản ảnh gốc và làm mịn bằng bộ lọc Gaussian Blur bán kính lớn
bg = img.copyResize(fg, width: w, height: h);
int blurRadius = (_blurIntensity * 25).toInt().clamp(1, 25);
bg = img.gaussianBlur(bg, radius: blurRadius);
} else if (_selectedBgMode == BackgroundMode.replace && _selectedBgAssetPath != null) {
// CHẾ ĐỘ THAY NỀN: Nạp ảnh nền mới và co giãn cho khớp với ảnh chụp
final ByteData bgData = await rootBundle.load(_selectedBgAssetPath!);
img.Image? decodedBg = img.decodeImage(bgData.buffer.asUint8List());
if (decodedBg != null) {
bg = img.copyResize(decodedBg, width: w, height: h);
}
} else {
segmenter.close();
return capturedImage; // Không bật tính năng -> Trả về ảnh gốc
}
// 3. VÒNG LẶP DUYỆT PIXEL: KHỬ RĂNG CƯA & TRỘN MÀU MẢNH TÓC
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
final int idx = y * w + x;
double alpha = confidences[idx];
// KHÁI NIỆM VÙNG BIÊN: Nơi có điểm số AI mập mờ từ 0.1 (Nền) đến 0.9 (Người)
// --- THUẬT TOÁN 1: FEATHERING (LÀM MỀM BIÊN MẶT NẠ) ---
if (alpha > 0.1 && alpha < 0.9) {
alpha = smoothStep(0.1, 0.9, alpha); // Nội suy giúp viền tóc mềm mại hơn
}
final fgPixel = fg.getPixel(x, y);
final bgPixel = bg.getPixel(x, y);
int fgR = fgPixel.r.toInt();
int fgG = fgPixel.g.toInt();
int fgB = fgPixel.b.toInt();
final int bgR = bgPixel.r.toInt();
final int bgG = bgPixel.g.toInt();
final int bgB = bgPixel.b.toInt();
// --- THUẬT TOÁN 2: COLOR DECONTAMINATION (KHỬ LEM MÀU NỀN CŨ) ---
// Triệt tiêu ánh màu của phòng/nền cũ dính vào các kẽ tóc mảnh ở vùng viền
if (alpha > 0.1 && alpha < 0.85) {
final double grayIntensity = (fgR + fgG + fgB) / 3.0;
// Hút bớt độ rực màu bị nhiễu của môi trường cũ, ép sợi tóc tiến về màu trung tính
fgR = (fgR * alpha + grayIntensity * (1.0 - alpha)).toInt();
fgG = (fgG * alpha + grayIntensity * (1.0 - alpha)).toInt();
fgB = (fgB * alpha + grayIntensity * (1.0 - alpha)).toInt();
}
// --- THUẬT TOÁN 3: ALPHA BLENDING (HÒA TRỘN MA TRẬN PHÂN LỚP) ---
// Lồng hạt màu của sợi tóc vào kẽ hở của phông nền mới dựa trên tỷ lệ Alpha chuyển tiếp
final int outR = (fgR * alpha + bgR * (1.0 - alpha)).toInt();
final int outG = (fgG * alpha + bgG * (1.0 - alpha)).toInt();
final int outB = (fgB * alpha + bgB * (1.0 - alpha)).toInt();
output.setPixel(x, y, img.ColorRgb8(outR, outG, outB));
}
}
segmenter.close();
return output;
}
// Hàm bổ trợ tính toán Smoothstep cho dải biên chuyển tiếp đều đặn
double smoothStep(double edge0, double edge1, double x) {
final double t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}
```
---
## 🔄 4. Đồng Bộ Vào Chuỗi Pipeline Xử Lý Đồ Họa Khi Chụp Ảnh
Chuỗi sự kiện bất đồng bộ khi người dùng nhấn nút Shutter (được ẩn toàn bộ các thông báo `"Tách!..."` thừa thải) sẽ tuân theo cấu trúc tuyến tính khép kín sau:
```dart
Future<void> captureAndProcessImagePipeline() async {
try {
// BƯỚC 1: Chụp ảnh thô từ phần cứng camera
final XFile rawPhoto = await _cameraController.takePicture();
// BƯỚC 2: Đọc bytes ảnh vào bộ nhớ hệ thống
final Uint8List rawBytes = await rawPhoto.readAsBytes();
img.Image? workingImage = img.decodeImage(rawBytes);
if (workingImage == null) return;
// BƯỚC 3: CAN THIỆP AI - Xóa/Thay nền khử lỗi viền tóc (Như thuật toán ở Mục 3)
if (_selectedBgMode != BackgroundMode.none) {
workingImage = await _processAdvancedBackgroundAi(
capturedImage: workingImage,
rawPhotoPath: rawPhoto.path,
);
}
// BƯỚC 4: ĐỒNG BỘ SLIDERS - Áp dụng cấu hình bộ lọc màu (JSON Cũ)
// Thực hiện sau khi tách nền để màu của người và nền hòa quyện đồng nhất
workingImage = _applyUserColorSliders(workingImage);
// BƯỚC 5: MERGE FRAME - Ép mảnh khung ảnh Instax PNG trong suốt lên trên cùng
if (_isFrameApplied) {
final ByteData frameData = await rootBundle.load('assets/frames/instaxframe.png');
final img.Image? instaxFrame = img.decodePng(frameData.buffer.asUint8List());
if (instaxFrame != null) {
workingImage = img.copyResize(workingImage, width: instaxFrame.width, height: instaxFrame.height);
img.compositeImage(workingImage, instaxFrame); // Ép đè khung cố định lên ảnh
}
}
// BƯỚC 6: XUẤT FILE TẠM & GHI VÀO GALLERY MÁY
final Uint8List finalBytes = Uint8List.fromList(img.encodeJpg(workingImage, quality: 95));
final String tempPath = '${Directory.systemTemp.path}/photobooth_${DateTime.now().millisecondsSinceEpoch}.jpg';
final File tempFile = File(tempPath);
await tempFile.writeAsBytes(finalBytes);
await Gal.putImage(tempFile.path); // Lưu trực tiếp vào thư viện ảnh hệ thống
await tempFile.delete(); // Giải phóng tệp tạm thời
_showSuccessOverlay(); // Hiển thị giao diện báo lưu thành công nhẹ nhàng
} catch (e) {
print("Lỗi hệ thống xử lý chuỗi ảnh: $e");
}
}
```
---
## 📅 5. Kế Hoạch Triển Khai Thực Địa (Checklist)
* [ ] **Đưa luồng xử lý vào Flutter Isolate:** Do vòng lặp pixel chạy qua hàng triệu điểm ảnh ở Bước 3 và Bước 5 rất nặng, bắt buộc phải dùng lệnh `compute()` hoặc `Isolate.run()` để đẩy toàn bộ tiến trình này xuống nhân CPU chạy ngầm, tránh gây đứng đứng hình giao diện kính ngắm.
* [ ] **Cấp quyền hệ thống đầy đủ:** Đảm bảo cấu hình các thẻ xin quyền lưu trữ vào thư viện máy đầy đủ trên cả hai nền tảng Android và iOS để `Gal` thực thi lệnh lưu ảnh an toàn.
* [ ] **Kiểm thử biên độ nhiễu màu:** Thực hiện chụp thử nghiệm các mẫu vật phức tạp (như gấu bông nhiều lông mịn hoặc nhân vật tóc xù) trước các hậu cảnh phức tạp để căn chỉnh thông số `smoothStep` đạt độ thẩm mỹ cao nhất.