Terjemahan disediakan oleh mesin penerjemah. Jika konten terjemahan yang diberikan bertentangan dengan versi bahasa Inggris aslinya, utamakan versi bahasa Inggris.
Menggunakan Penggantian Latar Belakang dengan IVS Broadcast SDK
Penggantian latar belakang adalah jenis filter kamera yang memungkinkan pembuat streaming langsung mengubah latar belakang mereka. Seperti yang ditunjukkan pada diagram berikut, mengganti latar belakang Anda melibatkan:
-
Mendapatkan gambar kamera dari umpan kamera langsung.
-
Mensegmentasinya ke komponen latar depan dan latar belakang menggunakan Google ML Kit.
-
Menggabungkan masker segmentasi yang dihasilkan dengan gambar latar belakang khusus.
-
Meneruskannya ke Sumber Gambar Kustom untuk disiarkan.
Web
Bagian ini mengasumsikan Anda sudah terbiasa dengan penerbitan dan berlangganan video menggunakan SDK Siaran Web.
Untuk mengganti latar belakang streaming langsung dengan gambar khusus, gunakan model segmentasi selfie
Untuk mengintegrasikan penggantian latar belakang dengan SDK siaran Web streaming real-time IVS, Anda perlu:
-
Instal MediaPipe dan Webpack. (Contoh kami menggunakan Webpack sebagai bundler, tetapi Anda dapat menggunakan bundler pilihan Anda.)
-
Buat
index.html. -
Tambahkan elemen media.
-
Tambahkan tag skrip.
-
Buat
app.js. -
Muat gambar latar belakang khusus.
-
Buat instans
ImageSegmenter. -
Render umpan video ke kanvas.
-
Buat logika penggantian latar belakang.
-
Buat File konfigurasi Webpack.
-
Bundel JavaScript file Anda.
Instal MediaPipe dan Webpack
Untuk memulai, instal paket @mediapipe/tasks-vision dan webpack npm. Contoh di bawah ini menggunakan Webpack sebagai JavaScript bundler; Anda dapat menggunakan bundler yang berbeda jika diinginkan.
npm i @mediapipe/tasks-vision webpack webpack-cli
Pastikan juga memperbarui package.json untuk menentukan webpack sebagai skrip build Anda:
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack" },
Buat index.html
Selanjutnya, buat boilerplate HTML dan impor SDK siaran Web sebagai tag skrip. Dalam kode berikut, pastikan untuk mengganti <SDK version> dengan versi SDK siaran yang Anda gunakan.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Import the SDK --> <script src="https://web-broadcast.live-video.net/<SDK version>/amazon-ivs-web-broadcast.js"></script> </head> <body> </body> </html>
Tambahkan Elemen Media
Selanjutnya, tambahkan elemen video dan dua elemen kanvas dalam tag tubuh. Elemen video akan berisi umpan kamera langsung Anda dan akan digunakan sebagai input ke Segmenter MediaPipe Gambar. Elemen kanvas pertama akan digunakan untuk membuat pratinjau umpan yang akan disiarkan. Elemen kanvas kedua akan digunakan untuk merender gambar kustom yang akan digunakan sebagai latar belakang. Karena kanvas kedua dengan gambar khusus hanya digunakan sebagai sumber untuk menyalin piksel secara terprogram dari itu ke kanvas akhir, itu disembunyikan dari pandangan.
<div class="row local-container"> <video id="webcam" autoplay style="display: none"></video> </div> <div class="row local-container"> <canvas id="canvas" width="640px" height="480px"></canvas> <div class="column" id="local-media"></div> <div class="static-controls hidden" id="local-controls"> <button class="button" id="mic-control">Mute Mic</button> <button class="button" id="camera-control">Mute Camera</button> </div> </div> <div class="row local-container"> <canvas id="background" width="640px" height="480px" style="display: none"></canvas> </div>
Menambahkan Tag Script
Tambahkan tag skrip untuk memuat JavaScript file yang dibundel yang akan berisi kode untuk melakukan penggantian latar belakang dan menerbitkannya ke tahap:
<script src="./dist/bundle.js"></script>
Buat app.js
Selanjutnya, buat JavaScript file untuk mendapatkan objek elemen untuk kanvas dan elemen video yang dibuat di halaman HTML. Impor FilesetResolver modul ImageSegmenter dan. ImageSegmenterModul ini akan digunakan untuk melakukan tugas segmentasi.
const canvasElement = document.getElementById("canvas"); const background = document.getElementById("background"); const canvasCtx = canvasElement.getContext("2d"); const backgroundCtx = background.getContext("2d"); const video = document.getElementById("webcam"); import { ImageSegmenter, FilesetResolver } from "@mediapipe/tasks-vision";
Selanjutnya, buat fungsi yang di init() panggil untuk mengambil MediaStream dari kamera pengguna dan memanggil fungsi callback setiap kali bingkai kamera selesai memuat. Tambahkan pendengar acara untuk tombol untuk bergabung dan meninggalkan panggung.
Perhatikan bahwa ketika bergabung dengan tahap, kita meneruskan variabel bernamasegmentationStream. Ini adalah aliran video yang diambil dari elemen kanvas, berisi gambar latar depan yang dilapisi pada gambar khusus yang mewakili latar belakang. Nantinya, aliran kustom ini akan digunakan untuk membuat instance aLocalStageStream, yang dapat dipublikasikan ke panggung.
const init = async () => { await initializeDeviceSelect(); cameraButton.addEventListener("click", () => { const isMuted = !cameraStageStream.isMuted; cameraStageStream.setMuted(isMuted); cameraButton.innerText = isMuted ? "Show Camera" : "Hide Camera"; }); micButton.addEventListener("click", () => { const isMuted = !micStageStream.isMuted; micStageStream.setMuted(isMuted); micButton.innerText = isMuted ? "Unmute Mic" : "Mute Mic"; }); localCamera = await getCamera(videoDevicesList.value); const segmentationStream = canvasElement.captureStream(); joinButton.addEventListener("click", () => { joinStage(segmentationStream); }); leaveButton.addEventListener("click", () => { leaveStage(); }); };
Muat Gambar Latar Belakang Kustom
Di bagian bawah init fungsi, tambahkan kode untuk memanggil fungsi bernamainitBackgroundCanvas, yang memuat gambar khusus dari file lokal dan merendernya ke kanvas. Kami akan mendefinisikan fungsi ini pada langkah berikutnya. Tetapkan yang MediaStream diambil dari kamera pengguna ke objek video. Nantinya, objek video ini akan diteruskan ke Segmenter Gambar. Juga, atur fungsi bernama fungsi renderVideoToCanvas panggilan balik untuk memanggil setiap kali bingkai video selesai dimuat. Kami akan mendefinisikan fungsi ini di langkah selanjutnya.
initBackgroundCanvas(); video.srcObject = localCamera; video.addEventListener("loadeddata", renderVideoToCanvas);
Mari kita implementasikan initBackgroundCanvas fungsi, yang memuat gambar dari file lokal. Dalam contoh ini, kita menggunakan gambar pantai sebagai latar belakang khusus. Kanvas yang berisi gambar khusus akan disembunyikan dari tampilan, karena Anda akan menggabungkannya dengan piksel latar depan dari elemen kanvas yang berisi umpan kamera.
const initBackgroundCanvas = () => { let img = new Image(); img.src = "beach.jpg"; img.onload = () => { backgroundCtx.clearRect(0, 0, canvas.width, canvas.height); backgroundCtx.drawImage(img, 0, 0); }; };
Buat Instance dari ImageSegmenter
Selanjutnya, buat contohImageSegmenter, yang akan mengelompokkan gambar dan mengembalikan hasilnya sebagai topeng. Saat membuat contohImageSegmenter, Anda akan menggunakan model segmentasi
const createImageSegmenter = async () => { const audio = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2/wasm"); imageSegmenter = await ImageSegmenter.createFromOptions(audio, { baseOptions: { modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite", delegate: "GPU", }, runningMode: "VIDEO", outputCategoryMask: true, }); };
Render Umpan Video ke Kanvas
Selanjutnya, buat fungsi yang merender umpan video ke elemen kanvas lainnya. Kita perlu merender umpan video ke kanvas sehingga kita dapat mengekstrak piksel latar depan darinya menggunakan Canvas 2D API. Saat melakukan ini, kami juga akan meneruskan bingkai video ke instance kamiImageSegmenter, menggunakan metode replaceBackground, untuk melakukan penggantian latar belakang.
const renderVideoToCanvas = async () => { if (video.currentTime === lastWebcamTime) { window.requestAnimationFrame(renderVideoToCanvas); return; } lastWebcamTime = video.currentTime; canvasCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight); if (imageSegmenter === undefined) { return; } let startTimeMs = performance.now(); imageSegmenter.segmentForVideo(video, startTimeMs, replaceBackground); };
Buat Logika Penggantian Latar Belakang
Buat replaceBackground fungsi, yang menggabungkan gambar latar belakang khusus dengan latar depan dari umpan kamera untuk menggantikan latar belakang. Fungsi ini pertama-tama mengambil data piksel yang mendasari gambar latar belakang kustom dan umpan video dari dua elemen kanvas yang dibuat sebelumnya. Kemudian mengulangi melalui topeng yang disediakan olehImageSegmenter, yang menunjukkan piksel mana yang ada di latar depan. Saat melakukan iterasi melalui topeng, ia secara selektif menyalin piksel yang berisi umpan kamera pengguna ke data piksel latar belakang yang sesuai. Setelah itu selesai, itu mengubah data piksel akhir dengan latar depan yang disalin ke latar belakang dan menggambarnya ke Kanvas.
function replaceBackground(result) { let imageData = canvasCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data; let backgroundData = backgroundCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data; const mask = result.categoryMask.getAsFloat32Array(); let j = 0; for (let i = 0; i < mask.length; ++i) { const maskVal = Math.round(mask[i] * 255.0); j += 4; // Only copy pixels on to the background image if the mask indicates they are in the foreground if (maskVal < 255) { backgroundData[j] = imageData[j]; backgroundData[j + 1] = imageData[j + 1]; backgroundData[j + 2] = imageData[j + 2]; backgroundData[j + 3] = imageData[j + 3]; } } // Convert the pixel data to a format suitable to be drawn to a canvas const uint8Array = new Uint8ClampedArray(backgroundData.buffer); const dataNew = new ImageData(uint8Array, video.videoWidth, video.videoHeight); canvasCtx.putImageData(dataNew, 0, 0); window.requestAnimationFrame(renderVideoToCanvas); }
Untuk referensi, berikut adalah app.js file lengkap yang berisi semua logika di atas:
/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ // All helpers are expose on 'media-devices.js' and 'dom.js' const { setupParticipant } = window; const { Stage, LocalStageStream, SubscribeType, StageEvents, ConnectionState, StreamType } = IVSBroadcastClient; const canvasElement = document.getElementById("canvas"); const background = document.getElementById("background"); const canvasCtx = canvasElement.getContext("2d"); const backgroundCtx = background.getContext("2d"); const video = document.getElementById("webcam"); import { ImageSegmenter, FilesetResolver } from "@mediapipe/tasks-vision"; let cameraButton = document.getElementById("camera-control"); let micButton = document.getElementById("mic-control"); let joinButton = document.getElementById("join-button"); let leaveButton = document.getElementById("leave-button"); let controls = document.getElementById("local-controls"); let audioDevicesList = document.getElementById("audio-devices"); let videoDevicesList = document.getElementById("video-devices"); // Stage management let stage; let joining = false; let connected = false; let localCamera; let localMic; let cameraStageStream; let micStageStream; let imageSegmenter; let lastWebcamTime = -1; const init = async () => { await initializeDeviceSelect(); cameraButton.addEventListener("click", () => { const isMuted = !cameraStageStream.isMuted; cameraStageStream.setMuted(isMuted); cameraButton.innerText = isMuted ? "Show Camera" : "Hide Camera"; }); micButton.addEventListener("click", () => { const isMuted = !micStageStream.isMuted; micStageStream.setMuted(isMuted); micButton.innerText = isMuted ? "Unmute Mic" : "Mute Mic"; }); localCamera = await getCamera(videoDevicesList.value); const segmentationStream = canvasElement.captureStream(); joinButton.addEventListener("click", () => { joinStage(segmentationStream); }); leaveButton.addEventListener("click", () => { leaveStage(); }); initBackgroundCanvas(); video.srcObject = localCamera; video.addEventListener("loadeddata", renderVideoToCanvas); }; const joinStage = async (segmentationStream) => { if (connected || joining) { return; } joining = true; const token = document.getElementById("token").value; if (!token) { window.alert("Please enter a participant token"); joining = false; return; } // Retrieve the User Media currently set on the page localMic = await getMic(audioDevicesList.value); cameraStageStream = new LocalStageStream(segmentationStream.getVideoTracks()[0]); micStageStream = new LocalStageStream(localMic.getAudioTracks()[0]); const strategy = { stageStreamsToPublish() { return [cameraStageStream, micStageStream]; }, shouldPublishParticipant() { return true; }, shouldSubscribeToParticipant() { return SubscribeType.AUDIO_VIDEO; }, }; stage = new Stage(token, strategy); // Other available events: // https://aws.github.io/amazon-ivs-web-broadcast/docs/sdk-guides/stages#events stage.on(StageEvents.STAGE_CONNECTION_STATE_CHANGED, (state) => { connected = state === ConnectionState.CONNECTED; if (connected) { joining = false; controls.classList.remove("hidden"); } else { controls.classList.add("hidden"); } }); stage.on(StageEvents.STAGE_PARTICIPANT_JOINED, (participant) => { console.log("Participant Joined:", participant); }); stage.on(StageEvents.STAGE_PARTICIPANT_STREAMS_ADDED, (participant, streams) => { console.log("Participant Media Added: ", participant, streams); let streamsToDisplay = streams; if (participant.isLocal) { // Ensure to exclude local audio streams, otherwise echo will occur streamsToDisplay = streams.filter((stream) => stream.streamType === StreamType.VIDEO); } const videoEl = setupParticipant(participant); streamsToDisplay.forEach((stream) => videoEl.srcObject.addTrack(stream.mediaStreamTrack)); }); stage.on(StageEvents.STAGE_PARTICIPANT_LEFT, (participant) => { console.log("Participant Left: ", participant); teardownParticipant(participant); }); try { await stage.join(); } catch (err) { joining = false; connected = false; console.error(err.message); } }; const leaveStage = async () => { stage.leave(); joining = false; connected = false; cameraButton.innerText = "Hide Camera"; micButton.innerText = "Mute Mic"; controls.classList.add("hidden"); }; function replaceBackground(result) { let imageData = canvasCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data; let backgroundData = backgroundCtx.getImageData(0, 0, video.videoWidth, video.videoHeight).data; const mask = result.categoryMask.getAsFloat32Array(); let j = 0; for (let i = 0; i < mask.length; ++i) { const maskVal = Math.round(mask[i] * 255.0); j += 4; if (maskVal < 255) { backgroundData[j] = imageData[j]; backgroundData[j + 1] = imageData[j + 1]; backgroundData[j + 2] = imageData[j + 2]; backgroundData[j + 3] = imageData[j + 3]; } } const uint8Array = new Uint8ClampedArray(backgroundData.buffer); const dataNew = new ImageData(uint8Array, video.videoWidth, video.videoHeight); canvasCtx.putImageData(dataNew, 0, 0); window.requestAnimationFrame(renderVideoToCanvas); } const createImageSegmenter = async () => { const audio = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.2/wasm"); imageSegmenter = await ImageSegmenter.createFromOptions(audio, { baseOptions: { modelAssetPath: "https://storage.googleapis.com/mediapipe-models/image_segmenter/selfie_segmenter/float16/latest/selfie_segmenter.tflite", delegate: "GPU", }, runningMode: "VIDEO", outputCategoryMask: true, }); }; const renderVideoToCanvas = async () => { if (video.currentTime === lastWebcamTime) { window.requestAnimationFrame(renderVideoToCanvas); return; } lastWebcamTime = video.currentTime; canvasCtx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight); if (imageSegmenter === undefined) { return; } let startTimeMs = performance.now(); imageSegmenter.segmentForVideo(video, startTimeMs, replaceBackground); }; const initBackgroundCanvas = () => { let img = new Image(); img.src = "beach.jpg"; img.onload = () => { backgroundCtx.clearRect(0, 0, canvas.width, canvas.height); backgroundCtx.drawImage(img, 0, 0); }; }; createImageSegmenter(); init();
Buat File Konfigurasi Webpack
Tambahkan konfigurasi ini ke file konfigurasi Webpack Anda ke bundelapp.js, sehingga panggilan impor akan berfungsi:
const path = require("path"); module.exports = { entry: ["./app.js"], output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), }, };
Bundel JavaScript file Anda
npm run build
Mulai server HTTP sederhana dari direktori yang berisi index.html dan buka localhost:8000 untuk melihat hasilnya:
python3 -m http.server -d ./
Android
Untuk mengganti latar belakang di streaming langsung Anda, Anda dapat menggunakan API segmentasi selfie dari Google ML Kit
Untuk mengintegrasikan penggantian latar belakang dengan IVS real-time streaming Android Broadcast SDK, Anda perlu:
-
Instal pustaka CameraX dan kit Google ML.
-
Inisialisasi variabel boilerplate.
-
Buat sumber gambar kustom.
-
Kelola bingkai kamera.
-
Berikan bingkai kamera ke Google ML Kit.
-
Hamparkan latar depan bingkai kamera ke latar belakang kustom Anda.
-
Umpan gambar baru ke sumber gambar khusus.
Instal Perpustakaan CameraX dan Google ML Kit
Untuk mengekstrak gambar dari umpan kamera langsung, gunakan pustaka CameraX Android. Untuk menginstal library CameraX dan Google ML Kit, tambahkan yang berikut ini ke build.gradle file modul Anda. Ganti ${camerax_version} dan ${google_ml_kit_version} dengan versi terbaru perpustakaan CameraX
implementation "com.google.mlkit:segmentation-selfie:${google_ml_kit_version}" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}"
Impor pustaka berikut:
import androidx.camera.core.CameraSelector import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageProxy import androidx.camera.lifecycle.ProcessCameraProvider import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
Inisialisasi Variabel Boilerplate
Menginisialisasi instance ImageAnalysis dan instance dari: ExecutorService
private lateinit var binding: ActivityMainBinding private lateinit var cameraExecutor: ExecutorService private var analysisUseCase: ImageAnalysis? = null
Inisialisasi instance Segmenter di STREAM_MODE:
private val options = SelfieSegmenterOptions.Builder() .setDetectorMode(SelfieSegmenterOptions.STREAM_MODE) .build() private val segmenter = Segmentation.getClient(options)
Buat Sumber Gambar Kustom
Dalam onCreate metode aktivitas Anda, buat instance DeviceDiscovery objek dan buat sumber gambar khusus. Yang Surface disediakan oleh Sumber Gambar Kustom akan menerima gambar akhir, dengan latar depan dilapisi pada gambar latar belakang khusus. Anda kemudian akan membuat instance ImageLocalStageStream menggunakan Sumber Gambar Kustom. Contoh a ImageLocalStageStream (dinamai filterStream dalam contoh ini) kemudian dapat dipublikasikan ke tahap. Lihat Panduan IVS Android Broadcast SDK untuk petunjuk tentang menyiapkan panggung. Terakhir, buat juga utas yang akan digunakan untuk mengelola kamera.
var deviceDiscovery = DeviceDiscovery(applicationContext) var customSource = deviceDiscovery.createImageInputSource( BroadcastConfiguration.Vec2( 720F, 1280F )) var surface: Surface = customSource.inputSurface var filterStream = ImageLocalStageStream(customSource) cameraExecutor = Executors.newSingleThreadExecutor()
Kelola Bingkai Kamera
Selanjutnya, buat fungsi untuk menginisialisasi kamera. Fungsi ini menggunakan pustaka CameraX untuk mengekstrak gambar dari umpan kamera langsung. Pertama, Anda membuat instance dari yang dipang ProcessCameraProvider gilcameraProviderFuture. Objek ini mewakili hasil masa depan untuk mendapatkan penyedia kamera. Kemudian Anda memuat gambar dari proyek Anda sebagai bitmap. Contoh ini menggunakan gambar pantai sebagai latar belakang, tetapi bisa berupa gambar apa pun yang Anda inginkan.
Anda kemudian menambahkan pendengar kecameraProviderFuture. Pendengar ini diberitahu ketika kamera tersedia atau jika terjadi kesalahan selama proses mendapatkan penyedia kamera.
private fun startCamera(surface: Surface) { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) val imageResource = R.drawable.beach val bgBitmap: Bitmap = BitmapFactory.decodeResource(resources, imageResource) var resultBitmap: Bitmap; cameraProviderFuture.addListener({ val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() if (mediaImage != null) { val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels) canvas = surface.lockCanvas(null); canvas.drawBitmap(resultBitmap, 0f, 0f, null) surface.unlockCanvasAndPost(canvas); } .addOnFailureListener { exception -> Log.d("App", exception.message!!) } .addOnCompleteListener { imageProxy.close() } } }; val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA try { // Unbind use cases before rebinding cameraProvider.unbindAll() // Bind use cases to camera cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase) } catch(exc: Exception) { Log.e(TAG, "Use case binding failed", exc) } }, ContextCompat.getMainExecutor(this)) }
Di dalam pendengar, buat ImageAnalysis.Builder untuk mengakses setiap frame individu dari umpan kamera langsung. Atur strategi tekanan balik keSTRATEGY_KEEP_ONLY_LATEST. Ini menjamin bahwa hanya satu bingkai kamera pada satu waktu yang dikirim untuk diproses. Konversikan setiap bingkai kamera individu ke bitmap, sehingga Anda dapat mengekstrak pikselnya untuk kemudian menggabungkannya dengan gambar latar belakang khusus.
val imageAnalyzer = ImageAnalysis.Builder() analysisUseCase = imageAnalyzer .setTargetResolution(Size(360, 640)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() analysisUseCase?.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy -> val mediaImage = imageProxy.image val tempBitmap = imageProxy.toBitmap(); val inputBitmap = tempBitmap.rotate(imageProxy.imageInfo.rotationDegrees.toFloat())
Berikan Bingkai Kamera ke Google ML Kit
Selanjutnya, buat InputImage dan berikan ke instance Segmenter untuk diproses. An InputImage dapat dibuat dari yang ImageProxy disediakan oleh instance ofImageAnalysis. Setelah InputImage diberikan ke Segmenter, ia mengembalikan topeng dengan skor kepercayaan yang menunjukkan kemungkinan piksel berada di latar depan atau latar belakang. Topeng ini juga menyediakan properti lebar dan tinggi, yang akan Anda gunakan untuk membuat array baru yang berisi piksel latar belakang dari gambar latar belakang kustom yang dimuat sebelumnya.
if (mediaImage != null) { val inputImage = InputImage.fromMediaImag segmenter.process(inputImage) .addOnSuccessListener { segmentationMask -> val mask = segmentationMask.buffer val maskWidth = segmentationMask.width val maskHeight = segmentationMask.height val backgroundPixels = IntArray(maskWidth * maskHeight) bgBitmap.getPixels(backgroundPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight)
Hamparkan Latar Depan Bingkai Kamera ke Latar Belakang Kustom Anda
Dengan topeng yang berisi skor kepercayaan, bingkai kamera sebagai bitmap, dan piksel warna dari gambar latar belakang khusus, Anda memiliki semua yang Anda butuhkan untuk melapisi latar depan ke latar belakang khusus Anda. overlayForegroundFungsi ini kemudian dipanggil dengan parameter berikut:
resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels)
Fungsi ini mengulangi melalui topeng dan memeriksa nilai kepercayaan untuk menentukan apakah akan mendapatkan warna piksel yang sesuai dari gambar latar belakang atau bingkai kamera. Jika nilai kepercayaan menunjukkan bahwa piksel dalam topeng kemungkinan besar berada di latar belakang, itu akan mendapatkan warna piksel yang sesuai dari gambar latar belakang; jika tidak, itu akan mendapatkan warna piksel yang sesuai dari bingkai kamera untuk membangun latar depan. Setelah fungsi selesai melakukan iterasi melalui mask, bitmap baru dibuat menggunakan array piksel warna baru dan dikembalikan. Bitmap baru ini berisi latar depan yang dilapisi pada latar belakang kustom.
private fun overlayForeground( byteBuffer: ByteBuffer, maskWidth: Int, maskHeight: Int, cameraBitmap: Bitmap, backgroundPixels: IntArray ): Bitmap { @ColorInt val colors = IntArray(maskWidth * maskHeight) val cameraPixels = IntArray(maskWidth * maskHeight) cameraBitmap.getPixels(cameraPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight) for (i in 0 until maskWidth * maskHeight) { val backgroundLikelihood: Float = 1 - byteBuffer.getFloat() // Apply the virtual background to the color if it's not part of the foreground if (backgroundLikelihood > 0.9) { // Get the corresponding pixel color from the background image // Set the color in the mask based on the background image pixel color colors[i] = backgroundPixels.get(i) } else { // Get the corresponding pixel color from the camera frame // Set the color in the mask based on the camera image pixel color colors[i] = cameraPixels.get(i) } } return Bitmap.createBitmap( colors, maskWidth, maskHeight, Bitmap.Config.ARGB_8888 ) }
Umpan Gambar Baru ke Sumber Gambar Kustom
Anda kemudian dapat menulis bitmap baru ke yang Surface disediakan oleh sumber gambar kustom. Ini akan menyiarkannya ke panggung Anda.
resultBitmap = overlayForeground(mask, inputBitmap, mutableBitmap, bgBitmap) canvas = surface.lockCanvas(null); canvas.drawBitmap(resultBitmap, 0f, 0f, null)
Berikut adalah fungsi lengkap untuk mendapatkan bingkai kamera, meneruskannya ke Segmenter, dan melapiskannya di latar belakang:
@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class) private fun startCamera(surface: Surface) { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) val imageResource = R.drawable.clouds val bgBitmap: Bitmap = BitmapFactory.decodeResource(resources, imageResource) var resultBitmap: Bitmap; cameraProviderFuture.addListener({ // Used to bind the lifecycle of cameras to the lifecycle owner val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() val imageAnalyzer = ImageAnalysis.Builder() analysisUseCase = imageAnalyzer .setTargetResolution(Size(720, 1280)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() analysisUseCase!!.setAnalyzer(cameraExecutor) { imageProxy: ImageProxy -> val mediaImage = imageProxy.image val tempBitmap = imageProxy.toBitmap(); val inputBitmap = tempBitmap.rotate(imageProxy.imageInfo.rotationDegrees.toFloat()) if (mediaImage != null) { val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) segmenter.process(inputImage) .addOnSuccessListener { segmentationMask -> val mask = segmentationMask.buffer val maskWidth = segmentationMask.width val maskHeight = segmentationMask.height val backgroundPixels = IntArray(maskWidth * maskHeight) bgBitmap.getPixels(backgroundPixels, 0, maskWidth, 0, 0, maskWidth, maskHeight) resultBitmap = overlayForeground(mask, maskWidth, maskHeight, inputBitmap, backgroundPixels) canvas = surface.lockCanvas(null); canvas.drawBitmap(resultBitmap, 0f, 0f, null) surface.unlockCanvasAndPost(canvas); } .addOnFailureListener { exception -> Log.d("App", exception.message!!) } .addOnCompleteListener { imageProxy.close() } } }; val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA try { // Unbind use cases before rebinding cameraProvider.unbindAll() // Bind use cases to camera cameraProvider.bindToLifecycle(this, cameraSelector, analysisUseCase) } catch(exc: Exception) { Log.e(TAG, "Use case binding failed", exc) } }, ContextCompat.getMainExecutor(this)) }