xref: /aosp_15_r20/external/tensorflow/tensorflow/tools/android/test/src/org/tensorflow/demo/StylizeActivity.java (revision b6fb3261f9314811a0f4371741dbb8839866f948)
1 /*
2  * Copyright 2017 The TensorFlow Authors. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *       http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package org.tensorflow.demo;
18 
19 import android.app.UiModeManager;
20 import android.content.Context;
21 import android.content.res.AssetManager;
22 import android.content.res.Configuration;
23 import android.graphics.Bitmap;
24 import android.graphics.Bitmap.Config;
25 import android.graphics.BitmapFactory;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.Matrix;
29 import android.graphics.Paint;
30 import android.graphics.Paint.Style;
31 import android.graphics.Rect;
32 import android.graphics.Typeface;
33 import android.media.ImageReader.OnImageAvailableListener;
34 import android.os.Bundle;
35 import android.os.SystemClock;
36 import android.util.DisplayMetrics;
37 import android.util.Size;
38 import android.util.TypedValue;
39 import android.view.Display;
40 import android.view.KeyEvent;
41 import android.view.MotionEvent;
42 import android.view.View;
43 import android.view.View.OnClickListener;
44 import android.view.View.OnTouchListener;
45 import android.view.ViewGroup;
46 import android.widget.BaseAdapter;
47 import android.widget.Button;
48 import android.widget.GridView;
49 import android.widget.ImageView;
50 import android.widget.RelativeLayout;
51 import android.widget.Toast;
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.Vector;
57 import org.tensorflow.contrib.android.TensorFlowInferenceInterface;
58 import org.tensorflow.demo.OverlayView.DrawCallback;
59 import org.tensorflow.demo.env.BorderedText;
60 import org.tensorflow.demo.env.ImageUtils;
61 import org.tensorflow.demo.env.Logger;
62 import org.tensorflow.demo.R; // Explicit import needed for internal Google builds.
63 
64 /**
65  * Sample activity that stylizes the camera preview according to "A Learned Representation For
66  * Artistic Style" (https://arxiv.org/abs/1610.07629)
67  */
68 public class StylizeActivity extends CameraActivity implements OnImageAvailableListener {
69   private static final Logger LOGGER = new Logger();
70 
71   private static final String MODEL_FILE = "file:///android_asset/stylize_quantized.pb";
72   private static final String INPUT_NODE = "input";
73   private static final String STYLE_NODE = "style_num";
74   private static final String OUTPUT_NODE = "transformer/expand/conv3/conv/Sigmoid";
75   private static final int NUM_STYLES = 26;
76 
77   private static final boolean SAVE_PREVIEW_BITMAP = false;
78 
79   // Whether to actively manipulate non-selected sliders so that sum of activations always appears
80   // to be 1.0. The actual style input tensor will be normalized to sum to 1.0 regardless.
81   private static final boolean NORMALIZE_SLIDERS = true;
82 
83   private static final float TEXT_SIZE_DIP = 12;
84 
85   private static final boolean DEBUG_MODEL = false;
86 
87   private static final int[] SIZES = {128, 192, 256, 384, 512, 720};
88 
89   private static final Size DESIRED_PREVIEW_SIZE = new Size(1280, 720);
90 
91   // Start at a medium size, but let the user step up through smaller sizes so they don't get
92   // immediately stuck processing a large image.
93   private int desiredSizeIndex = -1;
94   private int desiredSize = 256;
95   private int initializedSize = 0;
96 
97   private Integer sensorOrientation;
98 
99   private long lastProcessingTimeMs;
100   private Bitmap rgbFrameBitmap = null;
101   private Bitmap croppedBitmap = null;
102   private Bitmap cropCopyBitmap = null;
103 
104   private final float[] styleVals = new float[NUM_STYLES];
105   private int[] intValues;
106   private float[] floatValues;
107 
108   private int frameNum = 0;
109 
110   private Bitmap textureCopyBitmap;
111 
112   private Matrix frameToCropTransform;
113   private Matrix cropToFrameTransform;
114 
115   private BorderedText borderedText;
116 
117   private TensorFlowInferenceInterface inferenceInterface;
118 
119   private int lastOtherStyle = 1;
120 
121   private boolean allZero = false;
122 
123   private ImageGridAdapter adapter;
124   private GridView grid;
125 
126   private final OnTouchListener gridTouchAdapter =
127       new OnTouchListener() {
128         ImageSlider slider = null;
129 
130         @Override
131         public boolean onTouch(final View v, final MotionEvent event) {
132           switch (event.getActionMasked()) {
133             case MotionEvent.ACTION_DOWN:
134               for (int i = 0; i < NUM_STYLES; ++i) {
135                 final ImageSlider child = adapter.items[i];
136                 final Rect rect = new Rect();
137                 child.getHitRect(rect);
138                 if (rect.contains((int) event.getX(), (int) event.getY())) {
139                   slider = child;
140                   slider.setHilighted(true);
141                 }
142               }
143               break;
144 
145             case MotionEvent.ACTION_MOVE:
146               if (slider != null) {
147                 final Rect rect = new Rect();
148                 slider.getHitRect(rect);
149 
150                 final float newSliderVal =
151                     (float)
152                         Math.min(
153                             1.0,
154                             Math.max(
155                                 0.0, 1.0 - (event.getY() - slider.getTop()) / slider.getHeight()));
156 
157                 setStyle(slider, newSliderVal);
158               }
159               break;
160 
161             case MotionEvent.ACTION_UP:
162               if (slider != null) {
163                 slider.setHilighted(false);
164                 slider = null;
165               }
166               break;
167 
168             default: // fall out
169 
170           }
171           return true;
172         }
173       };
174 
175   @Override
onCreate(final Bundle savedInstanceState)176   public void onCreate(final Bundle savedInstanceState) {
177     super.onCreate(savedInstanceState);
178   }
179 
180   @Override
getLayoutId()181   protected int getLayoutId() {
182     return R.layout.camera_connection_fragment_stylize;
183   }
184 
185   @Override
getDesiredPreviewFrameSize()186   protected Size getDesiredPreviewFrameSize() {
187     return DESIRED_PREVIEW_SIZE;
188   }
189 
getBitmapFromAsset(final Context context, final String filePath)190   public static Bitmap getBitmapFromAsset(final Context context, final String filePath) {
191     final AssetManager assetManager = context.getAssets();
192 
193     Bitmap bitmap = null;
194     try {
195       final InputStream inputStream = assetManager.open(filePath);
196       bitmap = BitmapFactory.decodeStream(inputStream);
197     } catch (final IOException e) {
198       LOGGER.e("Error opening bitmap!", e);
199     }
200 
201     return bitmap;
202   }
203 
204   private class ImageSlider extends ImageView {
205     private float value = 0.0f;
206     private boolean hilighted = false;
207 
208     private final Paint boxPaint;
209     private final Paint linePaint;
210 
ImageSlider(final Context context)211     public ImageSlider(final Context context) {
212       super(context);
213       value = 0.0f;
214 
215       boxPaint = new Paint();
216       boxPaint.setColor(Color.BLACK);
217       boxPaint.setAlpha(128);
218 
219       linePaint = new Paint();
220       linePaint.setColor(Color.WHITE);
221       linePaint.setStrokeWidth(10.0f);
222       linePaint.setStyle(Style.STROKE);
223     }
224 
225     @Override
onDraw(final Canvas canvas)226     public void onDraw(final Canvas canvas) {
227       super.onDraw(canvas);
228       final float y = (1.0f - value) * canvas.getHeight();
229 
230       // If all sliders are zero, don't bother shading anything.
231       if (!allZero) {
232         canvas.drawRect(0, 0, canvas.getWidth(), y, boxPaint);
233       }
234 
235       if (value > 0.0f) {
236         canvas.drawLine(0, y, canvas.getWidth(), y, linePaint);
237       }
238 
239       if (hilighted) {
240         canvas.drawRect(0, 0, getWidth(), getHeight(), linePaint);
241       }
242     }
243 
244     @Override
onMeasure(final int widthMeasureSpec, final int heightMeasureSpec)245     protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
246       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
247       setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
248     }
249 
setValue(final float value)250     public void setValue(final float value) {
251       this.value = value;
252       postInvalidate();
253     }
254 
setHilighted(final boolean highlighted)255     public void setHilighted(final boolean highlighted) {
256       this.hilighted = highlighted;
257       this.postInvalidate();
258     }
259   }
260 
261   private class ImageGridAdapter extends BaseAdapter {
262     final ImageSlider[] items = new ImageSlider[NUM_STYLES];
263     final ArrayList<Button> buttons = new ArrayList<>();
264 
265     {
266       final Button sizeButton =
267           new Button(StylizeActivity.this) {
268             @Override
269             protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
270               super.onMeasure(widthMeasureSpec, heightMeasureSpec);
271               setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
272             }
273           };
274       sizeButton.setText("" + desiredSize);
sizeButton.setOnClickListener( new OnClickListener() { @Override public void onClick(final View v) { desiredSizeIndex = (desiredSizeIndex + 1) % SIZES.length; desiredSize = SIZES[desiredSizeIndex]; sizeButton.setText("" + desiredSize); sizeButton.postInvalidate(); } })275       sizeButton.setOnClickListener(
276           new OnClickListener() {
277             @Override
278             public void onClick(final View v) {
279               desiredSizeIndex = (desiredSizeIndex + 1) % SIZES.length;
280               desiredSize = SIZES[desiredSizeIndex];
281               sizeButton.setText("" + desiredSize);
282               sizeButton.postInvalidate();
283             }
284           });
285 
286       final Button saveButton =
287           new Button(StylizeActivity.this) {
288             @Override
289             protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
290               super.onMeasure(widthMeasureSpec, heightMeasureSpec);
291               setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
292             }
293           };
294       saveButton.setText("save");
295       saveButton.setTextSize(12);
296 
saveButton.setOnClickListener( new OnClickListener() { @Override public void onClick(final View v) { if (textureCopyBitmap != null) { ImageUtils.saveBitmap(textureCopyBitmap, "stylized" + frameNum + ".png"); Toast.makeText( StylizeActivity.this, "Saved image to: /sdcard/tensorflow/" + "stylized" + frameNum + ".png", Toast.LENGTH_LONG) .show(); } } })297       saveButton.setOnClickListener(
298           new OnClickListener() {
299             @Override
300             public void onClick(final View v) {
301               if (textureCopyBitmap != null) {
302                 // TODO(andrewharp): Save as jpeg with guaranteed unique filename.
303                 ImageUtils.saveBitmap(textureCopyBitmap, "stylized" + frameNum + ".png");
304                 Toast.makeText(
305                         StylizeActivity.this,
306                         "Saved image to: /sdcard/tensorflow/" + "stylized" + frameNum + ".png",
307                         Toast.LENGTH_LONG)
308                     .show();
309               }
310             }
311           });
312 
313       buttons.add(sizeButton);
314       buttons.add(saveButton);
315 
316       for (int i = 0; i < NUM_STYLES; ++i) {
317         LOGGER.v("Creating item %d", i);
318 
319         if (items[i] == null) {
320           final ImageSlider slider = new ImageSlider(StylizeActivity.this);
321           final Bitmap bm =
322               getBitmapFromAsset(StylizeActivity.this, "thumbnails/style" + i + ".jpg");
323           slider.setImageBitmap(bm);
324 
325           items[i] = slider;
326         }
327       }
328     }
329 
330     @Override
getCount()331     public int getCount() {
332       return buttons.size() + NUM_STYLES;
333     }
334 
335     @Override
getItem(final int position)336     public Object getItem(final int position) {
337       if (position < buttons.size()) {
338         return buttons.get(position);
339       } else {
340         return items[position - buttons.size()];
341       }
342     }
343 
344     @Override
getItemId(final int position)345     public long getItemId(final int position) {
346       return getItem(position).hashCode();
347     }
348 
349     @Override
getView(final int position, final View convertView, final ViewGroup parent)350     public View getView(final int position, final View convertView, final ViewGroup parent) {
351       if (convertView != null) {
352         return convertView;
353       }
354       return (View) getItem(position);
355     }
356   }
357 
358   @Override
onPreviewSizeChosen(final Size size, final int rotation)359   public void onPreviewSizeChosen(final Size size, final int rotation) {
360     final float textSizePx = TypedValue.applyDimension(
361         TypedValue.COMPLEX_UNIT_DIP, TEXT_SIZE_DIP, getResources().getDisplayMetrics());
362     borderedText = new BorderedText(textSizePx);
363     borderedText.setTypeface(Typeface.MONOSPACE);
364 
365     inferenceInterface = new TensorFlowInferenceInterface(getAssets(), MODEL_FILE);
366 
367     previewWidth = size.getWidth();
368     previewHeight = size.getHeight();
369 
370     final Display display = getWindowManager().getDefaultDisplay();
371     final int screenOrientation = display.getRotation();
372 
373     LOGGER.i("Sensor orientation: %d, Screen orientation: %d", rotation, screenOrientation);
374 
375     sensorOrientation = rotation + screenOrientation;
376 
377     addCallback(
378         new DrawCallback() {
379           @Override
380           public void drawCallback(final Canvas canvas) {
381             renderDebug(canvas);
382           }
383         });
384 
385     adapter = new ImageGridAdapter();
386     grid = (GridView) findViewById(R.id.grid_layout);
387     grid.setAdapter(adapter);
388     grid.setOnTouchListener(gridTouchAdapter);
389 
390     // Change UI on Android TV
391     UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
392     if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
393       DisplayMetrics displayMetrics = new DisplayMetrics();
394       getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
395       int styleSelectorHeight = displayMetrics.heightPixels;
396       int styleSelectorWidth = displayMetrics.widthPixels - styleSelectorHeight;
397       RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(styleSelectorWidth, ViewGroup.LayoutParams.MATCH_PARENT);
398 
399       // Calculate number of style in a row, so all the style can show up without scrolling
400       int numOfStylePerRow = 3;
401       while (styleSelectorWidth / numOfStylePerRow * Math.ceil((float) (adapter.getCount() - 2) / numOfStylePerRow) > styleSelectorHeight) {
402         numOfStylePerRow++;
403       }
404       grid.setNumColumns(numOfStylePerRow);
405       layoutParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
406       grid.setLayoutParams(layoutParams);
407       adapter.buttons.clear();
408     }
409 
410     setStyle(adapter.items[0], 1.0f);
411   }
412 
setStyle(final ImageSlider slider, final float value)413   private void setStyle(final ImageSlider slider, final float value) {
414     slider.setValue(value);
415 
416     if (NORMALIZE_SLIDERS) {
417       // Slider vals correspond directly to the input tensor vals, and normalization is visually
418       // maintained by remanipulating non-selected sliders.
419       float otherSum = 0.0f;
420 
421       for (int i = 0; i < NUM_STYLES; ++i) {
422         if (adapter.items[i] != slider) {
423           otherSum += adapter.items[i].value;
424         }
425       }
426 
427       if (otherSum > 0.0) {
428         float highestOtherVal = 0;
429         final float factor = otherSum > 0.0f ? (1.0f - value) / otherSum : 0.0f;
430         for (int i = 0; i < NUM_STYLES; ++i) {
431           final ImageSlider child = adapter.items[i];
432           if (child == slider) {
433             continue;
434           }
435           final float newVal = child.value * factor;
436           child.setValue(newVal > 0.01f ? newVal : 0.0f);
437 
438           if (child.value > highestOtherVal) {
439             lastOtherStyle = i;
440             highestOtherVal = child.value;
441           }
442         }
443       } else {
444         // Everything else is 0, so just pick a suitable slider to push up when the
445         // selected one goes down.
446         if (adapter.items[lastOtherStyle] == slider) {
447           lastOtherStyle = (lastOtherStyle + 1) % NUM_STYLES;
448         }
449         adapter.items[lastOtherStyle].setValue(1.0f - value);
450       }
451     }
452 
453     final boolean lastAllZero = allZero;
454     float sum = 0.0f;
455     for (int i = 0; i < NUM_STYLES; ++i) {
456       sum += adapter.items[i].value;
457     }
458     allZero = sum == 0.0f;
459 
460     // Now update the values used for the input tensor. If nothing is set, mix in everything
461     // equally. Otherwise everything is normalized to sum to 1.0.
462     for (int i = 0; i < NUM_STYLES; ++i) {
463       styleVals[i] = allZero ? 1.0f / NUM_STYLES : adapter.items[i].value / sum;
464 
465       if (lastAllZero != allZero) {
466         adapter.items[i].postInvalidate();
467       }
468     }
469   }
470 
resetPreviewBuffers()471   private void resetPreviewBuffers() {
472     croppedBitmap = Bitmap.createBitmap(desiredSize, desiredSize, Config.ARGB_8888);
473 
474     frameToCropTransform = ImageUtils.getTransformationMatrix(
475         previewWidth, previewHeight,
476         desiredSize, desiredSize,
477         sensorOrientation, true);
478 
479     cropToFrameTransform = new Matrix();
480     frameToCropTransform.invert(cropToFrameTransform);
481     intValues = new int[desiredSize * desiredSize];
482     floatValues = new float[desiredSize * desiredSize * 3];
483     initializedSize = desiredSize;
484   }
485 
486   @Override
processImage()487   protected void processImage() {
488     if (desiredSize != initializedSize) {
489       LOGGER.i(
490           "Initializing at size preview size %dx%d, stylize size %d",
491           previewWidth, previewHeight, desiredSize);
492 
493       rgbFrameBitmap = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);
494       croppedBitmap = Bitmap.createBitmap(desiredSize, desiredSize, Config.ARGB_8888);
495       frameToCropTransform = ImageUtils.getTransformationMatrix(
496           previewWidth, previewHeight,
497           desiredSize, desiredSize,
498           sensorOrientation, true);
499 
500       cropToFrameTransform = new Matrix();
501       frameToCropTransform.invert(cropToFrameTransform);
502       intValues = new int[desiredSize * desiredSize];
503       floatValues = new float[desiredSize * desiredSize * 3];
504       initializedSize = desiredSize;
505     }
506     rgbFrameBitmap.setPixels(getRgbBytes(), 0, previewWidth, 0, 0, previewWidth, previewHeight);
507     final Canvas canvas = new Canvas(croppedBitmap);
508     canvas.drawBitmap(rgbFrameBitmap, frameToCropTransform, null);
509 
510     // For examining the actual TF input.
511     if (SAVE_PREVIEW_BITMAP) {
512       ImageUtils.saveBitmap(croppedBitmap);
513     }
514 
515     runInBackground(
516         new Runnable() {
517           @Override
518           public void run() {
519             cropCopyBitmap = Bitmap.createBitmap(croppedBitmap);
520             final long startTime = SystemClock.uptimeMillis();
521             stylizeImage(croppedBitmap);
522             lastProcessingTimeMs = SystemClock.uptimeMillis() - startTime;
523             textureCopyBitmap = Bitmap.createBitmap(croppedBitmap);
524             requestRender();
525             readyForNextImage();
526           }
527         });
528     if (desiredSize != initializedSize) {
529       resetPreviewBuffers();
530     }
531   }
532 
stylizeImage(final Bitmap bitmap)533   private void stylizeImage(final Bitmap bitmap) {
534     ++frameNum;
535     bitmap.getPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
536 
537     if (DEBUG_MODEL) {
538       // Create a white square that steps through a black background 1 pixel per frame.
539       final int centerX = (frameNum + bitmap.getWidth() / 2) % bitmap.getWidth();
540       final int centerY = bitmap.getHeight() / 2;
541       final int squareSize = 10;
542       for (int i = 0; i < intValues.length; ++i) {
543         final int x = i % bitmap.getWidth();
544         final int y = i / bitmap.getHeight();
545         final float val =
546             Math.abs(x - centerX) < squareSize && Math.abs(y - centerY) < squareSize ? 1.0f : 0.0f;
547         floatValues[i * 3] = val;
548         floatValues[i * 3 + 1] = val;
549         floatValues[i * 3 + 2] = val;
550       }
551     } else {
552       for (int i = 0; i < intValues.length; ++i) {
553         final int val = intValues[i];
554         floatValues[i * 3] = ((val >> 16) & 0xFF) / 255.0f;
555         floatValues[i * 3 + 1] = ((val >> 8) & 0xFF) / 255.0f;
556         floatValues[i * 3 + 2] = (val & 0xFF) / 255.0f;
557       }
558     }
559 
560     // Copy the input data into TensorFlow.
561     LOGGER.i("Width: %s , Height: %s", bitmap.getWidth(), bitmap.getHeight());
562     inferenceInterface.feed(
563         INPUT_NODE, floatValues, 1, bitmap.getWidth(), bitmap.getHeight(), 3);
564     inferenceInterface.feed(STYLE_NODE, styleVals, NUM_STYLES);
565 
566     inferenceInterface.run(new String[] {OUTPUT_NODE}, isDebug());
567     inferenceInterface.fetch(OUTPUT_NODE, floatValues);
568 
569     for (int i = 0; i < intValues.length; ++i) {
570       intValues[i] =
571           0xFF000000
572               | (((int) (floatValues[i * 3] * 255)) << 16)
573               | (((int) (floatValues[i * 3 + 1] * 255)) << 8)
574               | ((int) (floatValues[i * 3 + 2] * 255));
575     }
576 
577     bitmap.setPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
578   }
579 
renderDebug(final Canvas canvas)580   private void renderDebug(final Canvas canvas) {
581     // TODO(andrewharp): move result display to its own View instead of using debug overlay.
582     final Bitmap texture = textureCopyBitmap;
583     if (texture != null) {
584       final Matrix matrix = new Matrix();
585       final float scaleFactor =
586           DEBUG_MODEL
587               ? 4.0f
588               : Math.min(
589                   (float) canvas.getWidth() / texture.getWidth(),
590                   (float) canvas.getHeight() / texture.getHeight());
591       matrix.postScale(scaleFactor, scaleFactor);
592       canvas.drawBitmap(texture, matrix, new Paint());
593     }
594 
595     if (!isDebug()) {
596       return;
597     }
598 
599     final Bitmap copy = cropCopyBitmap;
600     if (copy == null) {
601       return;
602     }
603 
604     canvas.drawColor(0x55000000);
605 
606     final Matrix matrix = new Matrix();
607     final float scaleFactor = 2;
608     matrix.postScale(scaleFactor, scaleFactor);
609     matrix.postTranslate(
610         canvas.getWidth() - copy.getWidth() * scaleFactor,
611         canvas.getHeight() - copy.getHeight() * scaleFactor);
612     canvas.drawBitmap(copy, matrix, new Paint());
613 
614     final Vector<String> lines = new Vector<>();
615 
616     final String[] statLines = inferenceInterface.getStatString().split("\n");
617     Collections.addAll(lines, statLines);
618 
619     lines.add("");
620 
621     lines.add("Frame: " + previewWidth + "x" + previewHeight);
622     lines.add("Crop: " + copy.getWidth() + "x" + copy.getHeight());
623     lines.add("View: " + canvas.getWidth() + "x" + canvas.getHeight());
624     lines.add("Rotation: " + sensorOrientation);
625     lines.add("Inference time: " + lastProcessingTimeMs + "ms");
626     lines.add("Desired size: " + desiredSize);
627     lines.add("Initialized size: " + initializedSize);
628 
629     borderedText.drawLines(canvas, 10, canvas.getHeight() - 10, lines);
630   }
631 
632   @Override
onKeyDown(int keyCode, KeyEvent event)633   public boolean onKeyDown(int keyCode, KeyEvent event) {
634     int moveOffset = 0;
635     switch (keyCode) {
636       case KeyEvent.KEYCODE_DPAD_LEFT:
637         moveOffset = -1;
638         break;
639       case KeyEvent.KEYCODE_DPAD_RIGHT:
640         moveOffset = 1;
641         break;
642       case KeyEvent.KEYCODE_DPAD_UP:
643         moveOffset = -1 * grid.getNumColumns();
644         break;
645       case KeyEvent.KEYCODE_DPAD_DOWN:
646         moveOffset = grid.getNumColumns();
647         break;
648       default:
649         return super.onKeyDown(keyCode, event);
650     }
651 
652     // get the highest selected style
653     int currentSelect = 0;
654     float highestValue = 0;
655     for (int i = 0; i < adapter.getCount(); i++) {
656       if (adapter.items[i].value > highestValue) {
657         currentSelect = i;
658         highestValue = adapter.items[i].value;
659       }
660     }
661     setStyle(adapter.items[(currentSelect + moveOffset + adapter.getCount()) % adapter.getCount()], 1);
662 
663     return true;
664   }
665 }
666