Skip to content

Commit de20d81

Browse files
committed
Added FPS and analysis time overlay
1 parent 84383b8 commit de20d81

File tree

9 files changed

+271
-3
lines changed

9 files changed

+271
-3
lines changed

AI_MultiBarcodes_Capture/src/main/java/com/zebra/ai_multibarcodes_capture/barcodedecoder/BarcodeAnalyzer.java

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ public interface DetectionCallback {
6262
void onDetectionResult(List<BarcodeEntity> list);
6363
}
6464

65+
/**
66+
* Interface for receiving analysis timing data.
67+
* Implement this interface to display performance metrics (FPS, analysis time).
68+
*/
69+
public interface AnalysisTimingCallback {
70+
/**
71+
* Called with timing information after each analysis completes.
72+
* @param analysisTimeMs The time in milliseconds taken to analyze the frame
73+
* @param fps The current frames per second (analysis rate)
74+
*/
75+
void onAnalysisTiming(long analysisTimeMs, float fps);
76+
}
77+
6578
private static final String TAG = "BarcodeAnalyzer";
6679
private final DetectionCallback callback;
6780
private final BarcodeDecoder barcodeDecoder;
@@ -80,6 +93,17 @@ public interface DetectionCallback {
8093
// Store the last image rotation degrees for coordinate transformation
8194
private volatile int lastImageRotationDegrees = 0;
8295

96+
// Timing tracking for performance monitoring
97+
@Nullable
98+
private volatile AnalysisTimingCallback timingCallback = null;
99+
private volatile boolean timingEnabled = false;
100+
private volatile long analysisStartTimeNanos = 0;
101+
private volatile long lastAnalysisTimeMs = 0;
102+
private volatile long fpsWindowStartTime = 0;
103+
private volatile int frameCountInWindow = 0;
104+
private volatile float currentFps = 0.0f;
105+
private static final long FPS_WINDOW_MS = 1000; // Calculate FPS over 1 second window
106+
83107
/**
84108
* Constructs a new BarcodeAnalyzer with the specified callback and barcode decoder.
85109
*
@@ -116,6 +140,10 @@ public void analyze(@NonNull ImageProxy image) {
116140
final int currentOffsetX = cropOffsetX;
117141
final int currentOffsetY = cropOffsetY;
118142

143+
// Capture timing state for this analysis
144+
final boolean trackTiming = timingEnabled && timingCallback != null;
145+
final long startTimeNanos = trackTiming ? System.nanoTime() : 0;
146+
119147
Future<?> future = executorService.submit(() -> {
120148
try {
121149
Log.d(TAG, "Starting image analysis" + (currentCropRegion != null ? " with crop region" : ""));
@@ -151,6 +179,34 @@ public void analyze(@NonNull ImageProxy image) {
151179

152180
barcodeDecoder.process(imageData)
153181
.thenAccept(result -> {
182+
// Calculate timing when entering thenAccept (before isStopped check)
183+
if (trackTiming) {
184+
long endTimeNanos = System.nanoTime();
185+
long analysisTimeMs = (endTimeNanos - startTimeNanos) / 1_000_000;
186+
lastAnalysisTimeMs = analysisTimeMs;
187+
188+
// Update FPS calculation
189+
long currentTimeMs = System.currentTimeMillis();
190+
frameCountInWindow++;
191+
192+
if (fpsWindowStartTime == 0) {
193+
fpsWindowStartTime = currentTimeMs;
194+
} else {
195+
long elapsedMs = currentTimeMs - fpsWindowStartTime;
196+
if (elapsedMs >= FPS_WINDOW_MS) {
197+
// Calculate FPS and reset window
198+
currentFps = (frameCountInWindow * 1000.0f) / elapsedMs;
199+
fpsWindowStartTime = currentTimeMs;
200+
frameCountInWindow = 0;
201+
}
202+
}
203+
204+
// Notify callback with timing data
205+
if (timingCallback != null) {
206+
timingCallback.onAnalysisTiming(analysisTimeMs, currentFps);
207+
}
208+
}
209+
154210
if (!isStopped) {
155211
// Adjust bounding boxes if we used a crop region
156212
List<BarcodeEntity> adjustedResult = result;
@@ -445,4 +501,56 @@ public boolean isCroppingEnabled() {
445501
public int getLastImageRotationDegrees() {
446502
return lastImageRotationDegrees;
447503
}
504+
505+
/**
506+
* Sets the timing callback for receiving analysis performance metrics.
507+
*
508+
* @param callback The callback to receive timing data, or null to disable
509+
*/
510+
public void setTimingCallback(@Nullable AnalysisTimingCallback callback) {
511+
this.timingCallback = callback;
512+
}
513+
514+
/**
515+
* Enables or disables timing tracking for performance monitoring.
516+
* When enabled, the analyzer will calculate FPS and analysis time for each frame.
517+
*
518+
* @param enabled true to enable timing tracking, false to disable
519+
*/
520+
public void setTimingEnabled(boolean enabled) {
521+
this.timingEnabled = enabled;
522+
if (enabled) {
523+
// Reset timing state when enabling
524+
fpsWindowStartTime = 0;
525+
frameCountInWindow = 0;
526+
currentFps = 0.0f;
527+
}
528+
}
529+
530+
/**
531+
* Checks if timing tracking is enabled.
532+
*
533+
* @return true if timing tracking is enabled, false otherwise
534+
*/
535+
public boolean isTimingEnabled() {
536+
return timingEnabled;
537+
}
538+
539+
/**
540+
* Gets the last calculated analysis time.
541+
*
542+
* @return The time in milliseconds taken to analyze the last frame
543+
*/
544+
public long getLastAnalysisTimeMs() {
545+
return lastAnalysisTimeMs;
546+
}
547+
548+
/**
549+
* Gets the current frames per second rate.
550+
*
551+
* @return The current analysis rate in frames per second
552+
*/
553+
public float getCurrentFps() {
554+
return currentFps;
555+
}
448556
}

