From e39e393b8b276780962803a0475a89486fbe70a5 Mon Sep 17 00:00:00 2001 From: matt_ed Date: Mon, 2 Mar 2026 23:48:20 -0800 Subject: [PATCH] ver 2.0 --- app/build.gradle | 7 + .../com/example/upcreader/CameraActivity.java | 168 +++++++++++++++++- .../example/upcreader/ObjectRecognizer.java | 103 +++++++++++ .../com/example/upcreader/UpcApiManager.java | 52 ++++++ .../com/example/upcreader/UpcApiService.java | 10 ++ .../com/example/upcreader/UpcProduct.java | 76 ++++++++ .../main/res/drawable/ic_product_error.xml | 14 ++ .../res/drawable/ic_product_placeholder.xml | 13 ++ app/src/main/res/layout/activity_camera.xml | 9 + .../layout_object_recognition_result.xml | 27 +++ .../main/res/layout/layout_product_image.xml | 26 +++ 11 files changed, 504 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/example/upcreader/ObjectRecognizer.java create mode 100644 app/src/main/java/com/example/upcreader/UpcApiManager.java create mode 100644 app/src/main/java/com/example/upcreader/UpcApiService.java create mode 100644 app/src/main/java/com/example/upcreader/UpcProduct.java create mode 100644 app/src/main/res/drawable/ic_product_error.xml create mode 100644 app/src/main/res/drawable/ic_product_placeholder.xml create mode 100644 app/src/main/res/layout/layout_object_recognition_result.xml create mode 100644 app/src/main/res/layout/layout_product_image.xml diff --git a/app/build.gradle b/app/build.gradle index 8b4167f..f137795 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/com/example/upcreader/CameraActivity.java b/app/src/main/java/com/example/upcreader/CameraActivity.java index 8ab2de5..138382a 100644 --- a/app/src/main/java/com/example/upcreader/CameraActivity.java +++ b/app/src/main/java/com/example/upcreader/CameraActivity.java @@ -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 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); + } } diff --git a/app/src/main/java/com/example/upcreader/ObjectRecognizer.java b/app/src/main/java/com/example/upcreader/ObjectRecognizer.java new file mode 100644 index 0000000..aef791b --- /dev/null +++ b/app/src/main/java/com/example/upcreader/ObjectRecognizer.java @@ -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 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(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/upcreader/UpcApiManager.java b/app/src/main/java/com/example/upcreader/UpcApiManager.java new file mode 100644 index 0000000..09f23cb --- /dev/null +++ b/app/src/main/java/com/example/upcreader/UpcApiManager.java @@ -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 call = apiService.getProductByUpc(upc); + call.enqueue(new retrofit2.Callback() { + @Override + public void onResponse(Call call, retrofit2.Response response) { + if (response.isSuccessful() && response.body() != null) { + callback.onSuccess(response.body()); + } else { + callback.onError("Failed to get product information"); + } + } + + @Override + public void onFailure(Call 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/upcreader/UpcApiService.java b/app/src/main/java/com/example/upcreader/UpcApiService.java new file mode 100644 index 0000000..046f6c9 --- /dev/null +++ b/app/src/main/java/com/example/upcreader/UpcApiService.java @@ -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 getProductByUpc(@Path("upc") String upc); +} \ No newline at end of file diff --git a/app/src/main/java/com/example/upcreader/UpcProduct.java b/app/src/main/java/com/example/upcreader/UpcProduct.java new file mode 100644 index 0000000..06a652a --- /dev/null +++ b/app/src/main/java/com/example/upcreader/UpcProduct.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_product_error.xml b/app/src/main/res/drawable/ic_product_error.xml new file mode 100644 index 0000000..5dbca48 --- /dev/null +++ b/app/src/main/res/drawable/ic_product_error.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_product_placeholder.xml b/app/src/main/res/drawable/ic_product_placeholder.xml new file mode 100644 index 0000000..aa89000 --- /dev/null +++ b/app/src/main/res/drawable/ic_product_placeholder.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml index 28e9862..d0d2bc6 100644 --- a/app/src/main/res/layout/activity_camera.xml +++ b/app/src/main/res/layout/activity_camera.xml @@ -31,4 +31,13 @@ android:padding="16dp" android:background="#333333" /> +