【问题标题】:Android: locating words on the screen. Google ML Kit bounding boxes are off a bitAndroid:在屏幕上定位文字。 Google ML Kit 边界框有点偏离
【发布时间】:2021-06-11 22:37:51
【问题描述】:

我正在尝试在手机屏幕上找到某个单词,然后在它们出现时在它们周围显示一个边界框。我遵循以下步骤:

  1. 捕获整个屏幕内容(使用MediaProjection API)。
  2. 将此屏幕截图传递给来自Google ML KitTextRecognizer 对象
  3. 检查检测到的单词,如果匹配则使用ML Kit返回的Rect在屏幕上绘制。

几乎有效,这是检测结果的屏幕截图,并在记事本应用上突出显示了单词 hello

如您所见,半透明的黄色框在hellos 上并不完全。

以下是相关的代码示例。将截图位图传递给ML Kit

InputImage image = InputImage.fromBitmap(screenshotBitmap, 0);
//I checked: image, screen, and overlay view dimensions are exactly the same.
TextRecognizer recognizer = TextRecognition.getClient();
recognizer.process(image)
          .addOnSuccessListener(this::processText);

获取识别词的processText方法:

 for (Text.Element element : getElements()) {
      String elementText = element.getText(); 
      Rect bounds = element.getBoundingBox(); //Getting the bounding box
      if (elementText.equalsIgnoreCase("hello")) { //hello is hardcoded for now
          addHighlightCard(bounds.left, bounds.top, bounds.width(), bounds.height());
      }
 }

最后是addHighlightCard,它创建和定位您在屏幕截图上看到的视图。它使用带有RelativeLayout 的全屏覆盖,因为这种布局允许我指定子视图的确切位置和宽度。

public void addHighlightCard(int x, int y, int width, int height) {
    View highlightCard = inflater.inflate(R.layout.highlight_card, overlayRoot, false);
    RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width, height);
    params.leftMargin = x;
    params.topMargin = y;
    highlightCard.setLayoutParams(params);
    overlayRoot.addView(highlightCard, params);
}

正如您所见,没有进行任何缩放,我捕获了整个屏幕,并使用了填充整个屏幕(甚至工具栏)的布局。然后,我认为 ML Kit 返回的坐标应该可以直接用于绘制到屏幕上。但显然我错了,图像似乎在某个地方被缩小了,但我不知道在哪里。

解决方案:事实证明,Media Projection API 虚拟显示的尺寸不正确导致边界框未对齐。我不会让这个问题变得更长,而是在此处发布一个指向 GitHub 存储库的链接,您可以在其中找到一个示例应用程序,该应用程序显示了使用 Media Projection API 并在截图。

示例应用:test-text-recognition