AI_MultiBarcodes_Capture/src/main/java/com/zebra/ai_multibarcodes_capture/helpers/Constants.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,10 @@ public class Constants {
235235
public static final String SHARED_PREFERENCES_CAPTURE_TRIGGER_MODE = "SHARED_PREFERENCES_CAPTURE_TRIGGER_MODE";
236236
public static final String SHARED_PREFERENCES_CAPTURE_TRIGGER_MODE_DEFAULT = "press";
237237

238+
// Display Analysis Per Second preferences
239+
public static final String SHARED_PREFERENCES_DISPLAY_ANALYSIS_PER_SECOND = "SHARED_PREFERENCES_DISPLAY_ANALYSIS_PER_SECOND";
240+
public static final boolean SHARED_PREFERENCES_DISPLAY_ANALYSIS_PER_SECOND_DEFAULT = false;
241+
238242
public static final int KEYCODE_BUTTON_R1 = 103;
239243
public static final int KEYCODE_SCAN = 10036;
240244
}

AI_MultiBarcodes_Capture/src/main/java/com/zebra/ai_multibarcodes_capture/java/CameraXLivePreviewActivity.java

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import android.view.WindowManager;
2020
import android.widget.Button;
2121
import android.widget.ImageView;
22+
import android.widget.TextView;
2223
import android.widget.Toast;
2324

2425
import androidx.annotation.Nullable;
@@ -92,7 +93,7 @@
9293
9394
* Note: Ensure that the appropriate permissions are configured in the AndroidManifest to utilize camera capabilities.
9495
*/
95-
public class CameraXLivePreviewActivity extends AppCompatActivity implements BarcodeAnalyzer.DetectionCallback {
96+
public class CameraXLivePreviewActivity extends AppCompatActivity implements BarcodeAnalyzer.DetectionCallback, BarcodeAnalyzer.AnalysisTimingCallback {
9697

9798
private ActivityCameraXlivePreviewBinding binding;
9899
private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
@@ -150,6 +151,10 @@ public void run() {
150151
private boolean isFilteringEnabled = false;
151152
private String filteringRegex = "";
152153

154+
// Analysis timing overlay
155+
private TextView analysisOverlay;
156+
private boolean displayAnalysisPerSecond = false;
157+
153158
private BarcodeHandler barcodeHandler;
154159

155160
private String selectedModel = BARCODE_DETECTION;
@@ -272,6 +277,8 @@ public void onClick(View view) {
272277
}
273278
});
274279

280+
analysisOverlay = findViewById(R.id.analysis_overlay);
281+
275282
initCaptureZone();
276283
}
277284

@@ -661,6 +668,53 @@ private void loadCaptureModeSettings() {
661668
LogUtils.d(TAG, "=== loadCaptureModeSettings() END ===");
662669
}
663670

