react-native-webrtc-kaleidoscope 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE.md +17 -6
- package/README.md +67 -19
- package/android/build.gradle +6 -5
- package/android/src/main/assets/backgrounds/dark-office.webp +0 -0
- package/android/src/main/assets/backgrounds/debug-resolutions.webp +0 -0
- package/android/src/main/assets/backgrounds/home-dark.webp +0 -0
- package/android/src/main/assets/backgrounds/home-light.webp +0 -0
- package/android/src/main/assets/backgrounds/light-office.webp +0 -0
- package/android/src/main/assets/backgrounds/nature-dark.webp +0 -0
- package/android/src/main/assets/backgrounds/nature-light.webp +0 -0
- package/android/src/main/assets/backgrounds/simiancraft-dark.webp +0 -0
- package/android/src/main/assets/backgrounds/simiancraft-light.webp +0 -0
- package/android/src/main/assets/backgrounds/stylized-dark.webp +0 -0
- package/android/src/main/assets/backgrounds/stylized-light.webp +0 -0
- package/android/src/main/assets/selfie_segmenter.tflite +0 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/EffectTuning.kt +41 -10
- package/android/src/main/java/com/simiancraft/kaleidoscope/KaleidoscopeModule.kt +8 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/Registration.kt +52 -13
- package/android/src/main/java/com/simiancraft/kaleidoscope/effects/BackgroundImageFactory.kt +53 -23
- package/android/src/main/java/com/simiancraft/kaleidoscope/effects/BlurFactory.kt +114 -50
- package/android/src/main/java/com/simiancraft/kaleidoscope/effects/TransformFactory.kt +277 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/FramePipeline.kt +158 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GlProgram.kt +5 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Ingest.kt +123 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Orientation.kt +58 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/Shaders.kt +23 -51
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/ShadersGenerated.kt +76 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/segmentation/Mask.kt +100 -89
- package/android/src/main/java/com/simiancraft/kaleidoscope/segmentation/SegmentationEngine.kt +148 -0
- package/app.plugin.js +254 -4
- package/dist/backgrounds/dark-office.d.ts +3 -0
- package/dist/backgrounds/dark-office.d.ts.map +1 -0
- package/dist/backgrounds/dark-office.js +5 -0
- package/dist/backgrounds/dark-office.js.map +1 -0
- package/dist/backgrounds/dark-office.web.d.ts +3 -0
- package/dist/backgrounds/dark-office.web.d.ts.map +1 -0
- package/dist/backgrounds/dark-office.web.js +8 -0
- package/dist/backgrounds/dark-office.web.js.map +1 -0
- package/dist/backgrounds/dark-office.webp +0 -0
- package/dist/backgrounds/debug-resolutions.d.ts +3 -0
- package/dist/backgrounds/debug-resolutions.d.ts.map +1 -0
- package/dist/backgrounds/debug-resolutions.js +6 -0
- package/dist/backgrounds/debug-resolutions.js.map +1 -0
- package/dist/backgrounds/debug-resolutions.web.d.ts +3 -0
- package/dist/backgrounds/debug-resolutions.web.d.ts.map +1 -0
- package/dist/backgrounds/debug-resolutions.web.js +8 -0
- package/dist/backgrounds/debug-resolutions.web.js.map +1 -0
- package/dist/backgrounds/debug-resolutions.webp +0 -0
- package/dist/backgrounds/home-dark.d.ts +3 -0
- package/dist/backgrounds/home-dark.d.ts.map +1 -0
- package/dist/backgrounds/home-dark.js +5 -0
- package/dist/backgrounds/home-dark.js.map +1 -0
- package/dist/backgrounds/home-dark.web.d.ts +3 -0
- package/dist/backgrounds/home-dark.web.d.ts.map +1 -0
- package/dist/backgrounds/home-dark.web.js +6 -0
- package/dist/backgrounds/home-dark.web.js.map +1 -0
- package/dist/backgrounds/home-dark.webp +0 -0
- package/dist/backgrounds/home-light.d.ts +3 -0
- package/dist/backgrounds/home-light.d.ts.map +1 -0
- package/dist/backgrounds/home-light.js +5 -0
- package/dist/backgrounds/home-light.js.map +1 -0
- package/dist/backgrounds/home-light.web.d.ts +3 -0
- package/dist/backgrounds/home-light.web.d.ts.map +1 -0
- package/dist/backgrounds/home-light.web.js +6 -0
- package/dist/backgrounds/home-light.web.js.map +1 -0
- package/dist/backgrounds/home-light.webp +0 -0
- package/dist/backgrounds/index.d.ts +3 -0
- package/dist/backgrounds/index.d.ts.map +1 -0
- package/dist/backgrounds/index.js +6 -0
- package/dist/backgrounds/index.js.map +1 -0
- package/dist/backgrounds/light-office.d.ts +3 -0
- package/dist/backgrounds/light-office.d.ts.map +1 -0
- package/dist/backgrounds/light-office.js +5 -0
- package/dist/backgrounds/light-office.js.map +1 -0
- package/dist/backgrounds/light-office.web.d.ts +3 -0
- package/dist/backgrounds/light-office.web.d.ts.map +1 -0
- package/dist/backgrounds/light-office.web.js +8 -0
- package/dist/backgrounds/light-office.web.js.map +1 -0
- package/dist/backgrounds/light-office.webp +0 -0
- package/dist/backgrounds/nature-dark.d.ts +3 -0
- package/dist/backgrounds/nature-dark.d.ts.map +1 -0
- package/dist/backgrounds/nature-dark.js +5 -0
- package/dist/backgrounds/nature-dark.js.map +1 -0
- package/dist/backgrounds/nature-dark.web.d.ts +3 -0
- package/dist/backgrounds/nature-dark.web.d.ts.map +1 -0
- package/dist/backgrounds/nature-dark.web.js +6 -0
- package/dist/backgrounds/nature-dark.web.js.map +1 -0
- package/dist/backgrounds/nature-dark.webp +0 -0
- package/dist/backgrounds/nature-light.d.ts +3 -0
- package/dist/backgrounds/nature-light.d.ts.map +1 -0
- package/dist/backgrounds/nature-light.js +5 -0
- package/dist/backgrounds/nature-light.js.map +1 -0
- package/dist/backgrounds/nature-light.web.d.ts +3 -0
- package/dist/backgrounds/nature-light.web.d.ts.map +1 -0
- package/dist/backgrounds/nature-light.web.js +6 -0
- package/dist/backgrounds/nature-light.web.js.map +1 -0
- package/dist/backgrounds/nature-light.webp +0 -0
- package/dist/backgrounds/preset-source.types.d.ts +2 -0
- package/dist/backgrounds/preset-source.types.d.ts.map +1 -0
- package/dist/backgrounds/preset-source.types.js +2 -0
- package/dist/backgrounds/preset-source.types.js.map +1 -0
- package/dist/backgrounds/presets.d.ts +3 -0
- package/dist/backgrounds/presets.d.ts.map +1 -0
- package/dist/backgrounds/presets.js +34 -0
- package/dist/backgrounds/presets.js.map +1 -0
- package/dist/backgrounds/simiancraft-dark.d.ts +3 -0
- package/dist/backgrounds/simiancraft-dark.d.ts.map +1 -0
- package/dist/backgrounds/simiancraft-dark.js +5 -0
- package/dist/backgrounds/simiancraft-dark.js.map +1 -0
- package/dist/backgrounds/simiancraft-dark.web.d.ts +3 -0
- package/dist/backgrounds/simiancraft-dark.web.d.ts.map +1 -0
- package/dist/backgrounds/simiancraft-dark.web.js +6 -0
- package/dist/backgrounds/simiancraft-dark.web.js.map +1 -0
- package/dist/backgrounds/simiancraft-dark.webp +0 -0
- package/dist/backgrounds/simiancraft-light.d.ts +3 -0
- package/dist/backgrounds/simiancraft-light.d.ts.map +1 -0
- package/dist/backgrounds/simiancraft-light.js +5 -0
- package/dist/backgrounds/simiancraft-light.js.map +1 -0
- package/dist/backgrounds/simiancraft-light.web.d.ts +3 -0
- package/dist/backgrounds/simiancraft-light.web.d.ts.map +1 -0
- package/dist/backgrounds/simiancraft-light.web.js +6 -0
- package/dist/backgrounds/simiancraft-light.web.js.map +1 -0
- package/dist/backgrounds/simiancraft-light.webp +0 -0
- package/dist/backgrounds/stylized-dark.d.ts +3 -0
- package/dist/backgrounds/stylized-dark.d.ts.map +1 -0
- package/dist/backgrounds/stylized-dark.js +5 -0
- package/dist/backgrounds/stylized-dark.js.map +1 -0
- package/dist/backgrounds/stylized-dark.web.d.ts +3 -0
- package/dist/backgrounds/stylized-dark.web.d.ts.map +1 -0
- package/dist/backgrounds/stylized-dark.web.js +6 -0
- package/dist/backgrounds/stylized-dark.web.js.map +1 -0
- package/dist/backgrounds/stylized-dark.webp +0 -0
- package/dist/backgrounds/stylized-light.d.ts +3 -0
- package/dist/backgrounds/stylized-light.d.ts.map +1 -0
- package/dist/backgrounds/stylized-light.js +5 -0
- package/dist/backgrounds/stylized-light.js.map +1 -0
- package/dist/backgrounds/stylized-light.web.d.ts +3 -0
- package/dist/backgrounds/stylized-light.web.d.ts.map +1 -0
- package/dist/backgrounds/stylized-light.web.js +6 -0
- package/dist/backgrounds/stylized-light.web.js.map +1 -0
- package/dist/backgrounds/stylized-light.webp +0 -0
- package/dist/index.d.ts +19 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +71 -22
- package/dist/index.js.map +1 -1
- package/dist/index.web.d.ts +25 -3
- package/dist/index.web.d.ts.map +1 -1
- package/dist/index.web.js +55 -16
- package/dist/index.web.js.map +1 -1
- package/dist/livekit.d.ts +24 -0
- package/dist/livekit.d.ts.map +1 -0
- package/dist/livekit.js +57 -0
- package/dist/livekit.js.map +1 -0
- package/dist/types.d.ts +20 -13
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +1 -1
- package/dist/web/blur-kernel.d.ts +6 -0
- package/dist/web/blur-kernel.d.ts.map +1 -0
- package/dist/web/blur-kernel.js +41 -0
- package/dist/web/blur-kernel.js.map +1 -0
- package/dist/web/effects/background-image.d.ts.map +1 -1
- package/dist/web/effects/background-image.js +100 -33
- package/dist/web/effects/background-image.js.map +1 -1
- package/dist/web/effects/blur.d.ts.map +1 -1
- package/dist/web/effects/blur.js +169 -38
- package/dist/web/effects/blur.js.map +1 -1
- package/dist/web/effects/transform.d.ts +4 -0
- package/dist/web/effects/transform.d.ts.map +1 -0
- package/dist/web/effects/transform.js +62 -0
- package/dist/web/effects/transform.js.map +1 -0
- package/dist/web/insertable-streams.d.ts +11 -1
- package/dist/web/insertable-streams.d.ts.map +1 -1
- package/dist/web/insertable-streams.js +22 -4
- package/dist/web/insertable-streams.js.map +1 -1
- package/dist/web/segmenter.d.ts +9 -0
- package/dist/web/segmenter.d.ts.map +1 -1
- package/dist/web/segmenter.js +32 -0
- package/dist/web/segmenter.js.map +1 -1
- package/dist/web/shaders.d.ts +1 -3
- package/dist/web/shaders.d.ts.map +1 -1
- package/dist/web/shaders.generated.d.ts +4 -0
- package/dist/web/shaders.generated.d.ts.map +1 -0
- package/dist/web/shaders.generated.js +54 -0
- package/dist/web/shaders.generated.js.map +1 -0
- package/dist/web/shaders.js +29 -103
- package/dist/web/shaders.js.map +1 -1
- package/dist/web/tuning.d.ts +4 -0
- package/dist/web/tuning.d.ts.map +1 -1
- package/dist/web/tuning.js +17 -5
- package/dist/web/tuning.js.map +1 -1
- package/ios/Kaleidoscope.podspec +87 -14
- package/ios/KaleidoscopeModule/EffectTuning.swift +47 -7
- package/ios/KaleidoscopeModule/KaleidoscopeModule.swift +12 -0
- package/ios/KaleidoscopeModule/Registration.swift +78 -13
- package/ios/KaleidoscopeModule/effects/BackgroundImageProcessor.swift +293 -0
- package/ios/KaleidoscopeModule/effects/BlurProcessor.swift +232 -18
- package/ios/KaleidoscopeModule/effects/FrameBridge.swift +46 -0
- package/ios/KaleidoscopeModule/effects/TransformProcessor.swift +173 -0
- package/ios/KaleidoscopeModule/gpu/Ingest.swift +169 -0
- package/ios/KaleidoscopeModule/gpu/MetalRenderer.swift +601 -0
- package/ios/KaleidoscopeModule/gpu/Orientation.swift +83 -0
- package/ios/KaleidoscopeModule/gpu/ShaderLibrary.swift +100 -0
- package/ios/KaleidoscopeModule/gpu/TextureBridge.swift +291 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/dark-office.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/debug-resolutions.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/home-dark.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/home-light.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/light-office.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/nature-dark.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/nature-light.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/simiancraft-dark.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/simiancraft-light.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/stylized-dark.webp +0 -0
- package/ios/KaleidoscopeModule/resources/backgrounds/stylized-light.webp +0 -0
- package/ios/KaleidoscopeModule/resources/selfie_segmenter.tflite +0 -0
- package/ios/KaleidoscopeModule/segmentation/MaskTuning.swift +23 -0
- package/ios/KaleidoscopeModule/segmentation/Segmenter.swift +444 -25
- package/ios/KaleidoscopeModule/shaders/SHADERS.txt +6 -0
- package/ios/KaleidoscopeModule/shaders/blur.metalsrc +72 -0
- package/ios/KaleidoscopeModule/shaders/composite.metalsrc +22 -0
- package/ios/KaleidoscopeModule/shaders/nebula.metalsrc +72 -0
- package/ios/KaleidoscopeModule/shaders/passthrough.metalsrc +20 -0
- package/ios/KaleidoscopeModule/shaders/simianlights.metalsrc +72 -0
- package/ios/KaleidoscopeModule/shaders/transform.metalsrc +22 -0
- package/package.json +120 -11
- package/src/backgrounds/README.md +83 -0
- package/src/backgrounds/assets.d.ts +7 -0
- package/src/backgrounds/dark-office.ts +6 -0
- package/src/backgrounds/dark-office.web.ts +9 -0
- package/src/backgrounds/dark-office.webp +0 -0
- package/src/backgrounds/debug-resolutions.ts +7 -0
- package/src/backgrounds/debug-resolutions.web.ts +9 -0
- package/src/backgrounds/debug-resolutions.webp +0 -0
- package/src/backgrounds/home-dark.ts +6 -0
- package/src/backgrounds/home-dark.web.ts +7 -0
- package/src/backgrounds/home-dark.webp +0 -0
- package/src/backgrounds/home-light.ts +6 -0
- package/src/backgrounds/home-light.web.ts +7 -0
- package/src/backgrounds/home-light.webp +0 -0
- package/src/backgrounds/index.ts +7 -0
- package/src/backgrounds/light-office.ts +6 -0
- package/src/backgrounds/light-office.web.ts +9 -0
- package/src/backgrounds/light-office.webp +0 -0
- package/src/backgrounds/nature-dark.ts +6 -0
- package/src/backgrounds/nature-dark.web.ts +7 -0
- package/src/backgrounds/nature-dark.webp +0 -0
- package/src/backgrounds/nature-light.ts +6 -0
- package/src/backgrounds/nature-light.web.ts +7 -0
- package/src/backgrounds/nature-light.webp +0 -0
- package/src/backgrounds/preset-source.types.ts +5 -0
- package/src/backgrounds/presets.ts +36 -0
- package/src/backgrounds/simiancraft-dark.ts +6 -0
- package/src/backgrounds/simiancraft-dark.web.ts +7 -0
- package/src/backgrounds/simiancraft-dark.webp +0 -0
- package/src/backgrounds/simiancraft-light.ts +6 -0
- package/src/backgrounds/simiancraft-light.web.ts +7 -0
- package/src/backgrounds/simiancraft-light.webp +0 -0
- package/src/backgrounds/stylized-dark.ts +6 -0
- package/src/backgrounds/stylized-dark.web.ts +7 -0
- package/src/backgrounds/stylized-dark.webp +0 -0
- package/src/backgrounds/stylized-light.ts +6 -0
- package/src/backgrounds/stylized-light.web.ts +7 -0
- package/src/backgrounds/stylized-light.webp +0 -0
- package/src/index.ts +94 -29
- package/src/index.web.ts +69 -19
- package/src/livekit.ts +65 -0
- package/src/types.ts +23 -14
- package/src/web/blur-kernel.ts +44 -0
- package/src/web/effects/background-image.ts +121 -34
- package/src/web/effects/blur.ts +214 -40
- package/src/web/effects/transform.ts +69 -0
- package/src/web/insertable-streams.ts +33 -5
- package/src/web/segmenter.ts +34 -0
- package/src/web/shaders.generated.ts +56 -0
- package/src/web/shaders.ts +29 -106
- package/src/web/tuning.ts +19 -5
- package/android/src/main/assets/backgrounds/office-1.png +0 -0
- package/android/src/main/assets/backgrounds/office-2.png +0 -0
- package/android/src/main/java/com/simiancraft/kaleidoscope/effects/MirrorFactory.kt +0 -57
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GpuEffectFactory.kt +0 -14
- package/android/src/main/java/com/simiancraft/kaleidoscope/gpu/GpuEffectProcessor.kt +0 -198
- package/dist/backgrounds.d.ts +0 -3
- package/dist/backgrounds.d.ts.map +0 -1
- package/dist/backgrounds.js +0 -21
- package/dist/backgrounds.js.map +0 -1
- package/dist/web/effects/mirror.d.ts +0 -3
- package/dist/web/effects/mirror.d.ts.map +0 -1
- package/dist/web/effects/mirror.js +0 -31
- package/dist/web/effects/mirror.js.map +0 -1
- package/dist/web/effects/passthrough.d.ts +0 -3
- package/dist/web/effects/passthrough.d.ts.map +0 -1
- package/dist/web/effects/passthrough.js +0 -15
- package/dist/web/effects/passthrough.js.map +0 -1
- package/ios/KaleidoscopeModule/effects/MirrorProcessor.swift +0 -17
- package/plugin/build/withKaleidoscope.d.ts +0 -3
- package/plugin/build/withKaleidoscope.js +0 -14
- package/plugin/build/withKaleidoscope.js.map +0 -1
- package/src/backgrounds.ts +0 -23
- package/src/web/effects/mirror.ts +0 -37
- package/src/web/effects/passthrough.ts +0 -17
|
@@ -1,35 +1,43 @@
|
|
|
1
|
-
// Mask production:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Mask production: GL-side adapter around the process-wide SegmentationEngine.
|
|
2
|
+
//
|
|
3
|
+
// This class owns the per-processor GL state (downsample FBO/program, mask
|
|
4
|
+
// texture) and the per-stream temporal-smoothing (EMA) state. The actual
|
|
5
|
+
// segmentation (the worker thread + the MediaPipe ImageSegmenter) lives in the
|
|
6
|
+
// shared SegmentationEngine, so constructing a new Mask per effect switch does
|
|
7
|
+
// NOT spin up a new thread or segmenter; see SegmentationEngine for why.
|
|
4
8
|
//
|
|
5
9
|
// Per-frame flow on the GL thread:
|
|
6
10
|
// 1. If the worker produced a new mask bitmap since the last frame,
|
|
7
11
|
// upload it to the cached mask GL texture.
|
|
8
12
|
// 2. If no segmentation is currently in flight, render a small downsample
|
|
9
|
-
// snapshot of the input
|
|
13
|
+
// snapshot of the input and submit it to the SegmentationEngine.
|
|
10
14
|
// 3. Return the current mask GL texture handle (or -1 if no mask has
|
|
11
15
|
// completed yet — caller falls through to the original frame).
|
|
12
16
|
//
|
|
13
|
-
// The
|
|
14
|
-
//
|
|
15
|
-
// the
|
|
16
|
-
//
|
|
17
|
+
// The engine hands the raw foreground-confidence mask back (on its worker
|
|
18
|
+
// thread) via packMask(), which applies EMA smoothing, flips the mask back into
|
|
19
|
+
// the bottom-up orientation the upload/composite expects (the engine segmented
|
|
20
|
+
// an upright copy), quantizes to 8-bit, and stages the result for upload.
|
|
21
|
+
//
|
|
22
|
+
// RESIDUAL LEAK (bounded, documented): because upstream rebuilds the processor
|
|
23
|
+
// per effect switch with no teardown hook, the dropped processor's GL resources
|
|
24
|
+
// are not freed until the EGL context is destroyed (camera stop). That includes
|
|
25
|
+
// this Mask's texture/FBO/program AND the dropped processor's own state (the
|
|
26
|
+
// blur ping-pong FBOs/programs, the YuvConverter). All of it is bounded by the
|
|
27
|
+
// number of switches in a session and small per item; the unbounded thread/
|
|
28
|
+
// segmenter accumulation that would actually OOM is gone (it moved to the
|
|
29
|
+
// process-lived SegmentationEngine).
|
|
17
30
|
//
|
|
18
31
|
// All failure paths log under Kaleidoscope.Mask and return -1 (or a stale
|
|
19
32
|
// mask if one was previously computed).
|
|
20
33
|
|
|
21
34
|
package com.simiancraft.kaleidoscope.segmentation
|
|
22
35
|
|
|
36
|
+
import android.content.Context
|
|
23
37
|
import android.graphics.Bitmap
|
|
24
38
|
import android.opengl.GLES30
|
|
25
|
-
import android.os.Handler
|
|
26
|
-
import android.os.HandlerThread
|
|
27
39
|
import android.util.Log
|
|
28
|
-
import com.
|
|
29
|
-
import com.google.mlkit.vision.common.InputImage
|
|
30
|
-
import com.google.mlkit.vision.segmentation.Segmentation
|
|
31
|
-
import com.google.mlkit.vision.segmentation.Segmenter
|
|
32
|
-
import com.google.mlkit.vision.segmentation.selfie.SelfieSegmenterOptions
|
|
40
|
+
import com.simiancraft.kaleidoscope.EffectTuning
|
|
33
41
|
import com.simiancraft.kaleidoscope.gpu.Fbo
|
|
34
42
|
import com.simiancraft.kaleidoscope.gpu.GlDebug
|
|
35
43
|
import com.simiancraft.kaleidoscope.gpu.GlProgram
|
|
@@ -39,9 +47,7 @@ import java.nio.ByteOrder
|
|
|
39
47
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
40
48
|
import java.util.concurrent.atomic.AtomicReference
|
|
41
49
|
|
|
42
|
-
internal class Mask {
|
|
43
|
-
private var segmenter: Segmenter? = null
|
|
44
|
-
|
|
50
|
+
internal class Mask(private val context: Context) {
|
|
45
51
|
// Cached small-FBO state for the downsample pass.
|
|
46
52
|
private var downsampleFbo: Fbo? = null
|
|
47
53
|
private var downsampleProgram: GlProgram? = null
|
|
@@ -51,14 +57,8 @@ internal class Mask {
|
|
|
51
57
|
private var maskTexWidth: Int = 0
|
|
52
58
|
private var maskTexHeight: Int = 0
|
|
53
59
|
|
|
54
|
-
// Worker thread for blocking MLKit calls. Started lazily on first frame.
|
|
55
|
-
private val workerThread: HandlerThread = HandlerThread("Kaleidoscope.MaskWorker").apply {
|
|
56
|
-
start()
|
|
57
|
-
}
|
|
58
|
-
private val workerHandler: Handler = Handler(workerThread.looper)
|
|
59
|
-
|
|
60
60
|
// Throttle to a single in-flight segmentation at a time. Set true on
|
|
61
|
-
// kickoff (GL thread), reset false when the
|
|
61
|
+
// kickoff (GL thread), reset false when the engine reports done.
|
|
62
62
|
private val isProcessing = AtomicBoolean(false)
|
|
63
63
|
|
|
64
64
|
// Worker -> GL thread handoff: a Bitmap ready to upload as the mask
|
|
@@ -70,17 +70,24 @@ internal class Mask {
|
|
|
70
70
|
// Pre-allocated readback buffer (resized only on input-dim change).
|
|
71
71
|
private var pixelByteBuffer: ByteBuffer? = null
|
|
72
72
|
|
|
73
|
+
// Temporal-smoothing (EMA) state: the previous smoothed confidence buffer and
|
|
74
|
+
// its dims. Touched only on the SegmentationEngine worker thread (packMask),
|
|
75
|
+
// which is single-threaded, so no locking. (If a future change ever reads
|
|
76
|
+
// these off that thread, e.g. on the GL thread, they would need @Volatile.)
|
|
77
|
+
private var smoothedMask: FloatArray? = null
|
|
78
|
+
private var smoothedMaskW: Int = 0
|
|
79
|
+
private var smoothedMaskH: Int = 0
|
|
80
|
+
|
|
73
81
|
/**
|
|
74
|
-
* Per-frame mask production. Always returns immediately (no
|
|
75
|
-
* on the GL thread). Returns the GL texture handle of the latest
|
|
76
|
-
* mask, or -1 if no segmentation has completed yet. Callers must
|
|
77
|
-
* as "no mask this frame" and fall through to the original frame.
|
|
82
|
+
* Per-frame mask production. Always returns immediately (no segmentation
|
|
83
|
+
* blocking on the GL thread). Returns the GL texture handle of the latest
|
|
84
|
+
* available mask, or -1 if no segmentation has completed yet. Callers must
|
|
85
|
+
* treat -1 as "no mask this frame" and fall through to the original frame.
|
|
78
86
|
*/
|
|
79
87
|
fun produce(
|
|
80
88
|
source2D: Int,
|
|
81
89
|
sourceWidth: Int,
|
|
82
90
|
sourceHeight: Int,
|
|
83
|
-
downsampleSize: Int = 256,
|
|
84
91
|
): Int {
|
|
85
92
|
// Step 1: drain any pending mask the worker has produced. getAndSet
|
|
86
93
|
// claims the bitmap atomically; the GL thread is now its sole owner.
|
|
@@ -98,9 +105,15 @@ internal class Mask {
|
|
|
98
105
|
// Step 2: kick off a new segmentation if the worker is idle.
|
|
99
106
|
if (isProcessing.compareAndSet(false, true)) {
|
|
100
107
|
try {
|
|
101
|
-
val downsampleBmp =
|
|
108
|
+
val downsampleBmp =
|
|
109
|
+
renderAndReadback(source2D, sourceWidth, sourceHeight, EffectTuning.targetShortSide)
|
|
102
110
|
if (downsampleBmp != null) {
|
|
103
|
-
|
|
111
|
+
SegmentationEngine.submit(
|
|
112
|
+
downsampleBmp,
|
|
113
|
+
context,
|
|
114
|
+
onMask = { raw, w, h -> packMask(raw, w, h) },
|
|
115
|
+
onDone = { isProcessing.set(false) },
|
|
116
|
+
)
|
|
104
117
|
} else {
|
|
105
118
|
isProcessing.set(false)
|
|
106
119
|
}
|
|
@@ -114,13 +127,14 @@ internal class Mask {
|
|
|
114
127
|
}
|
|
115
128
|
|
|
116
129
|
/**
|
|
117
|
-
* Release
|
|
118
|
-
*
|
|
119
|
-
*
|
|
130
|
+
* Release this Mask's GL resources. Call from the GL thread. Does NOT touch
|
|
131
|
+
* the SegmentationEngine's worker thread or segmenter (those are process-
|
|
132
|
+
* lived and shared). Not currently invoked by any caller because
|
|
133
|
+
* VideoFrameProcessor has no explicit teardown hook; see the file header on
|
|
134
|
+
* the bounded GL leak this implies.
|
|
120
135
|
*/
|
|
121
136
|
fun release() {
|
|
122
137
|
try {
|
|
123
|
-
workerThread.quitSafely()
|
|
124
138
|
if (maskTextureId != 0) {
|
|
125
139
|
GLES30.glDeleteTextures(1, intArrayOf(maskTextureId), 0)
|
|
126
140
|
maskTextureId = 0
|
|
@@ -129,14 +143,57 @@ internal class Mask {
|
|
|
129
143
|
downsampleFbo = null
|
|
130
144
|
downsampleProgram?.delete()
|
|
131
145
|
downsampleProgram = null
|
|
132
|
-
segmenter?.close()
|
|
133
|
-
segmenter = null
|
|
134
146
|
pendingMaskBitmap.getAndSet(null)?.recycle()
|
|
147
|
+
smoothedMask = null
|
|
135
148
|
} catch (t: Throwable) {
|
|
136
149
|
Log.w(TAG, "Mask.release encountered an error; resources may leak", t)
|
|
137
150
|
}
|
|
138
151
|
}
|
|
139
152
|
|
|
153
|
+
// --- Worker thread (invoked by SegmentationEngine) -----------------------
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Apply EMA smoothing to the raw upright confidence mask, flip it back into
|
|
157
|
+
* the bottom-up orientation the upload/composite expects, quantize to 8-bit
|
|
158
|
+
* RGBA, and stage it for the GL thread to upload. Runs on the engine's single
|
|
159
|
+
* worker thread, so the EMA state below needs no locking.
|
|
160
|
+
*/
|
|
161
|
+
private fun packMask(raw: FloatArray, maskW: Int, maskH: Int) {
|
|
162
|
+
val pixelCount = maskW * maskH
|
|
163
|
+
|
|
164
|
+
// Temporal smoothing (exponential moving average) across mask updates, to
|
|
165
|
+
// damp shoulder-popping / edge shimmer. History resets on dim change.
|
|
166
|
+
val prevSmoothed = smoothedMask
|
|
167
|
+
val blend = prevSmoothed != null && smoothedMaskW == maskW && smoothedMaskH == maskH
|
|
168
|
+
val smoothed = if (blend) prevSmoothed!! else FloatArray(pixelCount)
|
|
169
|
+
|
|
170
|
+
val outPixels = IntArray(pixelCount)
|
|
171
|
+
for (i in 0 until pixelCount) {
|
|
172
|
+
val r = raw[i].coerceIn(0f, 1f)
|
|
173
|
+
val s = if (blend) MASK_EMA_ALPHA * r + (1f - MASK_EMA_ALPHA) * smoothed[i] else r
|
|
174
|
+
smoothed[i] = s
|
|
175
|
+
val c = (s * 255f + 0.5f).toInt() and 0xFF
|
|
176
|
+
// Flip vertically back into the orientation the upload/composite expects
|
|
177
|
+
// (the engine segmented an upright copy). smoothedMask stays in upright
|
|
178
|
+
// space for frame-to-frame EMA consistency; only the output is flipped.
|
|
179
|
+
val row = i / maskW
|
|
180
|
+
val col = i - row * maskW
|
|
181
|
+
outPixels[(maskH - 1 - row) * maskW + col] = (0xFF shl 24) or (c shl 16) or (c shl 8) or c
|
|
182
|
+
}
|
|
183
|
+
smoothedMask = smoothed
|
|
184
|
+
smoothedMaskW = maskW
|
|
185
|
+
smoothedMaskH = maskH
|
|
186
|
+
|
|
187
|
+
val outBmp = Bitmap.createBitmap(maskW, maskH, Bitmap.Config.ARGB_8888)
|
|
188
|
+
outBmp.setPixels(outPixels, 0, maskW, 0, 0, maskW, maskH)
|
|
189
|
+
|
|
190
|
+
// Hand off to GL thread. getAndSet atomically claims any previously
|
|
191
|
+
// unconsumed bitmap as `prev` so we own the recycle; the GL thread
|
|
192
|
+
// can never observe the same reference we are about to free.
|
|
193
|
+
val prev = pendingMaskBitmap.getAndSet(outBmp)
|
|
194
|
+
prev?.recycle()
|
|
195
|
+
}
|
|
196
|
+
|
|
140
197
|
// --- GL thread -----------------------------------------------------------
|
|
141
198
|
|
|
142
199
|
private fun renderAndReadback(
|
|
@@ -184,59 +241,8 @@ internal class Mask {
|
|
|
184
241
|
GlDebug.check("mask upload texImage2D")
|
|
185
242
|
}
|
|
186
243
|
|
|
187
|
-
// --- Worker thread -------------------------------------------------------
|
|
188
|
-
|
|
189
|
-
private fun runSegmentation(inputBmp: Bitmap) {
|
|
190
|
-
try {
|
|
191
|
-
val seg = ensureSegmenter()
|
|
192
|
-
val inputImage = InputImage.fromBitmap(inputBmp, 0)
|
|
193
|
-
val rawMask = Tasks.await(seg.process(inputImage))
|
|
194
|
-
|
|
195
|
-
val maskBuffer = rawMask.buffer.order(ByteOrder.nativeOrder()).asFloatBuffer()
|
|
196
|
-
val maskW = rawMask.width
|
|
197
|
-
val maskH = rawMask.height
|
|
198
|
-
|
|
199
|
-
val outPixels = IntArray(maskW * maskH)
|
|
200
|
-
maskBuffer.rewind()
|
|
201
|
-
for (i in 0 until maskW * maskH) {
|
|
202
|
-
val c = (maskBuffer.get().coerceIn(0f, 1f) * 255f + 0.5f).toInt() and 0xFF
|
|
203
|
-
outPixels[i] = (0xFF shl 24) or (c shl 16) or (c shl 8) or c
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
val outBmp = Bitmap.createBitmap(maskW, maskH, Bitmap.Config.ARGB_8888)
|
|
207
|
-
outBmp.setPixels(outPixels, 0, maskW, 0, 0, maskW, maskH)
|
|
208
|
-
|
|
209
|
-
// Hand off to GL thread. getAndSet atomically claims any previously
|
|
210
|
-
// unconsumed bitmap as `prev` so we own the recycle; the GL thread
|
|
211
|
-
// can never observe the same reference we are about to free.
|
|
212
|
-
val prev = pendingMaskBitmap.getAndSet(outBmp)
|
|
213
|
-
prev?.recycle()
|
|
214
|
-
} catch (t: Throwable) {
|
|
215
|
-
Log.e(TAG, "runSegmentation failed on worker", t)
|
|
216
|
-
} finally {
|
|
217
|
-
inputBmp.recycle()
|
|
218
|
-
isProcessing.set(false)
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
244
|
// --- Lazy init helpers ---------------------------------------------------
|
|
223
245
|
|
|
224
|
-
private fun ensureSegmenter(): Segmenter {
|
|
225
|
-
val existing = segmenter
|
|
226
|
-
if (existing != null) return existing
|
|
227
|
-
val seg = Segmentation.getClient(
|
|
228
|
-
SelfieSegmenterOptions.Builder()
|
|
229
|
-
.setDetectorMode(SelfieSegmenterOptions.STREAM_MODE)
|
|
230
|
-
// Raw model resolution; MLKit returns the mask at the segmenter's
|
|
231
|
-
// native size instead of upsampling internally. Faster per call;
|
|
232
|
-
// the composite shader's smoothstep softens the coarser edge.
|
|
233
|
-
.enableRawSizeMask()
|
|
234
|
-
.build(),
|
|
235
|
-
)
|
|
236
|
-
segmenter = seg
|
|
237
|
-
return seg
|
|
238
|
-
}
|
|
239
|
-
|
|
240
246
|
private fun ensureDownsampleFbo(w: Int, h: Int): Fbo {
|
|
241
247
|
val existing = downsampleFbo
|
|
242
248
|
if (existing != null && existing.width == w && existing.height == h) return existing
|
|
@@ -285,6 +291,11 @@ internal class Mask {
|
|
|
285
291
|
companion object {
|
|
286
292
|
private const val TAG = "Kaleidoscope.Mask"
|
|
287
293
|
|
|
294
|
+
// EMA weight for the new mask vs history. Higher = more responsive, lower =
|
|
295
|
+
// smoother (more lag). 0.5 is ~a 1-2 update time constant at the ~10-20 Hz
|
|
296
|
+
// mask rate: damps flicker without obvious lag.
|
|
297
|
+
private const val MASK_EMA_ALPHA = 0.5f
|
|
298
|
+
|
|
288
299
|
private const val TWO_D_PASSTHROUGH_FRAG = """#version 300 es
|
|
289
300
|
precision mediump float;
|
|
290
301
|
uniform sampler2D uTex;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Process-wide segmentation worker. Owns the ONE MediaPipe Tasks
|
|
2
|
+
// ImageSegmenter and the ONE worker thread for the whole process; every Mask
|
|
3
|
+
// instance (one per active VideoFrameProcessor) submits through here instead of
|
|
4
|
+
// spinning up its own thread + segmenter.
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS IS A SINGLETON: react-native-webrtc calls VideoFrameProcessorFactory
|
|
7
|
+
// .build() on EVERY effect switch from JS, constructing a fresh processor (and
|
|
8
|
+
// thus a fresh Mask) each time, and upstream has no teardown hook to dispose
|
|
9
|
+
// the old one. If each Mask owned a HandlerThread + ImageSegmenter, toggling
|
|
10
|
+
// effects would leak a thread and a native segmenter handle per switch and
|
|
11
|
+
// eventually OOM on a constrained device. Hoisting the thread + segmenter here
|
|
12
|
+
// bounds them to one-per-process regardless of how many Masks come and go.
|
|
13
|
+
//
|
|
14
|
+
// The segmenter is CPU (BitmapImageBuilder input), so sharing it across Masks
|
|
15
|
+
// is safe; VIDEO running mode only needs monotonically increasing timestamps,
|
|
16
|
+
// which a single shared counter guarantees. Per-stream temporal state (the EMA
|
|
17
|
+
// smoothing) stays on each Mask, so two simultaneous tracks do not blend masks.
|
|
18
|
+
//
|
|
19
|
+
// The thread and segmenter are created lazily on first submit and live for the
|
|
20
|
+
// process lifetime (the intended steady state: one thread, one segmenter). They
|
|
21
|
+
// are intentionally never torn down.
|
|
22
|
+
|
|
23
|
+
package com.simiancraft.kaleidoscope.segmentation
|
|
24
|
+
|
|
25
|
+
import android.content.Context
|
|
26
|
+
import android.graphics.Bitmap
|
|
27
|
+
import android.graphics.Matrix
|
|
28
|
+
import android.os.Handler
|
|
29
|
+
import android.os.HandlerThread
|
|
30
|
+
import android.util.Log
|
|
31
|
+
import com.google.mediapipe.framework.image.BitmapImageBuilder
|
|
32
|
+
import com.google.mediapipe.framework.image.ByteBufferExtractor
|
|
33
|
+
import com.google.mediapipe.framework.image.MPImage
|
|
34
|
+
import com.google.mediapipe.tasks.core.BaseOptions
|
|
35
|
+
import com.google.mediapipe.tasks.vision.core.RunningMode
|
|
36
|
+
import com.google.mediapipe.tasks.vision.imagesegmenter.ImageSegmenter
|
|
37
|
+
import java.nio.ByteBuffer
|
|
38
|
+
import java.nio.ByteOrder
|
|
39
|
+
|
|
40
|
+
internal object SegmentationEngine {
|
|
41
|
+
private const val TAG = "Kaleidoscope.SegEngine"
|
|
42
|
+
|
|
43
|
+
// Lazy, process-lived. Created on first submit, never torn down.
|
|
44
|
+
private val workerThread: HandlerThread by lazy {
|
|
45
|
+
HandlerThread("Kaleidoscope.MaskWorker").also { it.start() }
|
|
46
|
+
}
|
|
47
|
+
private val workerHandler: Handler by lazy { Handler(workerThread.looper) }
|
|
48
|
+
|
|
49
|
+
// Worker-thread-confined state.
|
|
50
|
+
private var segmenter: ImageSegmenter? = null
|
|
51
|
+
private var videoTimestamp: Long = 0
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Run segmentation for [inputBmp] off the GL thread. [inputBmp] is the
|
|
55
|
+
* downsampled, bottom-up GL readback; this uprights it (the selfie model is
|
|
56
|
+
* trained on upright people and does its worst on an inverted one), segments
|
|
57
|
+
* it, and hands the raw foreground-confidence mask back via [onMask] in the
|
|
58
|
+
* SAME upright orientation the caller's EMA/flip-back expects.
|
|
59
|
+
*
|
|
60
|
+
* [onMask] is invoked on the worker thread only on success. [onDone] is
|
|
61
|
+
* invoked on the worker thread exactly once, success or failure, so the
|
|
62
|
+
* caller can reset its in-flight throttle. [inputBmp] is recycled here.
|
|
63
|
+
*/
|
|
64
|
+
fun submit(
|
|
65
|
+
inputBmp: Bitmap,
|
|
66
|
+
context: Context,
|
|
67
|
+
onMask: (rawUpright: FloatArray, width: Int, height: Int) -> Unit,
|
|
68
|
+
onDone: () -> Unit,
|
|
69
|
+
) {
|
|
70
|
+
workerHandler.post {
|
|
71
|
+
var upright: Bitmap? = null
|
|
72
|
+
var mpImage: MPImage? = null
|
|
73
|
+
try {
|
|
74
|
+
val seg = ensureSegmenter(context)
|
|
75
|
+
// The GL readback (glReadPixels reads bottom-to-top) is vertically
|
|
76
|
+
// flipped (head-down). Feed the segmenter an upright copy; the caller
|
|
77
|
+
// flips the mask back so the upload/composite alignment is untouched.
|
|
78
|
+
// Vertical flip only, not a 180 rotate (columns are already correct).
|
|
79
|
+
upright = flipVertical(inputBmp)
|
|
80
|
+
mpImage = BitmapImageBuilder(upright).build()
|
|
81
|
+
val result = seg.segmentForVideo(mpImage, videoTimestamp++)
|
|
82
|
+
|
|
83
|
+
val confidenceMasks = result.confidenceMasks()
|
|
84
|
+
if (!confidenceMasks.isPresent || confidenceMasks.get().isEmpty()) {
|
|
85
|
+
Log.w(TAG, "segmentation produced no confidence mask")
|
|
86
|
+
return@post
|
|
87
|
+
}
|
|
88
|
+
// General selfie segmenter: a single foreground-confidence mask
|
|
89
|
+
// (float [0,1], higher = person). Copy it out of MediaPipe's buffer
|
|
90
|
+
// into a plain array the caller owns.
|
|
91
|
+
val maskImage = confidenceMasks.get()[0]
|
|
92
|
+
val maskW = maskImage.width
|
|
93
|
+
val maskH = maskImage.height
|
|
94
|
+
val maskBuffer = ByteBufferExtractor.extract(maskImage)
|
|
95
|
+
.order(ByteOrder.nativeOrder())
|
|
96
|
+
.asFloatBuffer()
|
|
97
|
+
val raw = FloatArray(maskW * maskH)
|
|
98
|
+
maskBuffer.rewind()
|
|
99
|
+
maskBuffer.get(raw)
|
|
100
|
+
onMask(raw, maskW, maskH)
|
|
101
|
+
} catch (t: Throwable) {
|
|
102
|
+
Log.e(TAG, "segmentation failed on worker", t)
|
|
103
|
+
} finally {
|
|
104
|
+
// close() in finally: if segmentForVideo throws, the MPImage native
|
|
105
|
+
// handle would otherwise leak (unbounded if the error recurs per frame).
|
|
106
|
+
mpImage?.close()
|
|
107
|
+
upright?.recycle()
|
|
108
|
+
inputBmp.recycle()
|
|
109
|
+
onDone()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private fun ensureSegmenter(context: Context): ImageSegmenter {
|
|
115
|
+
val existing = segmenter
|
|
116
|
+
if (existing != null) return existing
|
|
117
|
+
// Load the model as a direct ByteBuffer (setModelAssetBuffer) rather than
|
|
118
|
+
// setModelAssetPath: the path variant memory-maps the asset and requires it
|
|
119
|
+
// to be stored uncompressed (aaptOptions noCompress), which we cannot
|
|
120
|
+
// guarantee in the CONSUMING app's build. Reading the asset into a direct
|
|
121
|
+
// buffer ourselves works regardless of apk compression.
|
|
122
|
+
val appContext = context.applicationContext
|
|
123
|
+
val modelBytes = appContext.assets.open("selfie_segmenter.tflite").use { it.readBytes() }
|
|
124
|
+
val modelBuffer = ByteBuffer.allocateDirect(modelBytes.size).order(ByteOrder.nativeOrder())
|
|
125
|
+
modelBuffer.put(modelBytes)
|
|
126
|
+
modelBuffer.rewind()
|
|
127
|
+
|
|
128
|
+
val baseOptions = BaseOptions.builder()
|
|
129
|
+
.setModelAssetBuffer(modelBuffer)
|
|
130
|
+
.build()
|
|
131
|
+
val options = ImageSegmenter.ImageSegmenterOptions.builder()
|
|
132
|
+
.setBaseOptions(baseOptions)
|
|
133
|
+
.setRunningMode(RunningMode.VIDEO)
|
|
134
|
+
.setOutputConfidenceMasks(true)
|
|
135
|
+
.setOutputCategoryMask(false)
|
|
136
|
+
.build()
|
|
137
|
+
val seg = ImageSegmenter.createFromOptions(appContext, options)
|
|
138
|
+
segmenter = seg
|
|
139
|
+
return seg
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Vertical mirror (flip across the horizontal axis). Uprights the
|
|
143
|
+
* bottom-to-top glReadPixels frame before segmentation. */
|
|
144
|
+
private fun flipVertical(src: Bitmap): Bitmap {
|
|
145
|
+
val m = Matrix().apply { postScale(1f, -1f) }
|
|
146
|
+
return Bitmap.createBitmap(src, 0, 0, src.width, src.height, m, true)
|
|
147
|
+
}
|
|
148
|
+
}
|