【问题讨论】:

    标签: android overlay text-recognition google-mlkit


    【解决方案1】:

    分析

    我发现您的代码存在 4 个潜在问题。

    屏幕坐标的使用

    当你在这里创建你的亮点卡片时:

    public void addHighlightCard(int x, int y, int width, int height) {
        ...
        params.leftMargin = x;
        params.topMargin = y;
        ...
    } 
    

    您分配绝对坐标(屏幕坐标)xy,而不是相对于您的RelativeLayout 的坐标,这是错误的,因为RelativeLayout 在设备屏幕方面也有一些偏移。

    要分配正确的坐标,请先计算RelativeLayout 的屏幕坐标,然后根据这些坐标调整xy。例如:

    public void addHighlightCard(int x, int y, int width, int height) {
        ...
        int[] screenCoordinates = new int[2];
        overlayRoot.getLocationOnScreen(screenCoordinates);
        int xOffset = screenCoordinates[0];
        int yOffset = screenCoordinates[1];        
    
        params.leftMargin = x - xOffset;
        params.topMargin = y - yOffset;
        ...
    } 
    

    但是,如果您的根 View 占据了整个屏幕,那应该不是问题。

    RelativeLayout 的使用

    我相信这可能是个问题,因为如果您想在另一个FrameLayout 之上添加一个新的View,则应改为使用。但是,我无法确定这是否有问题,因为我没有看到完整的代码。

    使用 MediaProjection 进行屏幕捕捉

    您还没有向我们展示您是如何使用 MediaProjection 做到这一点的,因此这也可能是一个问题。我使用了另一种方式来捕获您可以在下面看到的屏幕。

    突出显示文本

    您正在从LayoutInflater 膨胀View 以突出显示找到的文本。对于测试,我通过组合 ShapeDrawableView 来做一些不同的事情,比如:

    ...
                                ShapeDrawable drawable = new ShapeDrawable();
                                drawable.getPaint().setColor(Color.YELLOW);
                                drawable.getPaint().setStyle(Paint.Style.STROKE);
                                drawable.getPaint().setStrokeWidth(5f);
                                View shapeView = new View(decorView.getContext());
                                shapeView.setBackground(drawable);
    ...
    

    下面会提供完整的代码。

    解决方案

    由于您提到您的RelativeLayout 占据了整个屏幕,因此我决定创建一个示例项目来证明与您的项目类似的项目运行良好。

    下面是解释和相关代码。

    build.gradle

    plugins {
        id 'com.android.application'
    }
    
    android {
        compileSdkVersion 30
        buildToolsVersion "30.0.2"
    
        defaultConfig {
            applicationId "com.example.myapplication"
            minSdkVersion 24
            targetSdkVersion 30
            versionCode 1
            versionName "1.0"
    
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        }
    
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
    }
    
    dependencies {
    
        implementation 'androidx.appcompat:appcompat:1.2.0'
        implementation 'com.google.android.material:material:1.3.0'
        implementation 'com.google.android.gms:play-services-mlkit-text-recognition:16.1.3'
        testImplementation 'junit:junit:4.+'
        androidTestImplementation 'androidx.test.ext:junit:1.1.2'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    }
    

    MainActivity.java

    这里,为了截屏,我使用以下代码:

                    Bitmap bitmap = Bitmap.createBitmap(decorView.getWidth(),
                            decorView.getHeight(), Bitmap.Config.ARGB_8888);
                    Canvas canvas = new Canvas(bitmap);
                    decorView.draw(canvas);
                    InputImage image = InputImage.fromBitmap(bitmap, 0);
    

    我在OnGlobalLayoutListener 中这样做是为了确保装饰视图具有适当的宽度和高度。好的,该类的完整代码如下:

    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            setContentView(R.layout.activity_main);
    
            RecyclerView recyclerView = findViewById(R.id.recycler_view);
            recyclerView.setAdapter(new RecyclerViewAdapter(this));
            recyclerView.setLayoutManager(new LinearLayoutManager(this));
    
            View decorView = getWindow().getDecorView();
            decorView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    decorView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
    
                    // take a screenshot of your screen
                    Bitmap bitmap = Bitmap.createBitmap(decorView.getWidth(),
                            decorView.getHeight(), Bitmap.Config.ARGB_8888);
                    Canvas canvas = new Canvas(bitmap);
                    decorView.draw(canvas);
                    InputImage image = InputImage.fromBitmap(bitmap, 0);
    
                    TextRecognizer recognizer = TextRecognition.getClient();
                    recognizer.process(image).addOnSuccessListener(new OnSuccessListener<Text>() {
                        @Override
                        public void onSuccess(Text text) {
                            for (Text.TextBlock textBlock : text.getTextBlocks()) {
                                if ("hello".equalsIgnoreCase(textBlock.getText())) {
                                    Rect box = textBlock.getBoundingBox();
                                    int left = box.left;
                                    int top = box.top;
                                    int right = box.right;
                                    int bottom = box.bottom;
    
                                    ShapeDrawable drawable = new ShapeDrawable();
                                    drawable.getPaint().setColor(Color.YELLOW);
                                    drawable.getPaint().setStyle(Paint.Style.STROKE);
                                    drawable.getPaint().setStrokeWidth(5f);
                                    View shapeView = new View(decorView.getContext());
                                    shapeView.setBackground(drawable);
    
                                    FrameLayout rootView = findViewById(R.id.root_view);
                                    int[] location = new int[2];
                                    rootView.getLocationOnScreen(location);
    
                                    FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(right - left,
                                            bottom - top);
                                    params.setMargins(left - location[0],
                                            top - location[1],
                                            right - location[0],
                                            bottom - location[1]);
    
                                    rootView.addView(shapeView, params);
                                }
                            }
                        }
                    });
                }
            });
        }
    
        private static class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerViewAdapter.RecyclerViewHolder> {
            private final Context context;
            private final String[] elements = new String[] {"Hello", "Hello", "Bye", "Hello", "Hi there", "Hello"};
    
            private RecyclerViewAdapter(Context context) {
                this.context = context;
            }
    
            @NonNull
            @Override
            public RecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
                View item = LayoutInflater.from(context).
                        inflate(R.layout.list_item, parent, false);
                return new RecyclerViewHolder(item);
            }
    
            @Override
            public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) {
                holder.textView.setText(elements[position]);
            }
    
            @Override
            public int getItemCount() {
                return elements.length;
            }
    
            public static class RecyclerViewHolder extends RecyclerView.ViewHolder {
                private final TextView textView;
    
                public RecyclerViewHolder(@NonNull View itemView) {
                    super(itemView);
    
                    this.textView = itemView.findViewById(R.id.element_view);
                }
            }
        }
    }
    

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/root_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingStart="30dp"
            android:orientation="vertical">
    
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/recycler_view"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scrollbars="vertical" />
    
        </LinearLayout>
    </FrameLayout>
    

    如您所见,我使用FrameLayout 作为根视图。

    list_item.xml

    <?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="wrap_content"
        android:background="?android:attr/selectableItemBackground"
        android:orientation="vertical">
    
        <TextView
            android:id="@+id/element_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingTop="16dp"
            android:paddingBottom="8dp"
            android:fontFamily="google-sans-medium"/>
    
        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000"/>
    
    </LinearLayout>
    

    布局没有什么特别之处 - 只是 RecyclerView 的一个简单布局。

    结果

    所有 4 个“Hello”结果都以黄色突出显示。

    更新

    如果您不是从Activity(在您的 GitHub 项目中您是从 Service 检索它),请确保您使用的是正确的方法来检索显示尺寸,因为您需要真实的显示尺寸,而不是别的东西。所以,请按照以下方式进行:

            // get width and height
            WindowManager wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
            Display display = wm.getDefaultDisplay();
            Point size = new Point();
            display.getRealSize(size);
            mWidth = size.x;
            mHeight = size.y;
    

    因此,在您的示例中,您必须将方法更改为:

        private void createVirtualDisplay() {
            // get width and height
            WindowManager wm = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
            Display display = wm.getDefaultDisplay();
            Point size = new Point();
            display.getRealSize(size);
            mWidth = size.x;
            mHeight = size.y;
            ...
        }
    

    就是这样。

    【讨论】:

      【解决方案2】:

      它更像是下一个调试步骤而不是答案。: 因此,您可以看到每个标记之间的边距随着每个突出显示调用而增加。 看来您没有捕获整个屏幕。也许没有添加状态栏。这是我的直觉。

      您绝对可以尝试增加上边距并检查结果。

      public void addHighlightCard(int x, int y, int width, int height) {
          View highlightCard = inflater.inflate(R.layout.highlight_card, overlayRoot, false);
          RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(width, height);
          params.leftMargin = x;
          params.topMargin = y+5;
          highlightCard.setLayoutParams(params);
          overlayRoot.addView(highlightCard, params);
      }
      

      【讨论】:

        【解决方案3】:

        输入 MLKit 并在预览中显示的图像可能有不同的尺寸。请参考mlkit example 了解如何缩放和映射它们。

        【讨论】:

          猜你喜欢
          • 2021-07-24
          • 2022-10-23
          • 1970-01-01
          • 1970-01-01
          • 2021-12-23
          • 1970-01-01
          • 1970-01-01
          • 2021-05-07
          • 1970-01-01
          相关资源
          最近更新 更多