This commit is contained in:
matt committed 2026-03-02 23:48:20 -08:00
1 parent e9dcb2b394
commit e39e393b8b
11 files changed
+504 -1

No files matched your search

+7
View File
@@ -34,6 +34,13 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.journeyapps:zxing-android-embedded:4.3.0'
implementation 'com.google.zxing:core:3.5.3'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'org.tensorflow:tensorflow-lite:2.14.0'
implementation 'org.tensorflow:tensorflow-lite-gpu:2.14.0'
implementation 'org.tensorflow:tensorflow-lite-support:0.4.7'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
@@ -1,10 +1,17 @@
package com.example.upcreader;
import android.Manifest;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -12,6 +19,7 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.bumptech.glide.Glide;
import com.google.zxing.ResultPoint;
import com.journeyapps.barcodescanner.BarcodeCallback;
import com.journeyapps.barcodescanner.BarcodeResult;
@@ -24,10 +32,22 @@ public class CameraActivity extends AppCompatActivity implements BarcodeCallback
private DecoratedBarcodeView barcodeView;
private Button scanButton;
private Button copyShareButton;
private TextView resultTextView;
private boolean isScanning = false;
private List<String> scannedCodes = new ArrayList<>();
// For product image display
private View productImageView;
private ImageView imageViewProduct;
private TextView textViewProductName;
// For object recognition
private ObjectRecognizer objectRecognizer;
private View recognitionResultView;
private ImageView imageViewRecognitionResult;
private TextView textViewRecognitionResult;
private static final int PERMISSION_REQUEST_CAMERA = 1;
@Override
@@ -37,8 +57,12 @@ public class CameraActivity extends AppCompatActivity implements BarcodeCallback
barcodeView = findViewById(R.id.barcode_scanner_view);
scanButton = findViewById(R.id.button_scan);
copyShareButton = findViewById(R.id.button_copy_share);
resultTextView = findViewById(R.id.text_view_results);
// Initialize object recognizer
objectRecognizer = new ObjectRecognizer(this);
// Check for camera permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA);
@@ -56,6 +80,78 @@ public class CameraActivity extends AppCompatActivity implements BarcodeCallback
}
}
});
copyShareButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (scannedCodes.isEmpty()) {
Toast.makeText(CameraActivity.this, "No codes to copy or share", Toast.LENGTH_SHORT).show();
return;
}
// Build the text to copy/share
StringBuilder sb = new StringBuilder();
for (String code : scannedCodes) {
sb.append(code).append("\n");
}
String textToCopy = sb.toString().trim();
// Copy to clipboard
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("UPC Codes", textToCopy);
clipboard.setPrimaryClip(clip);
// Show toast notification
Toast.makeText(CameraActivity.this, "Copied to clipboard", Toast.LENGTH_SHORT).show();
// Share via intent
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, textToCopy);
startActivity(Intent.createChooser(shareIntent, "Share UPC Codes"));
}
});
// Add touch listener to barcode view for object recognition
barcodeView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
// Get the touch coordinates
float x = event.getX();
float y = event.getY();
// Get the bitmap from the barcode view
Bitmap bitmap = barcodeView.getBitmap();
if (bitmap != null) {
// Calculate the crop region around the touch point
int cropSize = 224; // Size expected by the model
int centerX = (int) x;
int centerY = (int) y;
// Ensure the crop region stays within bounds
int left = Math.max(0, centerX - cropSize / 2);
int top = Math.max(0, centerY - cropSize / 2);
int right = Math.min(bitmap.getWidth(), left + cropSize);
int bottom = Math.min(bitmap.getHeight(), top + cropSize);
// Create a cropped bitmap
Bitmap croppedBitmap = Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top);
// Recognize the object
String recognitionResult = objectRecognizer.recognize(croppedBitmap);
// Display the result
showRecognitionResult(croppedBitmap, recognitionResult);
// Clean up
croppedBitmap.recycle();
}
return true;
}
return false;
}
});
}
private void setupScanner() {
@@ -96,6 +192,53 @@ public class CameraActivity extends AppCompatActivity implements BarcodeCallback
runOnUiThread(() -> {
resultTextView.setText("Found: " + code + "\nTotal: " + scannedCodes.size());
});
// Look up product information via API
UpcApiManager.getInstance().getProductByUpc(code, new UpcApiManager.UpcApiCallback() {
@Override
public void onSuccess(UpcProduct product) {
runOnUiThread(() -> {
// Create and show product image view
if (productImageView == null) {
productImageView = getLayoutInflater().inflate(R.layout.layout_product_image, null);
imageViewProduct = productImageView.findViewById(R.id.image_view_product);
textViewProductName = productImageView.findViewById(R.id.text_view_product_name);
// Add to activity layout
((ViewGroup) findViewById(android.R.id.content)).addView(productImageView);
}
// Display product image and name
if (product.getImageUrl() != null && !product.getImageUrl().isEmpty()) {
Glide.with(CameraActivity.this)
.load(product.getImageUrl())
.placeholder(R.drawable.ic_product_placeholder)
.error(R.drawable.ic_product_error)
.into(imageViewProduct);
}
if (product.getName() != null && !product.getName().isEmpty()) {
textViewProductName.setText(product.getName());
} else {
textViewProductName.setText("Product Information");
}
// Hide product image after 3 seconds
productImageView.postDelayed(() -> {
if (productImageView != null && productImageView.getParent() != null) {
((ViewGroup) productImageView.getParent()).removeView(productImageView);
}
}, 3000);
});
}
@Override
public void onError(String error) {
runOnUiThread(() -> {
Toast.makeText(CameraActivity.this, "Could not lookup product: " + error, Toast.LENGTH_SHORT).show();
});
}
});
}
}
}
@@ -131,4 +274,27 @@ public class CameraActivity extends AppCompatActivity implements BarcodeCallback
super.onPause();
barcodeView.pause();
}
private void showRecognitionResult(Bitmap bitmap, String result) {
// Create and show recognition result view
if (recognitionResultView == null) {
recognitionResultView = getLayoutInflater().inflate(R.layout.layout_object_recognition_result, null);
imageViewRecognitionResult = recognitionResultView.findViewById(R.id.image_view_recognition_result);
textViewRecognitionResult = recognitionResultView.findViewById(R.id.text_view_recognition_result);
// Add to activity layout
((ViewGroup) findViewById(android.R.id.content)).addView(recognitionResultView);
}
// Display the cropped bitmap and recognition result
imageViewRecognitionResult.setImageBitmap(bitmap);
textViewRecognitionResult.setText(result);
// Hide result after 3 seconds
recognitionResultView.postDelayed(() -> {
if (recognitionResultView != null && recognitionResultView.getParent() != null) {
((ViewGroup) recognitionResultView.getParent()).removeView(recognitionResultView);
}
}, 3000);
}
}
@@ -0,0 +1,103 @@
package com.example.upcreader;
import android.content.Context;
import android.graphics.Bitmap;
import android.util.Log;
import org.tensorflow.lite.Interpreter;
import org.tensorflow.lite.support.image.ImageProcessor;
import org.tensorflow.lite.support.image.TensorImage;
import org.tensorflow.lite.support.image.ops.ResizeOp;
import org.tensorflow.lite.support.image.ops.ResizeOp.ResizeMethod;
import org.tensorflow.lite.support.tensorbuffer.TensorBuffer;
import org.tensorflow.lite.support.common.FileUtil;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.util.ArrayList;
import java.util.List;
public class ObjectRecognizer {
private static final String MODEL_PATH = "detect.tflite";
private static final String LABELS_PATH = "labelmap.txt";
private static final int INPUT_SIZE = 224;
private static final int NUM_CLASSES = 1000; // Default for MobileNet
private Interpreter interpreter;
private List<String> labels;
public ObjectRecognizer(Context context) {
try {
// Load the model file
MappedByteBuffer model = FileUtil.loadMappedFile(context, MODEL_PATH);
interpreter = new Interpreter(model);
// Load the labels
labels = FileUtil.loadLabels(context, LABELS_PATH);
// Validate model
if (labels.size() != NUM_CLASSES) {
Log.w("ObjectRecognizer", "Label count mismatch: " + labels.size() + " vs " + NUM_CLASSES);
}
} catch (IOException e) {
Log.e("ObjectRecognizer", "Error loading model or labels", e);
}
}
public String recognize(Bitmap bitmap) {
if (interpreter == null || labels.isEmpty()) {
return "Model not loaded";
}
try {
// Preprocess the image
TensorImage inputImage = preprocessImage(bitmap);
// Create output buffer
float[][] output = new float[1][NUM_CLASSES];
// Run inference
interpreter.run(inputImage.getBuffer(), output);
// Get the best prediction
int bestIndex = 0;
float bestConfidence = 0;
for (int i = 0; i < NUM_CLASSES; i++) {
if (output[0][i] > bestConfidence) {
bestConfidence = output[0][i];
bestIndex = i;
}
}
// Return the label with highest confidence
if (bestConfidence > 0.1f) { // Minimum confidence threshold
return labels.get(bestIndex);
} else {
return "Unknown object";
}
} catch (Exception e) {
Log.e("ObjectRecognizer", "Error during inference", e);
return "Error recognizing object";
}
}
private TensorImage preprocessImage(Bitmap bitmap) {
// Convert bitmap to TensorImage
TensorImage tensorImage = TensorImage.fromBitmap(bitmap);
// Resize image to model input size
ImageProcessor imageProcessor = new ImageProcessor.Builder()
.add(new ResizeOp(INPUT_SIZE, INPUT_SIZE, ResizeMethod.BILINEAR))
.build();
return imageProcessor.process(tensorImage);
}
public void close() {
if (interpreter != null) {
interpreter.close();
}
}
}
@@ -0,0 +1,52 @@
package com.example.upcreader;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import android.util.Log;
public class UpcApiManager {
private static final String BASE_URL = "https://api.upcitemdb.com/";
private static UpcApiManager instance;
private UpcApiService apiService;
private UpcApiManager() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
apiService = retrofit.create(UpcApiService.class);
}
public static synchronized UpcApiManager getInstance() {
if (instance == null) {
instance = new UpcApiManager();
}
return instance;
}
public void getProductByUpc(String upc, UpcApiCallback callback) {
Call<UpcProduct> call = apiService.getProductByUpc(upc);
call.enqueue(new retrofit2.Callback<UpcProduct>() {
@Override
public void onResponse(Call<UpcProduct> call, retrofit2.Response<UpcProduct> response) {
if (response.isSuccessful() && response.body() != null) {
callback.onSuccess(response.body());
} else {
callback.onError("Failed to get product information");
}
}
@Override
public void onFailure(Call<UpcProduct> call, Throwable t) {
Log.e("UpcApiManager", "API call failed", t);
callback.onError("Network error: " + t.getMessage());
}
});
}
public interface UpcApiCallback {
void onSuccess(UpcProduct product);
void onError(String error);
}
}
@@ -0,0 +1,10 @@
package com.example.upcreader;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Path;
public interface UpcApiService {
@GET("/api/product/{upc}")
Call<UpcProduct> getProductByUpc(@Path("upc") String upc);
}
@@ -0,0 +1,76 @@
package com.example.upcreader;
import com.google.gson.annotations.SerializedName;
public class UpcProduct {
@SerializedName("upc")
private String upc;
@SerializedName("name")
private String name;
@SerializedName("description")
private String description;
@SerializedName("image_url")
private String imageUrl;
@SerializedName("brand")
private String brand;
@SerializedName("category")
private String category;
// Default constructor required for Gson
public UpcProduct() {}
// Getters
public String getUpc() {
return upc;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public String getImageUrl() {
return imageUrl;
}
public String getBrand() {
return brand;
}
public String getCategory() {
return category;
}
// Setters
public void setUpc(String upc) {
this.upc = upc;
}
public void setName(String name) {
this.name = name;
}
public void setDescription(String description) {
this.description = description;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public void setBrand(String brand) {
this.brand = brand;
}
public void setCategory(String category) {
this.category = category;
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#333333" />
</shape>
</item>
<item android:gravity="center">
<vector android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FF0000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5 5,-2.24 5,-5 -2.24,-5 -5,-5z"/>
<path android:fillColor="#FF0000" android:pathData="M15.5,15.5L8.5,8.5M8.5,15.5L15.5,8.5" android:strokeWidth="2" android:strokeColor="#FF0000" android:strokeLineCap="round"/>
</vector>
</item>
</layer-list>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="#333333" />
</shape>
</item>
<item android:gravity="center">
<vector android:width="48dp" android:height="48dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5 5,-2.24 5,-5 -2.24,-5 -5,-5z"/>
</vector>
</item>
</layer-list>
@@ -31,4 +31,13 @@
android:padding="16dp"
android:background="#333333" />
<Button
android:id="@+id/button_copy_share"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Copy/Share Scanned Codes"
android:textSize="16sp"
android:layout_margin="16dp"
android:backgroundTint="#2196F3" />
</LinearLayout>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#000000"
android:gravity="center"
android:padding="16dp">
<ImageView
android:id="@+id/image_view_recognition_result"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginBottom="16dp"
android:scaleType="centerInside"
android:background="#333333" />
<TextView
android:id="@+id/text_view_recognition_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textColor="#FFFFFF"
android:textAlignment="center"
android:padding="16dp" />
</LinearLayout>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#000000"
android:gravity="center">
<ImageView
android:id="@+id/image_view_product"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_marginBottom="16dp"
android:scaleType="centerInside"
android:background="#333333" />
<TextView
android:id="@+id/text_view_product_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textColor="#FFFFFF"
android:textAlignment="center"
android:padding="16dp" />
</LinearLayout>