Skip to content

Commit f5628f6

Browse files
authored
Refactor Android Example for Dynamic Image Segmentation (#37)
- Enhance image loading: Add support for loading images from /sdcard and /data partitions, with user-granted permissions. - Improve User Experience: Introduce user-friendly toast messages to provide feedback on various UI actions and status updates. - Comprehensive Testing: Verify functionality on both Android Emulator and physical devices.
1 parent f680777 commit f5628f6

File tree

4 files changed

+198
-54
lines changed

4 files changed

+198
-54
lines changed

dl3/android/DeepLabV3Demo/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
android:maxSdkVersion="40" />
99

1010
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
11+
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
1112

1213
<application
1314
android:allowBackup="false"

dl3/android/DeepLabV3Demo/app/src/main/java/org/pytorch/executorchexamples/dl3/MainActivity.java

Lines changed: 160 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,30 @@
88

99
package org.pytorch.executorchexamples.dl3;
1010

11+
import android.Manifest;
1112
import android.app.Activity;
1213
import android.content.Context;
14+
import android.content.pm.PackageManager;
1315
import android.graphics.Bitmap;
1416
import android.graphics.BitmapFactory;
1517
import android.os.Bundle;
18+
import android.os.Build;
19+
import android.os.Environment;
1620
import android.os.SystemClock;
1721
import android.util.Log;
1822
import android.view.View;
1923
import android.widget.Button;
2024
import android.widget.ImageView;
2125
import android.widget.ProgressBar;
26+
import android.widget.Toast;
27+
28+
import androidx.annotation.NonNull;
29+
import androidx.core.app.ActivityCompat;
30+
import androidx.core.content.ContextCompat;
31+
32+
import java.io.File;
2233
import java.io.IOException;
2334
import java.util.ArrayList;
24-
import java.util.Objects;
2535
import org.pytorch.executorch.EValue;
2636
import org.pytorch.executorch.Module;
2737
import org.pytorch.executorch.Tensor;
@@ -34,39 +44,134 @@ public class MainActivity extends Activity implements Runnable {
3444
private Module mModule = null;
3545
private String mImagename = "corgi.jpeg";
3646

37-
private String[] mImageFiles;
47+
private final ArrayList<String> mImageFiles = new ArrayList<>();
48+
3849
private int mCurrentImageIndex = 0;
3950

51+
private static final int REQUEST_READ_EXTERNAL_STORAGE = 1001;
52+
private static final String LOCAL_IMAGE_DIR = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getAbsolutePath() + "/";
53+
4054
// see http://host.robots.ox.ac.uk:8080/pascal/VOC/voc2007/segexamples/index.html for the list of
4155
// classes with indexes
4256
private static final int CLASSNUM = 21;
4357
private static final int DOG = 12;
4458
private static final int PERSON = 15;
4559
private static final int SHEEP = 17;
4660

47-
private void populateImage() {
61+
private void checkAndRequestStoragePermission() {
62+
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
63+
!= PackageManager.PERMISSION_GRANTED) {
64+
// Permission is not granted, request it
65+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+
66+
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_IMAGES)
67+
!= PackageManager.PERMISSION_GRANTED) {
68+
ActivityCompat.requestPermissions(this,
69+
new String[]{Manifest.permission.READ_MEDIA_IMAGES},
70+
REQUEST_READ_EXTERNAL_STORAGE);
71+
} else {
72+
// Permission already granted, proceed with file access
73+
loadImagesFromLocal();
74+
}
75+
} else {
76+
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
77+
!= PackageManager.PERMISSION_GRANTED) {
78+
ActivityCompat.requestPermissions(this,
79+
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
80+
REQUEST_READ_EXTERNAL_STORAGE);
81+
} else {
82+
// Permission already granted, proceed with file access
83+
loadImagesFromLocal();
84+
}
85+
}
86+
} else {
87+
// Permission already granted, proceed with file access
88+
loadImagesFromLocal();
89+
}
90+
}
91+
92+
// Handle the permission request result
93+
@Override
94+
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
95+
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
96+
if (requestCode == REQUEST_READ_EXTERNAL_STORAGE) {
97+
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
98+
// Permission granted
99+
loadImagesFromLocal();
100+
} else {
101+
// Permission denied, show a message or fallback
102+
showUIMessage(this, "Permission denied to read external storage");
103+
}
104+
}
105+
106+
// Load images from assets anyways
107+
populateImagePathFromAssets();
108+
}
109+
110+
private void loadImagesFromLocal() {
111+
// Load images from /data/local & /sdcard/Pictures
112+
boolean hasAnyLocalFiles = populateImagePathFromLocal();
113+
boolean isImageShown = showImage();
114+
if (hasAnyLocalFiles && isImageShown) {
115+
showUIMessage(this, "Refreshed images from external storage and/or Assets folder");
116+
}
117+
}
118+
119+
private boolean showImage() {
120+
boolean isImageShown = false;
121+
if (mImagename == null) {
122+
showUIMessage(this, "No image to display");
123+
mImageView.setImageBitmap(null);
124+
return isImageShown;
125+
}
48126
try {
49-
mBitmap = BitmapFactory.decodeStream(getAssets().open(mImagename));
50-
mBitmap = Bitmap.createScaledBitmap(mBitmap, 224, 224, true);
51-
mImageView.setImageBitmap(mBitmap);
127+
if (mImagename.startsWith("/")) {
128+
mBitmap = BitmapFactory.decodeFile(mImagename);
129+
} else {
130+
mBitmap = BitmapFactory.decodeStream(getAssets().open(mImagename));
131+
}
132+
if (mBitmap != null) {
133+
mBitmap = Bitmap.createScaledBitmap(mBitmap, 224, 224, true);
134+
mImageView.setImageBitmap(mBitmap);
135+
isImageShown = true;
136+
}
52137
} catch (IOException e) {
53-
Log.e("ImageSegmentation", "Error reading assets", e);
54-
finish();
138+
Log.e("ImageSegmentation", "Error reading image", e);
139+
mImageView.setImageBitmap(null);
55140
}
141+
return isImageShown;
142+
}
143+
144+
private boolean populateImagePathFromLocal() {
145+
boolean hasLocalFiles = false;
146+
File dir = new File(LOCAL_IMAGE_DIR);
147+
File[] files = dir.listFiles((d, name) ->
148+
name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".png"));
149+
ArrayList<String> imageList = new ArrayList<>();
150+
if (files != null && files.length > 0) {
151+
for (int i = 0; i < files.length; i++) {
152+
mImageFiles.add(files[i].getAbsolutePath());
153+
hasLocalFiles = true;
154+
}
155+
mImagename = mImageFiles.get(0);
156+
} else {
157+
mImagename = null;
158+
}
159+
160+
return hasLocalFiles;
56161
}
57162

