expo-background-remover 0.1.2 → 0.2.2
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/android/build.gradle
CHANGED
|
@@ -2,7 +2,7 @@ apply plugin: 'com.android.library'
|
|
|
2
2
|
apply plugin: 'kotlin-android'
|
|
3
3
|
|
|
4
4
|
group = 'expo.modules.backgroundremover'
|
|
5
|
-
version = '0.
|
|
5
|
+
version = '0.2.2'
|
|
6
6
|
|
|
7
7
|
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
8
8
|
apply from: expoModulesCorePlugin
|
|
@@ -37,7 +37,7 @@ android {
|
|
|
37
37
|
namespace "expo.modules.backgroundremover"
|
|
38
38
|
defaultConfig {
|
|
39
39
|
versionCode 1
|
|
40
|
-
versionName "0.
|
|
40
|
+
versionName "0.2.2"
|
|
41
41
|
}
|
|
42
42
|
lintOptions {
|
|
43
43
|
abortOnError false
|
|
@@ -65,5 +65,6 @@ dependencies {
|
|
|
65
65
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
|
|
66
66
|
|
|
67
67
|
implementation("com.google.android.gms:play-services-mlkit-subject-segmentation:16.0.0-beta1")
|
|
68
|
+
implementation("androidx.exifinterface:exifinterface:1.4.2")
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -3,10 +3,13 @@ package expo.modules.backgroundremover
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.graphics.*
|
|
5
5
|
import android.net.Uri
|
|
6
|
+
import androidx.exifinterface.media.ExifInterface // Added for getRotation
|
|
6
7
|
import com.google.mlkit.vision.common.InputImage
|
|
7
8
|
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
|
|
8
9
|
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
|
|
10
|
+
import kotlinx.coroutines.Dispatchers
|
|
9
11
|
import kotlinx.coroutines.tasks.await
|
|
12
|
+
import kotlinx.coroutines.withContext
|
|
10
13
|
import java.io.File
|
|
11
14
|
import java.io.FileOutputStream
|
|
12
15
|
import java.util.UUID
|
|
@@ -17,59 +20,80 @@ class BackgroundRemoverProcessor(private val context: Context) {
|
|
|
17
20
|
|
|
18
21
|
private val segmenter = SubjectSegmentation.getClient(
|
|
19
22
|
SubjectSegmenterOptions.Builder()
|
|
20
|
-
.enableForegroundConfidenceMask()
|
|
23
|
+
.enableForegroundConfidenceMask()
|
|
21
24
|
.build()
|
|
22
25
|
)
|
|
23
26
|
|
|
24
|
-
suspend fun processImage(uriString: String): String {
|
|
25
|
-
// 1. Efficiently load and downscale to max 2048px
|
|
27
|
+
suspend fun processImage(uriString: String): String = withContext(Dispatchers.IO) {
|
|
26
28
|
val bitmap = loadAndResizeBitmap(uriString, 2048)
|
|
27
|
-
|
|
29
|
+
|
|
30
|
+
// FIX 1: Defined getRotation helper
|
|
31
|
+
val inputImage = InputImage.fromBitmap(bitmap, getRotation(uriString))
|
|
28
32
|
|
|
29
|
-
// 2. Perform Segmentation
|
|
30
33
|
val result = segmenter.process(inputImage).await()
|
|
31
34
|
|
|
32
|
-
// 3. Retrieve global mask (Includes People + Objects automatically)
|
|
33
35
|
val maskBuffer = result.foregroundConfidenceMask
|
|
34
36
|
?: throw Exception("Could not detect subjects")
|
|
35
|
-
|
|
36
|
-
//
|
|
37
|
-
val
|
|
37
|
+
|
|
38
|
+
// FIX 2: Use bitmap dimensions (result does not have width/height)
|
|
39
|
+
val totalPixels = maskBuffer.remaining()
|
|
40
|
+
val maskWidth = bitmap.width
|
|
41
|
+
val maskHeight = totalPixels / maskWidth
|
|
42
|
+
|
|
43
|
+
val maskBitmap = createMaskFromBuffer(maskBuffer, maskWidth, maskHeight)
|
|
38
44
|
val outputBitmap = applyMaskToBitmap(bitmap, maskBitmap)
|
|
39
45
|
|
|
40
|
-
|
|
46
|
+
saveResult(outputBitmap)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Helper to handle image rotation logic
|
|
50
|
+
private fun getRotation(uriString: String): Int {
|
|
51
|
+
return try {
|
|
52
|
+
val inputStream = context.contentResolver.openInputStream(Uri.parse(uriString))
|
|
53
|
+
val exifInterface = inputStream?.use { ExifInterface(it) }
|
|
54
|
+
when (exifInterface?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) {
|
|
55
|
+
ExifInterface.ORIENTATION_ROTATE_90 -> 90
|
|
56
|
+
ExifInterface.ORIENTATION_ROTATE_180 -> 180
|
|
57
|
+
ExifInterface.ORIENTATION_ROTATE_270 -> 270
|
|
58
|
+
else -> 0
|
|
59
|
+
}
|
|
60
|
+
} catch (e: Exception) {
|
|
61
|
+
0
|
|
62
|
+
}
|
|
41
63
|
}
|
|
42
64
|
|
|
43
65
|
private fun loadAndResizeBitmap(uriString: String, maxDimension: Int): Bitmap {
|
|
44
66
|
val uri = Uri.parse(uriString)
|
|
45
|
-
|
|
46
|
-
// Stage A: Get dimensions only
|
|
47
67
|
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
48
|
-
context.contentResolver.openInputStream(uri)?.use { stream ->
|
|
49
|
-
BitmapFactory.decodeStream(stream, null, loadOptions)
|
|
50
|
-
}?: throw Exception("Failed to open input stream")
|
|
51
68
|
|
|
52
|
-
|
|
69
|
+
context.contentResolver.openInputStream(uri)?.use {
|
|
70
|
+
BitmapFactory.decodeStream(it, null, options)
|
|
71
|
+
} ?: throw Exception("Failed to open input stream")
|
|
72
|
+
|
|
53
73
|
var sampleSize = 1
|
|
54
74
|
while ((options.outWidth / (sampleSize * 2)) >= maxDimension &&
|
|
55
75
|
(options.outHeight / (sampleSize * 2)) >= maxDimension) {
|
|
56
76
|
sampleSize *= 2
|
|
57
77
|
}
|
|
58
78
|
|
|
59
|
-
// Stage C: Load the subsampled bitmap
|
|
60
79
|
val loadOptions = BitmapFactory.Options().apply { inSampleSize = sampleSize }
|
|
61
|
-
val subsampledBitmap = context.contentResolver.openInputStream(uri)
|
|
80
|
+
val subsampledBitmap = context.contentResolver.openInputStream(uri)?.use {
|
|
62
81
|
BitmapFactory.decodeStream(it, null, loadOptions)
|
|
63
82
|
} ?: throw Exception("Failed to decode image")
|
|
64
83
|
|
|
65
|
-
// Stage D: Precise scaling to exactly fit within 2048px while preserving aspect ratio
|
|
66
84
|
val scale = minOf(maxDimension.toFloat() / subsampledBitmap.width, maxDimension.toFloat() / subsampledBitmap.height)
|
|
67
85
|
if (scale >= 1f) return subsampledBitmap
|
|
68
86
|
|
|
69
|
-
val
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
87
|
+
val scaledBitmap = Bitmap.createScaledBitmap(
|
|
88
|
+
subsampledBitmap,
|
|
89
|
+
(subsampledBitmap.width * scale).toInt(),
|
|
90
|
+
(subsampledBitmap.height * scale).toInt(),
|
|
91
|
+
true
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
subsampledBitmap.recycle()
|
|
95
|
+
|
|
96
|
+
return scaledBitmap
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
private fun createMaskFromBuffer(buffer: java.nio.FloatBuffer, width: Int, height: Int): Bitmap {
|
|
@@ -77,7 +101,8 @@ class BackgroundRemoverProcessor(private val context: Context) {
|
|
|
77
101
|
buffer.rewind()
|
|
78
102
|
val pixels = IntArray(width * height)
|
|
79
103
|
for (i in 0 until width * height) {
|
|
80
|
-
|
|
104
|
+
// Mask transparency based on confidence (0.0 to 1.0)
|
|
105
|
+
val alpha = (buffer.get().coerceIn(0f, 1f) * 255).toInt()
|
|
81
106
|
pixels[i] = Color.argb(alpha, 0, 0, 0)
|
|
82
107
|
}
|
|
83
108
|
mask.setPixels(pixels, 0, width, 0, 0, width, height)
|
|
@@ -101,4 +126,8 @@ class BackgroundRemoverProcessor(private val context: Context) {
|
|
|
101
126
|
FileOutputStream(file).use { bitmap.compress(Bitmap.CompressFormat.PNG, 100, it) }
|
|
102
127
|
return Uri.fromFile(file).toString()
|
|
103
128
|
}
|
|
104
|
-
|
|
129
|
+
|
|
130
|
+
fun close() {
|
|
131
|
+
segmenter.close()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -3,6 +3,7 @@ package expo.modules.backgroundremover
|
|
|
3
3
|
import expo.modules.kotlin.modules.Module
|
|
4
4
|
import expo.modules.kotlin.modules.ModuleDefinition
|
|
5
5
|
import java.net.URL
|
|
6
|
+
|
|
6
7
|
|
|
7
8
|
class ExpoBackgroundRemoverModule : Module() {
|
|
8
9
|
// Each module class must implement the definition function. The definition consists of components
|
|
@@ -28,9 +29,13 @@ class ExpoBackgroundRemoverModule : Module() {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
AsyncFunction("removeBackgroundAsync") { imageUri: String ->
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
val processor = BackgroundRemoverProcessor(appContext.reactContext!!)
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return@AsyncFunction processor.processImage(imageUri)
|
|
36
|
+
} finally {
|
|
37
|
+
processor.close() //ensure cleanup
|
|
38
|
+
}
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
|