671+
private void loadDisplayAnalysisSettings() {
672+
LogUtils.d(TAG, "=== loadDisplayAnalysisSettings() START ===");
673+
674+
// Get the SharedPreferences object
675+
SharedPreferences sharedPreferences = getSharedPreferences(getPackageName(), Context.MODE_PRIVATE);
676+
677+
// Load display analysis per second setting
678+
displayAnalysisPerSecond = sharedPreferences.getBoolean(Constants.SHARED_PREFERENCES_DISPLAY_ANALYSIS_PER_SECOND, Constants.SHARED_PREFERENCES_DISPLAY_ANALYSIS_PER_SECOND_DEFAULT);
679+
680+
// Update overlay visibility
681+
if (analysisOverlay != null) {
682+
analysisOverlay.setVisibility(displayAnalysisPerSecond ? View.VISIBLE : View.GONE);
683+
}
684+
685+
// Update analyzer timing callback
686+
updateAnalyzerTimingCallback();
687+
688+
LogUtils.d(TAG, "Display analysis per second: " + displayAnalysisPerSecond);
689+
LogUtils.d(TAG, "=== loadDisplayAnalysisSettings() END ===");
690+
}
691+
692+
private void updateAnalyzerTimingCallback() {
693+
if (barcodeHandler != null && barcodeHandler.getBarcodeAnalyzer() != null) {
694+
BarcodeAnalyzer analyzer = barcodeHandler.getBarcodeAnalyzer();
695+
if (displayAnalysisPerSecond) {
696+
analyzer.setTimingCallback(this);
697+
analyzer.setTimingEnabled(true);
698+
LogUtils.d(TAG, "Timing callback enabled on analyzer");
699+
} else {
700+
analyzer.setTimingEnabled(false);
701+
analyzer.setTimingCallback(null);
702+
LogUtils.d(TAG, "Timing callback disabled on analyzer");
703+
}
704+
}
705+
}
706+
707+
@Override
708+
public void onAnalysisTiming(long analysisTimeMs, float fps) {
709+
// Update the overlay on UI thread
710+
runOnUiThread(() -> {
711+
if (analysisOverlay != null && displayAnalysisPerSecond) {
712+
String text = String.format(getString(R.string.analysis_overlay_format), fps, analysisTimeMs);
713+
analysisOverlay.setText(text);
714+
}
715+
});
716+
}
717+
664718
private boolean isValueMatchingFilteringRegex(String data)
665719
{
666720
if(isFilteringEnabled && filteringRegex.isEmpty() == false)
@@ -994,9 +1048,12 @@ private void bindAnalysisUseCase() {
9941048
barcodeHandler.setAnalyzerReadyCallback(new BarcodeHandler.AnalyzerReadyCallback() {
9951049
@Override
9961050
public void onAnalyzerReady(BarcodeAnalyzer analyzer) {
997-
LogUtils.d(TAG, "Analyzer ready, updating crop region");
1051+
LogUtils.d(TAG, "Analyzer ready, updating crop region and timing callback");
9981052
// Run on UI thread since we access UI components
999-
runOnUiThread(() -> updateAnalyzerCropRegion());
1053+
runOnUiThread(() -> {
1054+
updateAnalyzerCropRegion();
1055+
updateAnalyzerTimingCallback();
1056+
});
10001057
}
10011058
});
10021059
});
@@ -1061,6 +1118,9 @@ public void onResume() {
10611118
// Load capture mode settings
10621119
loadCaptureModeSettings();
10631120

1121+
// Load display analysis per second settings
1122+
loadDisplayAnalysisSettings();
1123+
10641124
// Flashlight settings are now loaded after camera is bound in bindPreviewUseCase()
10651125

10661126
int currentRotation = getWindowManager().getDefaultDisplay().getRotation();

AI_MultiBarcodes_Capture/src/main/java/com/zebra/ai_multibarcodes_capture/settings/SettingsActivity.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public class SettingsActivity extends AppCompatActivity {
8989
private CheckBox cbMICROPDF, cbMICROQR, cbMSI, cbPDF417, cbQRCODE, cbTLC39, cbTRIOPTIC39;
9090
private CheckBox cbUK_POSTAL, cbUPC_A, cbUPC_E, cbUPCE1, cbUSPLANET, cbUSPOSTNET;
9191
private CheckBox cbUS4STATE, cbUS4STATE_FICS;
92+
private CheckBox cbDisplayAnalysisPerSecond;
9293

9394
private DWScanReceiver mScanReceiver;
9495

@@ -230,6 +231,7 @@ protected void onCreate(Bundle savedInstanceState) {
230231
cbUSPOSTNET = findViewById(R.id.cbUSPOSTNET);
231232
cbUS4STATE = findViewById(R.id.cbUS4STATE);
232233
cbUS4STATE_FICS = findViewById(R.id.cbUS4STATE_FICS);
234+
cbDisplayAnalysisPerSecond = findViewById(R.id.cbDisplayAnalysisPerSecond);
233235

234236
// Initially hide the LinearLayout and set collapsed state
235237
llSymbologies.setVisibility(View.GONE);
@@ -378,6 +380,7 @@ private void loadPreferences()
378380
loadHttpsPostSettings(sharedPreferences);
379381
loadFilteringSettings(sharedPreferences);
380382
loadCaptureTriggerMode(sharedPreferences);
383+
loadDisplayAnalysisPerSecond(sharedPreferences);
381384

382385
etPrefix.setText(prefix);
383386
selectExtensionRadioButton(extension);
@@ -524,6 +527,7 @@ private void savePreferences()
524527
saveHttpsPostSettings(editor);
525528
saveFilteringSettings(editor);
526529
saveCaptureTriggerMode(editor);
530+
saveDisplayAnalysisPerSecond(editor);
527531

528532
editor.putString(SHARED_PREFERENCES_EXTENSION, getSelectedExtension());
529533

@@ -1233,4 +1237,13 @@ private void toggleFiltering() {
12331237
ivToggleFiltering.setImageResource(R.drawable.ic_expand_less_24);
12341238
}
12351239
}
1240+
1241+
private void loadDisplayAnalysisPerSecond(SharedPreferences sharedPreferences) {
1242+
boolean displayAnalysisPerSecond = sharedPreferences.getBoolean(SHARED_PREFERENCES_DISPLAY_ANALYSIS_PER_SECOND, SHARED_PREFERENCES_DISPLAY_ANALYSIS_PER_SECOND_DEFAULT);
1243+
cbDisplayAnalysisPerSecond.setChecked(displayAnalysisPerSecond);
1244+
}
1245+
1246+
private void saveDisplayAnalysisPerSecond(SharedPreferences.Editor editor) {
1247+
editor.putBoolean(SHARED_PREFERENCES_DISPLAY_ANALYSIS_PER_SECOND, cbDisplayAnalysisPerSecond.isChecked());
1248+
}
12361249
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<shape xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:shape="rectangle">
4+
<solid android:color="#80000000" />
5+
<stroke
6+
android:width="2dp"
7+
android:color="#FFFFFF" />
8+
<corners android:radius="8dp" />
9+
</shape>

AI_MultiBarcodes_Capture/src/main/res/layout/activity_camera_xlive_preview.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@
4343
app:layout_constraintTop_toTopOf="@id/preview_view"
4444
app:layout_constraintBottom_toBottomOf="@id/preview_view" />
4545

46+
<TextView
47+
android:id="@+id/analysis_overlay"
48+
android:layout_width="wrap_content"
49+
android:layout_height="wrap_content"
50+
android:layout_marginTop="16dp"
51+
android:background="@drawable/analysis_overlay_background"
52+
android:padding="12dp"
53+
android:textColor="#FFFFFF"
54+
android:textSize="16sp"
55+
android:textStyle="bold"
56+
android:gravity="center"
57+
android:visibility="gone"
58+
app:layout_constraintTop_toTopOf="@id/preview_view"
59+
app:layout_constraintStart_toStartOf="@id/preview_view"
60+
app:layout_constraintEnd_toEndOf="@id/preview_view"
61+
tools:text="12.5 FPS\n45 ms" />
62+
4663
<ImageView
4764
android:id="@+id/flashlight_toggle_icon"
4865
android:layout_width="56dp"

AI_MultiBarcodes_Capture/src/main/res/layout/activity_setup.xml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,6 +1214,49 @@
12141214

12151215
</androidx.cardview.widget.CardView>
12161216

1217+
<androidx.cardview.widget.CardView
1218+
android:layout_width="match_parent"
1219+
android:layout_height="wrap_content"
1220+
android:layout_margin="8dp"
1221+
app:cardCornerRadius="8dp"
1222+
app:cardElevation="2dp"
1223+
app:strokeColor="?android:attr/textColorSecondary"
1224+
app:strokeWidth="1dp">
1225+
1226+
<LinearLayout
1227+
android:layout_width="match_parent"
1228+
android:layout_height="wrap_content"
1229+
android:orientation="vertical"
1230+
android:padding="16dp">
1231+
1232+
<TextView
1233+
android:id="@+id/tvDisplayAnalysisTitle"
1234+
android:layout_width="match_parent"
1235+
android:layout_height="wrap_content"
1236+
android:text="@string/display_analysis_per_second_title"
1237+
android:textColor="?android:attr/textColorPrimary"
1238+
android:textStyle="bold"
1239+
android:textSize="16sp"
1240+
android:layout_marginBottom="12dp" />
1241+
1242+
<TextView
1243+
android:layout_width="match_parent"
1244+
android:layout_height="wrap_content"
1245+
android:text="@string/display_analysis_per_second_description"
1246+
android:textSize="14sp"
1247+
android:layout_marginBottom="16dp" />
1248+
1249+
<CheckBox
1250+
android:id="@+id/cbDisplayAnalysisPerSecond"
1251+
android:layout_width="match_parent"
1252+
android:layout_height="wrap_content"
1253+
android:buttonTint="?attr/colorPrimary"
1254+
android:text="@string/display_analysis_per_second_checkbox" />
1255+
1256+
</LinearLayout>
1257+
1258+
</androidx.cardview.widget.CardView>
1259+
12171260
</LinearLayout>
12181261

12191262
</LinearLayout>

0 commit comments

Comments
 (0)