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.
@@ -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.1.2'
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.1.2"
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() // High quality edges
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
- val inputImage = InputImage.fromBitmap(bitmap, 0)
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
- // 4. Create Mask Bitmap and Blend
37
- val maskBitmap = createMaskFromBuffer(maskBuffer, bitmap.width, bitmap.height)
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
- return saveResult(outputBitmap)
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
- // Stage B: Calculate optimal inSampleSize (power of 2)
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).use {
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 targetWidth = (subsampledBitmap.width * scale).toInt()
70
- val targetHeight = (subsampledBitmap.height * scale).toInt()
71
-
72
- return Bitmap.createScaledBitmap(subsampledBitmap, targetWidth, targetHeight, true)
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
- val alpha = (buffer.get() * 255).toInt()
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
- // Pass the internal context to the processor
32
- val processor = BackgroundRemoverProcessor(appContext.reactContext!!)
33
- return@AsyncFunction processor.processImage(imageUri)
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-background-remover",
3
- "version": "0.1.2",
3
+ "version": "0.2.2",
4
4
  "description": "Expo Module For High Quality Background Remover",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",