android Paddle 视频字幕识别TTS语音

OcrDemo

文章目录

        • OcrDemo
        • 介绍
          • 1:主要代码
          • 2:获取视频的播放页面
          • 3:判断是否在视频页面 并且监听是否实时横屏
          • 4:handler导致性能卡顿
          • 5:竖屏截图时,切换横屏时导致横屏截取的是竖屏的大小
          • 6:AccessibilityService的onkeyeven方法始终无反应
          • 7:其他相关代码
        • 自定义定时器
        • 软件架构
        • 安装教程
        • 使用说明
        • 参与贡献
        • 流程图

介绍

本项目是给盲人提供的一款看电影的实时英文字幕读取的软件。
主要采用的技术:
MediaProjection截取 + AccessibilityService + 动态横竖屏切换实时读取 + 百度底层开源OCR + EventBus进程间通信 + 讯飞TTS语音合成
攻克的技术难点:

1,后台截取图片时,每次截取都会弹窗授权
2,获取不到播放页面
3,获取不到视频软件是否打开
4,使用handler导致性能卡顿
5,竖屏截图时,切换横屏时导致横屏截取的是竖屏的大小
6,AccessibilityService的onkeyeven方法始终无反应
7,横竖屏切换数据不同步 导致截图宽高出错

有技术大佬可以一起探讨,自动定位字幕位置的方法,原因有亮点:

  • 一视频播放页面无效文字太多,比如左上角影名,右下角广告,右上角动态广告并且这些不是固定的也就是说有的视频有有的没有。
  • 二视频字幕不固定,比如:综艺节目演员字幕在左下角,导演字幕在中间,还有后期剪辑上去的歪歪扭扭的字幕,电影在中间,还有在上部分屏幕中间,还有在视频右下角的不是字幕的版本号
  • 三如何去除无效文字,我们知道无效文字的位置但是机器不知道,并且ocr每次返回的文字坐标也不固定所以我现在采取的是找规律只找有效。

我查到了此大佬的方式:

  • https://blog.csdn.net/flavioy/article/details/120218378?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&utm_relevant_index=2
  • 但是我也不会用他的方式哈哈哈
  • 若有大佬支持我将感激万分。gitee联系我拉你进去

下面是演示视频:

基于paddle的文字互转语音

手势操作版本请查看我另一篇文章:

https://blog.csdn.net/qq_39469700/article/details/123880230 手势执行操作

1:主要代码

相关代码贴出来 因为涉及公司秘密 所以不能贴出全部代码 这是我封装的工具类

/**
* 截屏相关工具类
* 录屏没有封装
* 可以一次授权 连续截屏
*/
public class MediaProjrct implements ImageReader.OnImageAvailableListener {
   /**
    * 截屏管理器
    */
   private static MediaProjectionManager systemService;
   private MediaProjection mediaProjection;
   @SuppressLint("StaticFieldLeak")
   public static MediaProjrct mediaProjrct;
   private VirtualDisplay virtualDisplay;
   @SuppressLint("StaticFieldLeak")
   public static Activity activity;
   private static int densityDpi;

   /**
    * 类创建 就初始化相关权限
    */
   @RequiresApi(api = Build.VERSION_CODES.M)
   public void with(Activity activity) {
       if (mediaProjrct == null) {
           MediaProjrct.activity = activity;
           mediaProjrct = new MediaProjrct();
       }
       initCutManger();
   }

   /**
    * 获取截瓶权限
    *
    * @return
    */
   @RequiresApi(api = Build.VERSION_CODES.M)
   @SuppressLint("WrongConstant")
   public void initCutManger() {
       densityDpi = activity.getResources().getDisplayMetrics().densityDpi;
       systemService = (MediaProjectionManager) activity.getSystemService(MEDIA_PROJECTION_SERVICE);
       Intent screenCaptureIntent = systemService.createScreenCaptureIntent();
       activity.startActivityForResult(screenCaptureIntent, ApkNames.PERMISSION_CODE);
       EventBus.getDefault().register(this);
   }