58163
private void populateImagePathFromAssets() {
59164
try {
60165
String[] allFiles = getAssets().list("");
61-
ArrayList<String> imageList = new ArrayList<>();
62-
for (String file : allFiles) {
63-
if (file.endsWith(".jpg") || file.endsWith(".jpeg") || file.endsWith(".png")) {
64-
imageList.add(file);
166+
if (allFiles != null && allFiles.length > 0) {
167+
for (String file : allFiles) {
168+
if (file.endsWith(".jpg") || file.endsWith(".jpeg") || file.endsWith(".png")) {
169+
mImageFiles.add(file);
170+
}
65171
}
172+
mCurrentImageIndex = 0;
173+
mImagename = !mImageFiles.isEmpty() ? mImageFiles.get(0) : null;
66174
}
67-
mImageFiles = imageList.toArray(new String[0]);
68-
mCurrentImageIndex = 0;
69-
mImagename = mImageFiles.length > 0 ? mImageFiles[0] : null;
70175
} catch (IOException e) {
71176
Log.e("ImageSegmentation", "Error listing assets", e);
72177
finish();
@@ -77,38 +182,33 @@ private void populateImagePathFromAssets() {
77182
protected void onCreate(Bundle savedInstanceState) {
78183
super.onCreate(savedInstanceState);
79184
setContentView(R.layout.activity_main);
80-
populateImagePathFromAssets();
81185

82-
try {
83-
mBitmap = BitmapFactory.decodeStream(getAssets().open(mImagename), null, null);
84-
mBitmap = Bitmap.createScaledBitmap(mBitmap, 224, 224, true);
85-
} catch (IOException e) {
86-
Log.e("ImageSegmentation", "Error reading assets", e);
87-
finish();
88-
}
186+
// Initialize all views first!
187+
mImageView = findViewById(R.id.imageView);
188+
mButtonXnnpack = findViewById(R.id.xnnpackButton);
189+
mProgressBar = findViewById(R.id.progressBar);
89190

90-
mModule = Module.load("/data/local/tmp/dl3_xnnpack_fp32.pte");
191+
populateImagePathFromAssets();
192+
showImage();
91193

92-
mImageView = findViewById(R.id.imageView);
194+
mModule = Module.load("/data/local/tmp/dl3_xnnpack_fp32.pte");
93195
mImageView.setImageBitmap(mBitmap);
94196

95197
final Button buttonNext = findViewById(R.id.nextButton);
96198
buttonNext.setOnClickListener(
97199
new View.OnClickListener() {
98200
public void onClick(View v) {
99-
if (mImageFiles == null || mImageFiles.length == 0) {
201+
if (mImageFiles == null || mImageFiles.isEmpty()) {
100202
// No images available
101203
return;
102204
}
103205
// Move to the next image, wrap around if at the end
104-
mCurrentImageIndex = (mCurrentImageIndex + 1) % mImageFiles.length;
105-
mImagename = mImageFiles[mCurrentImageIndex];
106-
populateImage();
206+
mCurrentImageIndex = (mCurrentImageIndex + 1) % mImageFiles.size();
207+
mImagename = mImageFiles.get(mCurrentImageIndex);
208+
showImage();
107209
}
108210
});
109211

110-
mButtonXnnpack = findViewById(R.id.xnnpackButton);
111-
mProgressBar = (ProgressBar) findViewById(R.id.progressBar);
112212
mButtonXnnpack.setOnClickListener(
113213
new View.OnClickListener() {
114214
public void onClick(View v) {
@@ -125,7 +225,19 @@ public void onClick(View v) {
125225

126226
final Button resetImage = findViewById(R.id.resetImage);
127227
resetImage.setOnClickListener(
128-
v -> populateImage());
228+
v -> showImage());
229+
230+
// Refresh Button for External Storage
231+
final Button loadAndRefreshButton = findViewById(R.id.loadAndRefreshButton);
232+
loadAndRefreshButton.setOnClickListener(
233+
v -> {
234+
mImageFiles.clear();
235+
checkAndRequestStoragePermission();
236+
237+
populateImagePathFromAssets();
238+
showImage();
239+
}
240+
);
129241
}
130242

131243
@Override
@@ -136,6 +248,7 @@ public void run() {
136248
TensorImageUtils.TORCHVISION_NORM_MEAN_RGB,
137249
TensorImageUtils.TORCHVISION_NORM_STD_RGB);
138250

251+
boolean imageSegementationSuccess = false;
139252
final long startTime = SystemClock.elapsedRealtime();
140253
Tensor outputTensor = mModule.forward(EValue.from(inputTensor))[0].toTensor();
141254
final long inferenceTime = SystemClock.elapsedRealtime() - startTime;
@@ -163,6 +276,9 @@ public void run() {
163276
else if (maxi == DOG) intValues[maxj * width + maxk] = 0xFF00FF00; // G
164277
else if (maxi == SHEEP) intValues[maxj * width + maxk] = 0xFF0000FF; // B
165278
else intValues[maxj * width + maxk] = 0xFF000000;
279+
if (maxi == PERSON || maxi == DOG || maxi == SHEEP) {
280+
imageSegementationSuccess = true;
281+
}
166282
}
167283
}
168284

@@ -179,12 +295,24 @@ public void run() {
179295
final Bitmap transferredBitmap =
180296
Bitmap.createScaledBitmap(outputBitmap, mBitmap.getWidth(), mBitmap.getHeight(), true);
181297

298+
final boolean showUserIndicationOnImgSegFail = !imageSegementationSuccess;
182299
runOnUiThread(
183300
() -> {
301+
if (showUserIndicationOnImgSegFail) {
302+
Toast.makeText(this, "ImageSegmentation Failed", Toast.LENGTH_SHORT).show();
303+
}
184304
mImageView.setImageBitmap(transferredBitmap);
185305
mButtonXnnpack.setEnabled(true);
186306
mButtonXnnpack.setText(R.string.run_xnnpack);
187307
mProgressBar.setVisibility(ProgressBar.INVISIBLE);
188308
});
189309
}
310+
311+
void showUIMessage(final Context context, final String msg) {
312+
runOnUiThread(new Runnable() {
313+
public void run() {
314+
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
315+
}
316+
});
317+
}
190318
}

dl3/android/DeepLabV3Demo/app/src/main/res/layout/activity_main.xml

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
android:layout_height="match_parent"
77
tools:context=".MainActivity">
88

9+
<!-- ImageView at the top -->
910
<ImageView
1011
android:id="@+id/imageView"
1112
android:layout_width="388dp"
@@ -18,49 +19,61 @@
1819
app:layout_constraintStart_toStartOf="parent"
1920
app:layout_constraintTop_toTopOf="parent" />
2021

21-
<Button
22-
android:id="@+id/nextButton"
22+
<!-- ProgressBar below ImageView -->
23+
<ProgressBar
24+
android:id="@+id/progressBar"
2325
android:layout_width="wrap_content"
2426
android:layout_height="wrap_content"
2527
android:layout_marginTop="10dp"
28+
android:visibility="invisible"
29+
app:layout_constraintTop_toBottomOf="@+id/imageView"
30+
app:layout_constraintStart_toStartOf="parent"
31+
app:layout_constraintEnd_toEndOf="parent" />
32+
33+
<!-- Row 1: Next and Reset buttons side by side -->
34+
<Button
35+
android:id="@+id/nextButton"
36+
android:layout_width="0dp"
37+
android:layout_height="wrap_content"
2638
android:text="@string/next"
2739
android:textAllCaps="false"
28-
app:layout_constraintEnd_toEndOf="parent"
29-
app:layout_constraintHorizontal_bias="0.45"
40+
app:layout_constraintTop_toBottomOf="@+id/progressBar"
3041
app:layout_constraintStart_toStartOf="parent"
31-
app:layout_constraintTop_toBottomOf="@+id/imageView" />
42+
app:layout_constraintEnd_toStartOf="@+id/resetImage"
43+
app:layout_constraintHorizontal_weight="1" />
3244

3345
<Button
3446
android:id="@+id/resetImage"
35-
android:layout_width="wrap_content"
47+
android:layout_width="0dp"
3648
android:layout_height="wrap_content"
3749
android:text="@string/reset"
3850
android:textAllCaps="false"
39-
app:layout_constraintRight_toRightOf="@+id/nextButton"
51+
app:layout_constraintTop_toBottomOf="@+id/progressBar"
4052
app:layout_constraintStart_toEndOf="@+id/nextButton"
41-
app:layout_constraintTop_toTopOf="@+id/nextButton" />
53+
app:layout_constraintEnd_toEndOf="parent"
54+
app:layout_constraintHorizontal_weight="1" />
4255

56+
<!-- Row 2: Single button spanning width of two above -->
4357
<Button
44-
android:id="@+id/xnnpackButton"
45-
android:layout_width="wrap_content"
58+
android:id="@+id/loadAndRefreshButton"
59+
android:layout_width="0dp"
4660
android:layout_height="wrap_content"
47-
android:layout_marginTop="20dp"
48-
android:text="@string/run_xnnpack"
61+
android:text="@string/load_and_refresh"
4962
android:textAllCaps="false"
50-
app:layout_constraintEnd_toEndOf="parent"
51-
app:layout_constraintHorizontal_bias="0.45"
63+
app:layout_constraintTop_toBottomOf="@+id/nextButton"
5264
app:layout_constraintStart_toStartOf="parent"
53-
app:layout_constraintTop_toBottomOf="@+id/nextButton" />
65+
app:layout_constraintEnd_toEndOf="parent" />
5466

55-
<ProgressBar
56-
android:id="@+id/progressBar"
67+
<!-- Row 3: xnnpackButton Centered Run button -->
68+
<Button
69+
android:id="@+id/xnnpackButton"
5770
android:layout_width="wrap_content"
5871
android:layout_height="wrap_content"
59-
android:layout_marginTop="10dp"
60-
android:visibility="invisible"
61-
app:layout_constraintEnd_toEndOf="parent"
62-
app:layout_constraintHorizontal_bias="0.45"
72+
android:layout_marginTop="20dp"
73+
android:text="@string/run_xnnpack"
74+
android:textAllCaps="false"
75+
app:layout_constraintTop_toBottomOf="@+id/loadAndRefreshButton"
6376
app:layout_constraintStart_toStartOf="parent"
64-
app:layout_constraintTop_toBottomOf="@+id/nextButton" />
77+
app:layout_constraintEnd_toEndOf="parent" />
6578

6679
</androidx.constraintlayout.widget.ConstraintLayout>

0 commit comments

Comments
 (0)