Làm ảnh thẻ bằng AI ngay trong trình duyệt: quy trình xây dựng, số đo và các lỗi đã gặp
1. Bài toán và ràng buộc
Ảnh thẻ có yêu cầu khá cứng: nền đơn sắc (trắng, xanh, đỏ tuỳ nơi nhận), mặt nhìn thẳng, đầu không nghiêng, đầu chiếm một tỉ lệ nhất định chiều cao ảnh, kích thước và độ phân giải in chuẩn. Quy trình thủ công trong một trình chỉnh sửa ảnh gồm: tách người khỏi nền, xoay cho thẳng, cắt theo khổ, tô nền, làm nét. Mục tiêu của công cụ là tự động hoá chuỗi này với các ràng buộc sau.
- Chạy hoàn toàn trên trình duyệt. Không có backend: ảnh khuôn mặt là dữ liệu cá nhân, và không phải gửi ảnh đi đâu cũng đồng nghĩa không phải vận hành, trả tiền hay bảo vệ một máy chủ.
- Hosting tĩnh. Website nằm trên GitHub Pages. Trang này không đặt được header
Cross-Origin-Opener-PolicyvàCross-Origin-Embedder-Policy, nên trình duyệt không cấpSharedArrayBuffervà WebAssembly chỉ chạy một luồng. Đây là ràng buộc quyết định tới lựa chọn mô hình ở Mục 2. - Giấy phép dùng thương mại được. Sản phẩm nằm trên website kinh doanh, nên chỉ chọn thành phần có giấy phép cho phép.
- Chất lượng ưu tiên hơn tốc độ, nhưng thời gian chờ phải ở mức người dùng chấp nhận được (chục giây, không phải phút).
2. Chọn công nghệ
Danh sách ứng viên gồm MediaPipe, YuNet và SCRFD cho phát hiện mặt; MODNet, BiRefNet, IS-Net (đóng gói trong thư viện của IMG.LY), PICORN và RMBG cho tách nền; Real-ESRGAN, SwinIR và OpenCV.js cho làm nét; ONNX Runtime Web, Transformers.js và WebGPU làm môi trường chạy. Tôi chọn theo bốn câu hỏi: có đủ thông tin hình học không, giấy phép có cho dùng thương mại không, nặng bao nhiêu, và có chạy tốt trên WebAssembly một luồng không.
| Khâu | Đã chọn | Lý do | Đã loại |
|---|---|---|---|
| Phát hiện và căn thẳng mặt | MediaPipe Face Landmarker (478 điểm, Apache-2.0, 3,76 MB) | Cho đồng thời tâm hai mắt (iris), cằm và hai bên mặt: đủ để tính góc nghiêng và chiều cao đầu bằng một mô hình | YuNet, SCRFD: chỉ có khung và vài điểm, không đủ để đo tỉ lệ đầu |
| Tách nền | MODNet (Apache-2.0, ONNX fp16, 12,98 MB) | Chuyên chân dung, không cần trimap, đầu ra là alpha mềm (có tóc) | IMG.LY dùng giấy phép AGPL. RMBG giới hạn phi thương mại. BiRefNet, PICORN: chưa thử trong bài này vì ngân sách thời gian với WebAssembly một luồng |
| Chạy mô hình | ONNX Runtime Web 1.20.1, WebAssembly, trong Web Worker | MIT, ổn định, nạp từ CDN nên không làm repo nặng thêm | WebGPU: chưa phổ biến đủ để làm đường chạy chính |
| Làm nét | Unsharp mask tự viết trên canvas | Ảnh thẻ chỉ cần làm nét nhẹ; super-resolution sinh chi tiết giả và rất nặng khi chạy trong trình duyệt | Real-ESRGAN, SwinIR (ghi vào hướng phát triển) |
| Làm mịn viền | Guided filter + Blur-Fusion | Hai thuật toán ngắn, tự cài được, không cần OpenCV.js (nhiều MB) | OpenCV.js |
Hai điểm cần nhấn mạnh. Thứ nhất, thông tin đầy đủ nhất về giấy phép nằm ở kho mã nguồn của từng mô hình, không phải ở bài tổng hợp, nên tôi đọc lại giấy phép của MODNet, MediaPipe và ONNX Runtime trước khi quyết định. Thứ hai, “nhẹ nhất” không đồng nghĩa “tốt nhất cho bài toán này”: một bộ phát hiện chỉ trả khung mặt không cho biết đầu nghiêng bao nhiêu độ.
3. Thử mô hình ngoài trình duyệt trước khi viết giao diện
Trước khi viết dòng JavaScript nào, tôi dùng Python và onnxruntime để kiểm tra phần tiền xử lý, hậu xử lý và so ba biến thể của MODNet trên một ảnh chân dung phạm vi công cộng. Quy ước đầu vào: ảnh RGB chuẩn hoá (x − 0,5) / 0,5, bố cục NCHW, cạnh ngắn bằng 512, cả hai cạnh chia hết cho 32; đầu ra là một kênh alpha trong đoạn [0, 1].
| Biến thể | Dung lượng | Sai khác trung bình so với fp32 | Phân vị 99 | Nhận xét |
|---|---|---|---|---|
| fp32 | 25,9 MB | — | — | chuẩn |
| fp16 (trọng số fp16, vào/ra float32) | 13,0 MB | 0,0001 | 0,0010 | giống fp32 ở mọi mục đích thực tế |
| int8 (lượng tử hoá) | 6,6 MB | 0,161 | 0,9998 | hỏng: alpha trung bình 0,377 so với 0,532 |
Kết quả khá bất ngờ nếu đã quen với kết quả int8 tốt ở các bộ phân loại nhỏ (xem bài về lượng tử hoá int8): một mạng matting dày đặc, nơi mỗi điểm ảnh cần một giá trị liên tục chính xác, nhạy hơn nhiều với việc làm thô trọng số và kích hoạt. Tôi dùng bản fp16: giảm một nửa dung lượng mà gần như không đổi kết quả. Lưu ý đo thời gian trên CPU native cho một ảnh duy nhất: fp32 2,88 s, fp16 4,23 s, int8 4,18 s. Bản fp16 không nhanh hơn trên CPU vì phải đổi kiểu số; lợi ích của nó là dung lượng tải, không phải tốc độ.
4. Kiến trúc: hai pha
Việc nặng (phát hiện mặt, tách nền) làm một lần cho mỗi ảnh. Các thông số người dùng hay chỉnh (khổ ảnh, màu nền, tỉ lệ đầu, độ nét, độ mềm viền) chỉ ảnh hưởng tới phần dựng ảnh, nên được tách riêng để kéo thanh trượt là thấy kết quả ngay.
Pha 1 - phân tích (một lần cho mỗi ảnh, ~15-25 s)
ảnh -> giải mã (tôn trọng EXIF) -> MediaPipe: 478 điểm -> góc nghiêng, hai mắt, cằm
-> MODNet (ONNX Runtime Web, worker): alpha thô ở độ phân giải 512
-> phóng alpha về kích thước ảnh gốc -> guided filter bám cạnh ảnh gốc
-> hình học: tâm mắt, góc theta, chiều cao đầu (đỉnh sọ -> cằm)
Pha 2 - dựng ảnh (tức thì, chạy lại mỗi khi đổi thông số)
xoay + co giãn + căn giữa -> khung khổ ảnh thẻ
-> ước lượng màu tiền cảnh (Blur-Fusion) -> ghép lên màu nền
-> cân bằng sáng (tuỳ chọn) -> unsharp mask -> PNG/JPG 300 DPI + tờ in
Toàn bộ mã nằm trong một tệp JavaScript (khoảng 520 dòng) không phụ thuộc thư viện nào ngoài ONNX Runtime Web và MediaPipe, cả hai nạp từ CDN. Hai mô hình (modnet-fp16.onnx 12,98 MB và face_landmarker.task 3,76 MB) nằm trong repo của website. Lần đầu mở công cụ phải tải tổng cộng khoảng 37 MB, gồm cả WebAssembly của hai thư viện; các lần sau trình duyệt dùng bản lưu.
5. Hình học: xoay, tỉ lệ đầu và vị trí
5.1 Góc nghiêng từ hai mắt
Face Landmarker trả 478 điểm, trong đó điểm 468 và 473 là tâm hai mắt. Với \(\mathbf e_A\) và \(\mathbf e_B\) là toạ độ hai tâm mắt (đã sắp theo thứ tự trái sang phải trong ảnh), góc nghiêng và khoảng cách hai mắt là
$$\theta=\operatorname{atan2}(e_{B,y}-e_{A,y},\;e_{B,x}-e_{A,x}),\qquad E=\lVert\mathbf e_B-\mathbf e_A\rVert.$$Mỗi điểm \(\mathbf p\) được quy về hệ toạ độ “đã xoay thẳng” có gốc tại trung điểm hai mắt \(\mathbf c_0\):
$$\begin{pmatrix}x'\\y'\end{pmatrix}=\begin{pmatrix}\cos\theta&\sin\theta\\-\sin\theta&\cos\theta\end{pmatrix}\begin{pmatrix}p_x-c_{0,x}\\p_y-c_{0,y}\end{pmatrix}.$$Trong hệ này, hai mắt nằm trên cùng một hàng ngang, và khoảng cách từ đường mắt đến cằm (điểm 152) là \(H_{ec}=y'_{\text{cằm}}\).
5.2 Đỉnh sọ: đo từ alpha, kẹp bằng tỉ lệ cơ thể
Chiều cao đầu ảnh thẻ tính từ đỉnh sọ đến cằm, nhưng Face Landmarker không có điểm đỉnh sọ đáng tin (tóc che). Tôi lấy đỉnh sọ từ matte: xoay alpha vào hệ đã thẳng, quét từ hàng mắt lên trên trong một dải cột quanh tâm mặt, hàng đầu tiên có ít nhất 3 điểm alpha lớn hơn 0,5 là đỉnh sọ. Với tóc xù, mũ hay lỗi tách nền, kết quả này có thể sai nhiều, nên có hai lớp bảo vệ. Nếu matte không cho kết quả, dùng ước lượng \(y'_{\text{đỉnh}}=-1{,}17\,H_{ec}\). Nếu matte cho kết quả ngoài khoảng \([-1{,}6\,H_{ec},\,-0{,}95\,H_{ec}]\), kẹp vào khoảng đó. Hệ số 1,17 và hai cận là hệ số thực nghiệm của tôi, rút ra từ tỉ lệ khoảng cách mắt-cằm so với chiều cao đầu; nó không phải một chuẩn quy định.
5.3 Phép biến đổi vào khung ảnh thẻ
Gọi \(\rho\) là tỉ lệ đầu (chiều cao đầu trên chiều cao ảnh) và \(h_{out}\) là chiều cao ảnh đầu ra tính bằng điểm ảnh. Tỉ lệ co giãn là \(\sigma=\rho\,h_{out}/H_{head}\), với \(H_{head}=H_{ec}-y'_{\text{đỉnh}}\). Phép biến đổi được cài thành ma trận cho canvas:
$$a=\sigma\cos\theta,\quad b=-\sigma\sin\theta,\quad c=\sigma\sin\theta,\quad d=\sigma\cos\theta,$$và dịch sao cho điểm nằm trên đường đỉnh sọ, ngay trên tâm mặt, rơi vào toạ độ \((w_{out}/2,\;m)\) với khoảng đệm phía trên \(m=h_{out}(1-\rho)\cdot 0{,}36\). Hệ số 0,36 chia phần còn lại của ảnh (không thuộc đầu) theo tỉ lệ khoảng 36% phía trên đầu và 64% phía dưới (cổ và vai). Khổ và tỉ lệ đầu mặc định:
| Khổ | mm | Điểm ảnh ở 300 DPI | Tỉ lệ đầu mặc định \(\rho\) |
|---|---|---|---|
| 3×4 | 30×40 | 354×472 | 0,70 |
| 4×6 | 40×60 | 472×709 | 0,72 |
| 2×3 | 20×30 | 236×354 | 0,70 |
| 35×45 | 35×45 | 413×531 | 0,75 |
| 2×2 inch | 50,8×50,8 | 600×600 | 0,60 |
Các tỉ lệ này là giá trị mặc định thông dụng, người dùng chỉnh được bằng thanh trượt; quy định chính xác tuỳ cơ quan tiếp nhận nên cần đối chiếu trước khi nộp.
var scale = ratio * outH / g.headPx;
var topMargin = outH * (1 - ratio) * 0.36;
var a = scale * cs, b = -scale * sn, c = scale * sn, d = scale * cs;
var e = outW / 2 - (a * P0x + c * P0y), f = topMargin - (b * P0x + d * P0y);
rctx.setTransform(a, b, c, d, e, f); // vẽ ảnh gốc vào khung ảnh thẻ
rctx.drawImage(S.src, 0, 0);
6. Làm sạch viền: từ alpha thô đến ảnh ghép
6.1 Guided filter
MODNet chạy ở cạnh ngắn 512 điểm ảnh, nên alpha thô, răng cưa ở rìa tóc khi phóng lên ảnh gốc. Guided filter (He, Sun, Tang, 2013) lọc alpha \(p\) với một ảnh dẫn \(I\) (ở đây là ảnh xám của ảnh gốc), nên cạnh của alpha bám theo cạnh thật của ảnh. Trong mỗi cửa sổ, đầu ra là một hàm tuyến tính của ảnh dẫn:
$$q_i=\bar a_i I_i+\bar b_i,\qquad a_k=\frac{\operatorname{cov}_k(I,p)}{\operatorname{var}_k(I)+\varepsilon},\qquad b_k=\bar p_k-a_k\bar I_k,$$với \(\bar a_i,\bar b_i\) là trung bình của \(a_k,b_k\) trên các cửa sổ chứa điểm \(i\). Tôi cài bằng bộ lọc hộp tách biệt hai chiều (tính theo tổng trượt, nên thời gian không phụ thuộc bán kính), với \(r=\max(3,\operatorname{round}(\max(W,H)/260))\) và \(\varepsilon=10^{-4}\).
6.2 Ước lượng màu tiền cảnh (Blur-Fusion)
Cho dù alpha chính xác, việc ghép \(C=\alpha F+(1-\alpha)B'\) với \(B'\) là màu nền mới vẫn lộ viền nếu lấy luôn màu ảnh gốc \(I\) làm \(F\): điểm ảnh ở rìa tóc là hỗn hợp của tóc và nền cũ, nên viền mang màu nền cũ (viền trắng quanh tóc khi đổi sang nền xanh). Cần ước lượng màu tiền cảnh \(F\) bằng cách loại phần nền cũ ra khỏi các điểm ảnh bán trong suốt. Phương pháp Blur-Fusion (Forte, 2021) làm điều đó bằng một vài bước làm mờ:
$$\hat F=\frac{\overline{F\alpha}}{\bar\alpha}+\alpha\Bigl(I-\alpha\frac{\overline{F\alpha}}{\bar\alpha}-(1-\alpha)\frac{\overline{B(1-\alpha)}}{1-\bar\alpha}\Bigr),$$trong đó thanh trên là trung bình trong cửa sổ. Tôi chạy hai lượt như mã tham chiếu của tác giả: lượt đầu dùng cửa sổ lớn (bán kính \(\max(9,\,h_{out}/14)\) điểm ảnh), lượt sau dùng cửa sổ nhỏ (bán kính 3) trên kết quả của lượt đầu.
6.3 Làm nét và cân bằng sáng
Bước làm nét là unsharp mask: \(I'=I+k\,(I-G_\sigma*I)\), với bộ làm mờ Gauss xấp xỉ bằng ba lần lọc hộp, \(\sigma=\max(0{,}8,\,h_{out}/520)\), và bỏ qua chênh lệch nhỏ hơn 2 mức xám để không khuếch đại nhiễu. Hệ số \(k\) đề xuất tự động theo độ sắc của vùng mặt, đo bằng phương sai của toán tử Laplace \(\mathrm{Var}(\nabla^2 I)\): dưới 25 thì \(k=1{,}4\), dưới 60 thì 1,1, dưới 140 thì 0,75, còn lại 0,45. Nhờ đó ảnh đã nét không bị làm quá, và ảnh mờ được làm nét mạnh hơn. Cân bằng sáng (tuỳ chọn) kéo giãn đoạn từ phân vị 1 đến phân vị 99 của độ sáng vùng chủ thể, rồi trộn 60% với ảnh gốc để không làm mất tự nhiên.
7. Chạy mô hình trong trình duyệt
Trên GitHub Pages không có SharedArrayBuffer, nên ONNX Runtime Web chạy một luồng. Phiên bản đầu tiên chạy suy luận trên luồng chính và làm đơ cả trang trong lúc tách nền; công cụ kiểm thử còn bị “timeout” vì trang không phản hồi. Đặt ort.env.wasm.proxy = true chuyển suy luận vào một Web Worker và giải quyết triệt để, trang vẫn kéo thanh trượt, đổi ngôn ngữ được trong lúc chờ. Mã khởi tạo:
libs.ort.env.wasm.wasmPaths = ORT_BASE; // jsDelivr, onnxruntime-web 1.20.1
libs.ort.env.wasm.proxy = true; // suy luận chạy trong worker
libs.ort.env.wasm.numThreads = crossOriginIsolated ? Math.min(4, hw) : 1;
libs.session = await libs.ort.InferenceSession.create(modelBuf,
{ executionProviders: ['wasm'], graphOptimizationLevel: 'all' });
Nếu sau này website chuyển sang nơi hosting cho phép hai header cách ly nguồn gốc, đoạn mã trên sẽ tự dùng tới 4 luồng.
8. Kết quả trên ảnh thật
Tôi kiểm thử bằng Chrome thật (điều khiển qua CDP), nạp ảnh vào ô chọn tệp như người dùng, đo thời gian từ lúc nạp tới khi có kết quả. Ảnh minh hoạ trong bài là chân dung vận động viên bơi lội Nguyễn Thị Ánh Viên, một ảnh nhìn thẳng, chọn vì có giấy phép cho phép chỉnh sửa và đăng lại. Dưới đây là ảnh gốc và ba kết quả cho cùng một ảnh, mỗi kết quả chạy hoàn toàn trong trình duyệt:




Với ảnh này công cụ không phải xoay (đã thẳng), tách được cả sợi tóc rời ở đỉnh đầu, đầu chiếm khoảng 70% chiều cao ở khổ 3×4 (đo bằng mắt trên ảnh đầu ra). Cùng một lần phân tích có thể xuất ra tờ in 10×15 cm với chín ảnh 3×4:
Ca nghiêng đầu
Ảnh bên dưới là một ảnh tự chụp, đầu nghiêng và quay nhẹ. Công cụ đo góc nghiêng \(-18{,}5^\circ\) và xoay thẳng; đây là ảnh minh hoạ cho phép căn thẳng, không phải ví dụ về một ảnh thẻ hợp lệ (tư thế quay nghiêng không đạt yêu cầu mặt nhìn thẳng của ảnh thẻ thật).


Bảng thời gian và các ca khác
| Ảnh kiểm thử | Khổ / nền | Thời gian | Kết quả |
|---|---|---|---|
| Chân dung NASA 700×900 | 3×4 / xanh | 17,5 s | tự xoay \(-5{,}4^\circ\), viền sạch; có cảnh báo “độ phân giải mặt thấp” vì ảnh nhỏ |
| Cùng ảnh, xoay thêm \(14^\circ\), 1600 px | 35×45 / đỏ | 20,1 s | đo \(-19{,}3^\circ\) (bằng \(14^\circ\) cộng \(5{,}4^\circ\) nghiêng sẵn có), kết quả thẳng |
| Phi hành gia trong bộ đồ trắng | 2×2 inch / trắng | 27,3 s | tách đầu tốt; bộ đồ trắng giữ lại vì là tiền cảnh, mép đồ nhoè nhẹ vào nền trắng |
| Ảnh tự chụp nghiêng đầu (ở trên) | 3×4 / xanh | 21,6 s | đo \(-18{,}5^\circ\), xoay thẳng |
| Ánh Viên (ở trên) | 3×4 / xanh | 18,0 s | đạt, không phải xoay |
| Ánh Viên | 35×45 / trắng | 20,0 s | đạt; còn một quầng sáng rất nhẹ quanh tai và tóc |
| Ánh Viên | 3×4 / đỏ | 104,8 s | kết quả đạt; thời gian chênh hẳn các lần khác, chưa xác định nguyên nhân |
| Ảnh mèo (không có mặt) | 3×4 | 6,8 s | báo lỗi đúng: không tìm thấy khuôn mặt |
Sáu lần chạy cho ảnh có mặt (không kể lần 104,8 s) nằm trong khoảng 17,5-27,3 s, trung vị khoảng 20 s, đo trên một máy thử với WebAssembly một luồng. Đây là số liệu từ một máy, không phải benchmark; máy khác sẽ cho con số khác. Sau khi triển khai, tôi chạy lại một ảnh trên chính shopsmon.com (khổ 4×6, nền trắng): mô hình và thư viện tải được từ CDN, tờ in xếp bốn ảnh. Mọi lần chạy không phát sinh lỗi trong console.
9. Các lỗi đã gặp và cách sửa
| Hiện tượng | Nguyên nhân | Cách sửa |
|---|---|---|
| Trang đơ khi tách nền, công cụ kiểm thử báo timeout | Suy luận chạy trên luồng chính | ort.env.wasm.proxy = true (Web Worker) |
| Đầu chỉ chiếm khoảng 58% thay vì 70% | Khoảng kẹp đỉnh sọ sai: \([-2{,}1,\,-1{,}45]\cdot H_{ec}\) | Tính lại thành \([-1{,}6,\,-0{,}95]\cdot H_{ec}\) và ước lượng \(-1{,}17\,H_{ec}\); sau sửa, đầu chiếm khoảng 72% ở ảnh thử |
| Alpha sai hoàn toàn với MODNet int8 | Lượng tử hoá làm hỏng mạng matting | Dùng bản fp16 |
Phần tử có thuộc tính hidden vẫn hiện | CSS display:grid đè lên thuộc tính | Thêm [hidden]{display:none!important} |
Công cụ sinh trang thỉnh thoảng báo lỗi OSError 22 khi ghi tệp trên Windows | Lỗi tạm thời khi tiến trình khác quét tệp | Vòng thử lại tối đa 8 lần |
| Trích dẫn sai tác giả của Blur-Fusion | Ghi thêm một tác giả không có trong bài | Kiểm tra DOI, sửa thành một tác giả (M. Forte) |
Lỗi thứ hai đáng kể nhất: nó chỉ lộ ra khi nhìn ảnh đầu ra chứ không lộ ra ở bất kỳ phép kiểm tra tự động nào, vì chương trình vẫn chạy trơn tru và ảnh trông “hợp lý”. Tôi chỉ phát hiện khi đo tỉ lệ đầu trên ảnh xuất ra và so với giá trị đặt (70%). Bài học: với sản phẩm xử lý ảnh, phải mở ảnh đầu ra và đo, không chỉ kiểm tra “không có lỗi”.
10. Giới hạn
- Nền cùng màu với quần áo (đồ trắng trên nền trắng) có thể khiến một phần nền hoặc quần áo bị nhận nhầm; kết quả ở ca phi hành gia ở trên là ví dụ.
- Trên nền trắng đôi khi còn một quầng sáng rất nhẹ quanh tai và tóc, do alpha ở rìa không tuyệt đối chính xác.
- Ảnh nhỏ cho ảnh thẻ mờ khi phóng; công cụ cảnh báo thay vì tạo chi tiết giả.
- Đỉnh sọ phụ thuộc vào matte nên tóc xù, mũ, khăn trùm đầu làm giảm độ chính xác của tỉ lệ đầu; thanh trượt tỉ lệ đầu cho người dùng chỉnh tay.
- Công cụ không kiểm tra mắt nhắm, miệng há hay kính phản chiếu; cơ quan tiếp nhận có thể từ chối ảnh dù đúng khổ.
- Thời gian xử lý khoảng 15-30 s trên một luồng; tải lần đầu khoảng 37 MB.
- Chưa kiểm thử trên điện thoại thật, Safari và Firefox.
11. Hướng phát triển
- WebGPU cho ONNX Runtime Web, hoặc chuyển hosting sang nơi cho phép
SharedArrayBufferđể dùng nhiều luồng. - Thêm tuỳ chọn “chất lượng cao” với mô hình matting lớn hơn (như BiRefNet), chỉ tải khi chọn.
- Super-resolution nhỏ (ví dụ Real-ESRGAN bản nhẹ) chỉ khi ảnh gốc quá nhỏ.
- Cảnh báo chất lượng trước khi tạo: mắt nhắm, nghiêng đầu nhiều, kính, mũ.
- Thêm khổ theo quy định từng quốc gia và mẫu tờ in.
12. Quy trình rút ra để dùng lại
- Chốt ràng buộc (hosting, giấy phép, ngân sách thời gian) trước khi so mô hình.
- Loại sớm thành phần có giấy phép không phù hợp.
- Thử mô hình bằng Python, chốt tiền xử lý và biến thể số (fp32, fp16, int8), rồi mới viết JavaScript.
- Tách pha nặng và pha nhẹ; pha nặng chạy trong worker.
- Kiểm thử đầu-cuối trên trình duyệt thật bằng ảnh có nguồn và giấy phép rõ ràng; mở ảnh đầu ra và đo.
- Kiểm thử ca xấu: không có mặt, ảnh xoay, nền trùng màu, ảnh nhỏ.
- Triển khai, chờ CDN cập nhật, chạy lại thử nghiệm trên bản live.
Tôi nhận làm với tư cách freelancer. Mô tả ý tưởng trong vài câu, tôi sẽ trả lời bằng các câu hỏi và báo giá trọn gói theo phạm vi cố định.
Dùng thử công cụ Nhận báo giá
Tài liệu tham khảo
- Z. Ke, J. Sun, K. Li, Q. Yan và R. W. H. Lau. “MODNet: Real-Time Trimap-Free Portrait Matting via Objective Decomposition.” Proceedings of the AAAI Conference on Artificial Intelligence, 36(1), 2022. doi:10.1609/aaai.v36i1.19999.
- K. He, J. Sun và X. Tang. “Guided Image Filtering.” IEEE Transactions on Pattern Analysis and Machine Intelligence, 35(6), 2013, tr. 1397–1409.
- M. Forte. “Approximate Fast Foreground Colour Estimation.” IEEE International Conference on Image Processing (ICIP), 2021. doi:10.1109/ICIP42928.2021.9506164.
- Google. “MediaPipe Face Landmarker.” Tài liệu Google AI Edge, ai.google.dev/edge/mediapipe.
- Microsoft. “ONNX Runtime Web.” onnxruntime.ai/docs/tutorials/web.