   /**
    * 交给actvity 在mainactivity中的相同重载的方法中使用
    */
   @RequiresApi(api = Build.VERSION_CODES.M)
   public void onActivityReust(int requestCode, int resultCode, Intent data) {
       if (requestCode != ApkNames.PERMISSION_CODE) {
           return;
       }
       if (resultCode != RESULT_OK) {
           return;
       }
       mediaProjection = systemService.getMediaProjection(resultCode, data);
   }

   private int hight;

   @Subscribe(threadMode = ThreadMode.MAIN)
   public void eventPost(String mhight) {
       hight = Integer.parseInt(mhight);
       startScreenCapture();
   }

   int screenW;
   int screenH;

   /**
    * 截屏
    * VirtualDisplay表示一个虚拟显示,显示的内容render到 createVirtualDisplay()参数的Surface。
    * 因为virtual display内容render到应用程序提供的surface,所以当进程终止时,它将会自动释放,并且所以剩余的窗口都会被强制删除。但是,你仍然需要在使用完后显式地调用release()方法。
    * 此处的 with  和 hight 表示
    * 截图的宽和高
    * 但是由于不匹配会有黑色边框  所以加入三木
    * 如果此类的mhight == hight 则直接获取屏幕的高度 否则创建的截图宽和高是匹配的 但是截取的图片是全屏  然后导致有黑色边框
    * 反之直接创建屏幕的大小
    * 

* 问题场景 * 1 竖屏状态时需要截取的是视频view的宽和高 但是surface创建的是屏幕的宽和高 所以导创建的宽和高是匹配的 但是截取的却是整个屏幕 而我们需要的是视频播放的view 所以就不符合我们需求 * 2 横屏状态下 直接截取当前屏幕的宽和高 。 *

* 以上会有一个问题 就是长时间 横屏或者竖屏 突然横屏或者竖屏会导致img buff缓冲区不足 */ public void startScreenCapture() { if (mediaProjection != null) { screenW = WindoesCut.getScreenW(activity); screenH = WindoesCut.getScreenH(activity); ImageReader mImageReader = ImageReader.newInstance(screenW, screenH, 0x1, 1); mImageReader.setOnImageAvailableListener(this, null); virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture", screenW, screenH, densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null); } } /** * 这里的一定要设置为virtualDisplay = null * 虽然他会每次使用结束自动释放 但是你还是需要手动释放 * 否则导致 bitmap 花屏 */ private void stopScreenCapture() { if (virtualDisplay != null) { virtualDisplay.release(); virtualDisplay = null; bitmap = null; } } private Bitmap bitmap; @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireLatestImage(); if (image != null) { final Image.Plane[] planes = image.getPlanes(); if (planes.length > 0) { ByteBuffer buffer = planes[0].getBuffer(); //每个像素的间距 int pixelStride = planes[0].getPixelStride(); //总的间距 int rowStride = planes[0].getRowStride(); int rowPadding1 = rowStride - pixelStride * screenW; if (bitmap == null) { bitmap = Bitmap.createBitmap((screenW + (rowPadding1 / pixelStride)), screenH, Bitmap.Config.ARGB_8888); } try { bitmap.copyPixelsFromBuffer(buffer); AuButBitmap auButBitmap = null; if (auButBitmap == null) { auButBitmap = new AuButBitmap(); } auButBitmap.setBitmap(bitmap); auButBitmap.setHight(hight); auButBitmap.setScreen(WindoesCut.isH(activity)); EventBus.getDefault().post(auButBitmap); } catch (Exception e) { int h = WindoesCut.isH(activity); switch (h) { case Surface.ROTATION_0: Log.e("TAG", "onImageAvailable: buffer 异常当前竖屏" + hight); break; case Surface.ROTATION_90: case Surface.ROTATION_270: Log.e("TAG", "onImageAvailable: buffer 异常当前横屏" + hight); break; } } image.close(); stopScreenCapture(); } } } /** * 此处的imageReader 和 okhttp的 body 一样 必须要获取一遍后再使用 否则会导致空指针 * 因为imageReader 只能获取一次 所以要创建一个变量保存下来 * stopScreenCapture() *

* 因为涉及到横竖屏幕的切换 所以要及时吧bitmap设置未null * 不然会出现 竖屏状态时 图片完好 但是切换到横屏时 横屏截取的图片却和竖屏一样 反之也是 * 注意 这里的bitmapcreate的宽和高 并没有像上面一样进行判断 所以细节就在这个地方 * 因为我们竖屏状态下截取的不是整个屏幕 所以我们要把surface的宽高 进行截取 截取的就是bitmap的高所以就会符合我们的需求 *

* Buffer not large enough for pixels */ } /** * ocr识别文字的工具类 * 作用 去重等 */ public class TextUtils { /** * 获取字符串相等的个数 * @param s1 * @param s2 * @return */ public static int getEquals(String s1, String s2) { if (isEmpty(s1) && isEmpty(s2)) { return getPercent(s1, s2); } return 0; } private static int getPercent(String s1, String s2) { int num = 0; int length1 = s1.length(); int length2 = s2.length(); for (int i = 0; i < length1; i++) { for (int j = 0; j < length2; j++) { char c1 = s1.charAt(i); char c2 = s2.charAt(j); if (c1 == c2) { num++; } } } NumberFormat numberFormat = NumberFormat.getInstance(); numberFormat.setMaximumFractionDigits(0); return Integer.parseInt(numberFormat.format((float) num / (float) length1 * 100)); } /** * 判断字符串是否为空 * * @param s * @return */ public static boolean isEmpty(String s) { if (s == null || s.equals("")) { return true; } else { return false; } } /** * 判断是否包含特殊字符 * * @param str * @return */ public static boolean isSpecialChar(String str) { String regEx = "[0o _`~!@#$%^&*()+=|{}':;',\\[\\]<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]|\n|\r|\t"; Pattern p = Pattern.compile(regEx); Matcher m = p.matcher(str); return m.find(); } //字符串第一个字 是不是英文 public static boolean isFristAZ(String s){ String frist = s.substring(0, 1); boolean matches = frist.matches("[a-zA-Z]+"); return matches; } //字符串第一个字是不是数字 public static boolean isFristNum(String s){ String frist = s.substring(0, 1); boolean matches = frist.matches("[0-9]+"); return matches; } /** * 判断第一句话和第二句话是否差不多 * @param s1 * @param s2 * @return */ public static boolean isContain(String s1, String s2) { if (!isEmpty(s1) && !isEmpty(s2)) { if (s1.length() > s2.length()) { return s1.contains(s2); } else { return s2.contains(s1); } } return false; } } /** * 屏幕相关工具 * user gewu * time 22031815 */ public class WindoesCut { /** * 获取的是视频view的宽 * @param rect * @return */ public static int getWith(Rect rect) { return rect.right - rect.left; } /** * 获取的是视频的高 * @param rect * @return */ public static int gethight(Rect rect) { if (rect == null) { return 0; } return rect.bottom - rect.top; } /** * 裁剪一定高度保留下面 * @param srcBitmap * @param needHeight * @return */ public static Bitmap cropBitmapBottom(Bitmap srcBitmap, int needwith, int needHeight) { /**裁剪保留下部分的第一个像素的Y坐标*/ int needY = srcBitmap.getHeight() - needHeight; /**裁剪关键步骤*/ return Bitmap.createBitmap(srcBitmap, needwith, needY, srcBitmap.getWidth(), needHeight); } /** * 获取的是屏幕 * @param context * @return */ private static DisplayMetrics getDisplayMetrics(Context context) { DisplayMetrics metrics = new DisplayMetrics(); getDispaly(context).getMetrics(metrics); return metrics; } /** * 获取屏幕管理器 * @param context * @return */ private static Display getDispaly(Context context) { WindowManager systemService = (WindowManager) (context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE)); return systemService.getDefaultDisplay(); } /** * 获取屏幕宽度 * @param context * @return */ public static int getScreenW(Context context) { return getDisplayMetrics(context).widthPixels; } /** * 获取屏幕高度 * @param context * @return */ public static int getScreenH(Context context) { return getDisplayMetrics(context).heightPixels; } /** * 屏幕旋转角度 * 如果屏幕旋转90°或者270°是判断为横屏 */ public static int isH(Context context) { int angle = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation(); return angle; } }

2:获取视频的播放页面

这一块主要就是判断是否进入自己预定的视屏app 然后根据相关控件信息获取到播放视屏的view的rect 之后就可以获取到是视频view的宽度/高度

    /**
     * 获取视频view的rect
     */
    private void getVedioView() {
        AccessibilityNodeInfo rootInActiveWindow = getRootInActiveWindow();
        if (rootInActiveWindow != null && rootInActiveWindow.getChildCount() != 0) {
            for (int i = 0; i < rootInActiveWindow.getChildCount(); i++) {
                if (rootInActiveWindow.getChild(i).getChildCount() != 0) {
                    AccessibilityNodeInfo activeWindowChild = rootInActiveWindow.getChild(i);

                    int childCount = activeWindowChild.getChildCount();
                    for (int j = 0; j < childCount; j++) {
                        if (j + 2 > childCount || activeWindowChild.getChild(j).getClassName() == null) {
                            return;
                        }
                        //这里的判断是 判断是否为播放视频的控件
                        if ((activeWindowChild.getChild(j).getClassName().equals("android.widget.RelativeLayout")
                                && activeWindowChild.getChild(j + 1).getClassName().equals("android.widget.TextView") && activeWindowChild.getChild(j + 2).getClassName().equals("android.widget.TextView"))
                                || (activeWindowChild.getChild(j).getClassName().equals("android.widget.RelativeLayout") && activeWindowChild.getChild(j + 1).getClassName().equals("android.widget.ImageView")
                                && activeWindowChild.getChild(j + 2).getClassName().equals("android.widget.TextView"))) {
                            AccessibilityNodeInfo child = activeWindowChild.getChild(j);
                            rect = new Rect();
                            child.getBoundsInScreen(rect);
                        }
                    }
                }
            }
        }
    }
3:判断是否在视频页面 并且监听是否实时横屏

这一块主要就是判断是否进入视频app 并且监听横竖屏 更改到service里面进行

    public void onAccessibilityEvent(AccessibilityEvent event) {
          //判断是否在腾讯视频&&是否在播放页面
          if (event.getPackageName().toString().equals(ApkNames.QQLIVE)) {
              if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
                  int isOnVideo = AccessHelper.getActivityName(event, this);
                  if (isOnVideo == 1) {
                      if (rect == null){
                          getVedioView();
                      }
                      instion.setObj(WindoesCut.gethight(rect));
                  }
              }
          }
      }
  
      @Override
      public void onConfigurationChanged(Configuration newConfig) {
          super.onConfigurationChanged(newConfig);
          if (newConfig.orientation == 1) {
              instion.stopTimer();
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              instion.setObj(WindoesCut.gethight(rect));
              instion.startTimer();
          } else if (newConfig.orientation == 2) {
              instion.stopTimer();
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              int screenH = WindoesCut.getScreenH(this);
              Log.i("test", "onConfigurationChanged: "+screenH);
              instion.setObj(screenH);
              instion.startTimer();
          }
      }
4:handler导致性能卡顿

导致handler的性能卡顿的原因就是,字幕是实时的ocr转换也是实时的,所以导致有时候,一个ocr转换的文字被message发送若干遍。后来我采用EVENBUS的方式返回相关的数据,最后放在mainactivity中进行相关数据操作,解决了数据不同步问题

         if (getIsRun()) {
             EventBus.getDefault().post(String.valueOf(mhight));
         } else {
             Log.i("TAG", "run: ---------已经暂停......");
         }

这里主要是MediaProjrct截屏后返回的bitmap 我们需要用到bitmap之后用ocr转换成文字,但是千万不要忘记销毁bitmap否则会消耗大量的资源。

        Bitmap bitmap1 = auButBitmap.getBitmap();
        int hight = auButBitmap.getHight();
        int screen = auButBitmap.getScreen();
        int screenH = WindoesCut.getScreenH(getApplicationContext());
        int screenW = WindoesCut.getScreenW(getApplicationContext());
        Bitmap bitmapBottom;
        switch (screen) {
            case Surface.ROTATION_0:
                bitmapBottom = Bitmap.createBitmap(bitmap1, 0, hight / 2, bitmap1.getWidth(), hight / 2);
                break;
            case Surface.ROTATION_90:
            case Surface.ROTATION_270:
                bitmapBottom = Bitmap.createBitmap(bitmap1, 0, bitmap1.getHeight() / 2, bitmap1.getWidth(), bitmap1.getHeight() / 2);
                break;
            default:
                throw new IllegalStateException("Unexpected value: " + screen);
        }
        //判断当前截图的高和宽  与  获取屏幕的高和宽是否有区别  如果横屏/竖屏下  截屏的宽/高比获取的屏幕的宽小 就说明截图不对
        if (bitmapBottom.getWidth() < screenW   ||  bitmapBottom.getHeight() > screenH){
            return;
        }
//        ImageUtils.saveBitmap2file(bitmapBottom, getApplicationContext(), new Date().toString());
        recognitionText(bitmapBottom);
        bitmapBottom.recycle();

这里的识别文字用的是百度底层的PaddleLite开源框架,主要执行两个步骤,
1定位文字坐标,2返回文字内容
这里我把返回的文字进行了相关的处理:比如,包含,等于,相等率,非字符,英文等。第一次的想法是获取第一次的文字坐标Potion后根据第一次的坐标进行返回,
后来因为字幕的坐标的长度是不可控的,然后直接获取的视频高的2/3。

    runOnUiThread(new Runnable() {
               @Override
               public void run() {
                   predictor.setInputImage(bitmap);
                   boolean runModel = predictor.runModel();
                   if (runModel) {
                       List textResult = predictor.getTextResult();
                       //既然有字幕 那么字幕的坐标一定不为null
                       if (textResult != null && textResult.size() != 0) {
                           oldtextResult = textResult.get(0);
   
                           if (TextUtils.isFristNum(oldtextResult) || TextUtils.isFristAZ(oldtextResult) || oldtextResult.equals(newtextResult)
                                   || oldtextResult.length() == 1  || oldtextResult.equals(ApkNames.TXLIVE)) {
                           } else {
                               ttsUtils.startSpeech(oldtextResult);
                               newtextResult = oldtextResult;
                               Log.e("test", "handleMessage: 转换文字成功------字幕:" + oldtextResult);
                           }
   
                           /**先判断是否包含上一次的文字
                            * 包含就是文字重复
                            * 不包含就进行操作
                            *
                            * 在进行判断是否包含特殊字符
                            * 包含就不操作
                            * 不包含就继续判断概率
                            *
                            */
                       } else {
                           Log.i(TAG, "handleMessage: 转换文字失败");
                       }
                   }
               }
           });
5:竖屏截图时,切换横屏时导致横屏截取的是竖屏的大小

如下:

    /**
     * 截屏
     * VirtualDisplay表示一个虚拟显示,显示的内容render到 createVirtualDisplay()参数的Surface。
     * 因为virtual display内容render到应用程序提供的surface,所以当进程终止时,它将会自动释放,并且所以剩余的窗口都会被强制删除。但是,你仍然需要在使用完后显式地调用release()方法。
     * 此处的 with  和 hight 表示
     * 截图的宽和高
     * 但是由于不匹配会有黑色边框  所以加入三木
     * 如果此类的mhight == hight 则直接获取屏幕的高度 否则创建的截图宽和高是匹配的 但是截取的图片是全屏  然后导致有黑色边框
     * 反之直接创建屏幕的大小
     * 

* 问题场景 * 1 竖屏状态时需要截取的是视频view的宽和高 但是surface创建的是屏幕的宽和高 所以导创建的宽和高是匹配的 但是截取的却是整个屏幕 而我们需要的是视频播放的view 所以就不符合我们需求 * 2 横屏状态下 直接截取当前屏幕的宽和高 。 *

* 以上会有一个问题 就是长时间 横屏或者竖屏 突然横屏或者竖屏会导致img buff缓冲区不足 */ public void startScreenCapture() { if (mediaProjection != null) { screenW = WindoesCut.getScreenW(activity); screenH = WindoesCut.getScreenH(activity); ImageReader mImageReader = ImageReader.newInstance(screenW, screenH, 0x1, 1); mImageReader.setOnImageAvailableListener(this, null); virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture", screenW, screenH, densityDpi, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null); } }

6:AccessibilityService的onkeyeven方法始终无反应

查阅相关资料 android 10好像无解 如果您知道 请告诉我一下 谢谢。

7:其他相关代码

JNI层的java代码 主要就是获取文字坐标和文字

 public boolean runModel() {
        if (inputImage == null || !isLoaded()) {
            return false;
        }
        Bitmap scaleImage = Utils.resizeWithStep(inputImage, Long.valueOf(inputShape[2]).intValue(), 32);
        int channels = (int) inputShape[1];
        int width = scaleImage.getWidth();
        int height = scaleImage.getHeight();
        float[] inputData = new float[channels * width * height];
        if (channels == 3) {
            int[] channelIdx = null;
            if (inputColorFormat.equalsIgnoreCase("RGB")) {
                channelIdx = new int[]{0, 1, 2};
            } else if (inputColorFormat.equalsIgnoreCase("BGR")) {
                channelIdx = new int[]{2, 1, 0};
            } else {
                return false;
            }
            int[] channelStride = new int[]{width * height, width * height * 2};
            int[] pixels = new int[width * height];
            scaleImage.getPixels(pixels, 0, scaleImage.getWidth(), 0, 0, scaleImage.getWidth(), scaleImage.getHeight());
            for (int i = 0; i < pixels.length; i++) {
                int color = pixels[i];
                float[] rgb = new float[]{(float) red(color) / 255.0f, (float) green(color) / 255.0f,
                        (float) blue(color) / 255.0f};
                inputData[i] = (rgb[channelIdx[0]] - inputMean[0]) / inputStd[0];
                inputData[i + channelStride[0]] = (rgb[channelIdx[1]] - inputMean[1]) / inputStd[1];
                inputData[i + channelStride[1]] = (rgb[channelIdx[2]] - inputMean[2]) / inputStd[2];
            }
        } else if (channels == 1) {
            int[] pixels = new int[width * height];
            scaleImage.getPixels(pixels, 0, scaleImage.getWidth(), 0, 0, scaleImage.getWidth(), scaleImage.getHeight());
            for (int i = 0; i < pixels.length; i++) {
                int color = pixels[i];
                float gray = (float) (red(color) + green(color) + blue(color)) / 3.0f / 255.0f;
                inputData[i] = (gray - inputMean[0]) / inputStd[0];
            }
        } else {
            return false;
        }
        for (int i = 0; i < warmupIterNum; i++) {
            paddlePredictor.runImage(inputData, width, height, channels, inputImage);
        }
        warmupIterNum = 0;
        results = paddlePredictor.runImage(inputData, width, height, channels, inputImage);
        results = postprocess(results);

        if (inputImage!=null){
            inputImage .recycle();
        }
        return true;
    }

    private ArrayList postprocess(ArrayList results) {
        for (OcrResultModel r : results) {
            StringBuffer word = new StringBuffer();
            for (int index : r.getWordIndex()) {
                if (index >= 0 && index < wordLabels.size()) {
                    word.append(wordLabels.get(index));
                } else {
                    Log.e(TAG, "Word index is not in label list:" + index);
                    word.append("×");
                }
            }
            r.setLabel(word.toString());
        }
        return results;
    }


    public boolean isLoaded() {
        return paddlePredictor != null && isLoaded;
    }

    public void setInputImage(Bitmap image) {
        if (image == null) {
            return;
        }
        this.inputImage = image.copy(Bitmap.Config.ARGB_8888, true);
    }

    private List textResults(ArrayList results) {
        List stringList = new ArrayList<>();
        for (int i = 0; i < results.size(); i++) {
            OcrResultModel result = results.get(i);
            stringList.add(result.getLabel());
        }
        return stringList;
    }

    private List potinResults(ArrayList results) {
        List points = new ArrayList<>();
        for (int i = 0; i < results.size(); i++) {
            OcrResultModel result = results.get(i);
            points.addAll(result.getPoints());
        }
        return points;
    }

    public List getTextResult() {
        return textResults(results);
    }

    public List getTextPotion() {
        return potinResults(results);
    }

自定义定时器


/**
 * 自定义简单计时器
 * user : gewu
 * time : 22031709
 */
public class TimerTasks extends java.util.TimerTask {

    private static boolean isRun;
    public static Timer timer;
    @SuppressLint("StaticFieldLeak")
    private static TimerTasks timerTasks;
    @SuppressLint("StaticFieldLeak")
    private int mhight;

    public static TimerTasks getInstion() {
        if (timer == null) {
            timer = new Timer();
        }
        if (timerTasks == null) {
            timerTasks = new TimerTasks();
        }
        return timerTasks;
    }

    @Override
    public void run() {
        if (getIsRun()) {
            EventBus.getDefault().post(String.valueOf(mhight));
        } else {
            Log.i("TAG", "run: ---------已经暂停......");
        }
    }

    /**
     * 先获取timer的运行状态
     * 如果不在运行 就直接设置为他的反
     */
    public void startTimer() {
        if (!getIsRun()) {
            isRun = true;
        }
    }

    /**
     * 暂停
     */
    public void stopTimer() {
        isRun = false;
    }

    /**
     * 设置定时器的间隔时间
     */
    public void setTimer(int delay, int time) {
        if (timer == null) {
            Log.e("timer", "设置Timer: timer is null......");
            return;
        }
        timer.schedule(timerTasks, delay, time);
    }

    public void setObj(int hight) {
        this.mhight = hight;
    }

    /**
     * 获取当前的定时器状态
     */
    protected boolean getIsRun() {
        return isRun;
    }

    /**
     * 直接销毁定时器
     */
    public void cancelTimer() {
        if (timer == null) {
            Log.e("timer", "销毁Timer: timer is null......");
            return;
        }
        stopTimer();
        timer.purge();
        timer.cancel();
        timer = null;
        timerTasks = null;
    }
}

软件架构

最好的设计就是没有设计,本项目是mvc架构。
主要就是服务层和数据层获取到相关内容通知Activity进行更新。
没有UI 没有过多的页面绘制,主要的全部在逻辑后台。纯离线方式的实时字幕识别。

安装教程

打开本项目,里面有apk

使用说明

1,打开软件
2,点击获取两个权限
3,保持后台运行
4,打开相关视频app就OK了

参与贡献

如果您需要,可在此项目上加入实时翻译功能。
目前正在 训练文字识别模型 uping…

流程图

android Paddle 视频字幕识别TTS语音_第1张图片

你可能感兴趣的:(androd,百度,文字识别)