基于飞浆Paddle的Android字幕实时提取

基于飞浆Paddle的Android字幕实时提取

介绍

本项目是给盲人提供的一款看电影的实时英文字幕读取的软件;主要采用的技术: MediaProjection截取屏幕 + AccessibilityService监听手势 + 开源OCR飞浆Paddle + 进程间通信 + 讯飞TTS语音合成

一,对接PaddleLite编译相关文件

1,下载paddle官网的相关demo

官网文档 github.com/PaddlePaddl…

项目地址 gitee.com/gewussj/Ocr…

2,配置AndroidStudio的NDK

  • 把相关文件拷贝到自己的项目后,下载相关NDK,MARK用来编译我们拷贝的文件
  • 主要文件:
  • 1 OpenCV
  • 2 PaddleLite
  • 将1和2文件放在项目app包之下
  • 3 cpp
  • 将3放在app/src/main您的包名下
  • 4 assets
  • 将4放在src/main之下 并设置为source root(选中文件右键Mark dir as)
  • 5 java文件包名ocr

3,下载NDK和MARK

  • 下载相关匹配的NDK这里就不多说了
  • 配置您的build.gradle文件/参考官方demo的相同文件
  • 修改Predictor.java文件的相关方法,使他适合您自己使用

二,新建无障碍服务

  • 我这里主要实现了两个方法
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (newConfig.orientation == 1) {
            ttsUtils.startSpeech("已进入竖屏,读屏已经暂停");
        } else if (newConfig.orientation == 2) {
            ttsUtils.startSpeech("已进入横屏,请使用手势开始读屏");
        }
        timerInstance.stopDefaultTimer();
    }

    @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    protected boolean onGesture(int gestureId) {
        if (gestureId == GESTURE_SWIPE_DOWN_AND_UP) {
            int h = ScreenUtils.isH(getApplicationContext());
            if (h == Surface.ROTATION_270 || h == Surface.ROTATION_90) {
                timerInstance.starDefaultTimer();
                ttsUtils.startSpeech("已经开始读屏");
            } else {
                ttsUtils.startSpeech("当前竖屏状态,请进入视频播放页面横屏下使用读屏");
                timerInstance.stopDefaultTimer();
            }
        }
        return true;
    }
复制代码
  • 第一个函数主要是实时监测手机横屏或者竖屏

  • 第二个函数就是监测我们的手势

    • 当我们在屏幕上执行(先下后上手势)时此onGesture函数会执行,我这里由于需求的原因需要检测横竖屏,竖屏状态下不执行。
  • 第二个函数如何配置执行

    • 1在无障碍描述文件添加允许手势操作
    • android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews|flagRequestTouchExplorationMode|flagRequestFingerprintGestures"
    • 这是我的全部标记 因为我不仅需要监测手势,还需要监测包名等其他的操作
    • 注意:
    • android:canRequestTouchExplorationMode = "true"
      android:canRequestFingerprintGestures="true"
      复制代码
    • 这两句将会非常重要
  • 全部如下

<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:accessibilityEventTypes="typeAllMask"
    android:accessibilityFeedbackType="feedbackGeneric|feedbackSpoken|feedbackVisual"
    android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews|flagRequestTouchExplorationMode|flagRequestFingerprintGestures"
    android:canRetrieveWindowContent="true"
    android:canRequestFilterKeyEvents="true"
    android:canPerformGestures="true"
    android:notificationTimeout="100"
    android:canRequestTouchExplorationMode = "true"
    android:canRequestFingerprintGestures="true"
    tools:targetApi="o" />
复制代码

三,前台服务的辅助服务类


public class MediaProjrctService extends Service implements ImageReader.OnImageAvailableListener {

    private VirtualDisplay virtualDisplay;
    private MediaProjection mediaProjection;
    private Predictor predictor;
    private TtsUtils ttsUtils;
    private Activity activity;
    private int densityDpi;
    private  Context context;

    public Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            if (msg.what == 0) {
                startScreenCapture();
            }
        }
    };

    @Override
    public void onCreate() {
        context = getApplicationContext();
        super.onCreate();
        Notification notification = createForegroundNotification();
        int NOTIFICATION_ID = 1;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
        } else {
            startForeground(NOTIFICATION_ID, notification);
        }
        registerReceivers();
    }

    @Override
    public void onDestroy() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            stopForeground(STOP_FOREGROUND_REMOVE);
        } else {
            stopForeground(true);
        }
        stopScreenCapture();
        ttsUtils.cancleSpeech();
        unregisterReceiver(timerRecivier);
        super.onDestroy();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Activity activity = AppUtils.getActivity();
        MediaProjection mediaProjection = AppUtils.getMediaProjection();
        if (activity != null && mediaProjection != null) {
            this.activity = activity;
            this.mediaProjection = mediaProjection;
            this.densityDpi = activity.getResources().getDisplayMetrics().densityDpi;
            predictor = Predictor.getInstace();
            predictor.init(activity, AppUtils.assetModelDirPath, AppUtils.assetlabelFilePath);
            ttsUtils = TtsUtils.initSpeech(activity);
        }
        return super.onStartCommand(intent, flags, startId);
    }

    private int screenW;
    private int screenH;
    private int cenScreenW;
    private Bitmap bitmap;

    public void startScreenCapture() {
        if (mediaProjection != null) {
            screenW = AppUtils.getScreenW(activity);
            screenH = AppUtils.getScreenH(activity);
            @SuppressLint("WrongConstant")
            ImageReader mImageReader = ImageReader.newInstance(screenW, screenH, 0x1, 1);
            mImageReader.setOnImageAvailableListener(MediaProjrctService.this, null);
            virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture", screenW, screenH, densityDpi,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null);
        }
    }

    @Override
    public void onImageAvailable(ImageReader reader) {
        Image image = reader.acquireNextImage();
        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);
                    recognitionText(bitmap);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                image.close();
                stopScreenCapture();
            }
        }
    }


    private int textH;
    private int textY;
    private String text = "";

    public void recognitionText(Bitmap bitmap) {
        Bitmap bitmapBottom = null;
        if (correctSuccess) {
            bitmapBottom = AppUtils.cropBitmapCustom(bitmap, 0, textY, bitmap.getWidth(), textH);
            predictor.setInputImage(bitmapBottom);
        } else {
            cenScreenW = AppUtils.getScreenW(activity) / 2;
            predictor.setInputImage(bitmap);
        }

        boolean runModel = predictor.runModel();
        if (runModel) {
            List<String> textResult = predictor.getTextResult();
            if (textResult != null && textResult.size() != 0) {
                List<Point> textPotion = predictor.getTextPotion();
                //计算ocr返回的一共几组数据
                int textBody = textPotion.size() / 4;
                //遍历每一组 规律:因为每一组四个坐标 所以每组之后乘每组的个数
                for (int i = 0; i < textBody; i++) {
                    //计算每组数据的中心坐标
                    int textCenter = (textPotion.get(i * 4).x + textPotion.get(i * 4 + 1).x) / 2;
                    //判断每组数据的中心坐标  是否在规定的中心区域内  因为有误差 所以设置的是屏幕中心的-10/+10
                    if ((cenScreenW - 100) < textCenter && textCenter < (cenScreenW + 100)) {
                        if (correctSuccess) {
                            if (AppUtils.isSmooth(textResult.get(i), text)) {
                                text = textResult.get(i);
                                ttsUtils.startSpeech(textResult.get(i));
                                if (bitmapBottom != null && isSaveImg) {
                                    AppUtils.saveBitmap(bitmapBottom, activity);
                                }
                            }
                        } else {
                            //correctNum 是表示多少次之后 标价字幕位置
                            if (AppUtils.isSmooth(textResult.get(i), text)) {
                                text = textResult.get(i);
                                ttsUtils.startSpeech(textResult.get(i));
                                if (AppUtils.correctNum >= 10) {
                                    AppUtils.correctSuccess = true;
                                    //如果区域不设置大一点 则会出现 识别不到文字的情况
                                    textY = textPotion.get(i * 4).y - 50;
                                    textH = textPotion.get(i * 4 + 2).y + 50 - textY;
                                } else {
                                    correctNum++;
                                }
                                if (bitmap != null && isSaveImg) {
                                    AppUtils.saveBitmap(bitmap, activity);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }


    private void registerReceivers() {
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("ssjAction");
        timerRecivier = new TimerRecivier();
        registerReceiver(timerRecivier, intentFilter);
    }

    public Timer timer;
    public TimerTask timerTask;
    public TimerRecivier timerRecivier;

    public void starDefaultTimer() {
        if (timer == null) {
            timer = new Timer();
        }
        if (timerTask == null){
            timerTask = new TimerTask() {
                @Override
                public void run() {
                    handler.sendEmptyMessage(0);
                }
            };
        }
        timer.schedule(timerTask,  1000 ,800);
    }

    public void stopDefaultTimer() {
        if (timer != null) {
            timer.purge();
            timer.cancel();
            timer = null;
        }
        if (timerTask != null) {
            timerTask.cancel();
            timerTask = null;
        }
    }

    /**
     * 这里的一定要设置为virtualDisplay = null
     * 虽然他会每次使用结束自动释放 但是你还是需要手动释放
     * 否则导致 bitmap 花屏
     */
    public void stopScreenCapture() {
        if (virtualDisplay != null) {
            virtualDisplay.release();
            virtualDisplay = null;
        }
    }
    
     /**
      * 接收无障碍服务的手势 打开or关闭
      */
    public class TimerRecivier extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            boolean isAction = intent.getBooleanExtra("isAction", false);
            if (isAction) {
                starDefaultTimer();
            } else {
                stopDefaultTimer();
            }
        }
    }

     /**
      * 服务初始化时 通知栏显示一个 通知
      */
    private Notification createForegroundNotification() {
        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        String notificationChannelId = "notification_channel_id_screen_capture";
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            String channelName = "ScreenCapture";
            int importance = NotificationManager.IMPORTANCE_NONE;
            NotificationChannel notificationChannel = new NotificationChannel(notificationChannelId, channelName, importance);
            notificationChannel.setDescription("点明字幕识别");
            notificationChannel.enableLights(false);
            notificationChannel.enableVibration(false);
            if (notificationManager != null) {
                notificationManager.createNotificationChannel(notificationChannel);
            }
        }
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, notificationChannelId);
        builder.setSmallIcon(R.mipmap.dianming_ocr);
        builder.setContentTitle(AppUtils.getString(context,R.string.notificationTitle));
        builder.setContentTitle(AppUtils.getString(context,R.string.notificationText));
        builder.setWhen(System.currentTimeMillis());
        return builder.build();
    }

复制代码

四,截屏的功能实现

  • 截屏的相关接口

    • 谷歌给我们提供了一个截屏和录屏的对外类,我们可以使用这个类执行相关的操作
    • MediaProjection
  • MediaProjection的封装

    • 由于java基础薄弱,所以采用了单例模式进行封装
    • 主要对外开放的就是,开始截屏,和结束截屏
    • ImageReader.newInstance(screenW, screenH, 0x1, 1)
    • 主要是这个:这个主要是创建想要获取的屏幕大小,后面两个都一样不用多操心
      private int screenW;
      private int screenH;
      private String text = "";
      private int cenScreenW;
      private Bitmap bitmap;
  
      public void startScreenCapture() {
          if (mediaProjection!=null){
              screenW = AppUtils.getScreenW(activity);
              screenH = AppUtils.getScreenH(activity);
              @SuppressLint("WrongConstant") 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);
          }
      }

复制代码
  • 截屏后的回掉
    private Bitmap bitmap;

    @Override
    public void onImageAvailable(ImageReader reader) {
        Image image = reader.acquireNextImage();
        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);
                    EventBus.getDefault().post(bitmap);
                } catch (Exception e) {
                    Log.e("TAG", "onImageAvailable: " + e);
                }
                image.close();
                reader.close();//这个竟然忘记了  不然ImageReader一直有可用图像时将会一直调用
                stopScreenCapture();
            }
        }
    }
复制代码
  • 这里截屏后的回掉一定要注意,否则手机发烫严重。
  • 1 Image image = reader.acquireNextImage();
  • 这里为什么不进行直接使用,因为这里只可以获取一次,这行代码的意思是,获取截屏后返回的Image
  • 个人认为有点类似与OkHttp的Body返回也是只可以获取一次
  • 2 image.close(); reader.close();
  • 这里一定要记得关闭,否则你会发现当你执行结束截屏的时候此回掉依然会返回图片。

五,文字识别

public void recognitionText(Bitmap bitmap) {
           if (correctSuccess) {
               Bitmap bitmapBottom = AppUtils.cropBitmapCustom(bitmap, 0, textY, bitmap.getWidth(), textH);
               predictor.setInputImage(bitmapBottom);
               AppUtils.saveBitmap2file(bitmapBottom,activity,new Date().toString(),"/ssj/");
           } else {
               cenScreenW = AppUtils.getScreenW(activity) / 2;
               predictor.setInputImage(bitmap);
               AppUtils.saveBitmap2file(bitmap,activity,new Date().toString(),"/ssj/");
           }
   
           boolean runModel = predictor.runModel();
           if (runModel) {
               List<String> textResult = predictor.getTextResult();
               //既然有字幕 那么字幕的坐标一定不为null
               if (textResult != null && textResult.size() != 0) {
                   List<Point> textPotion = predictor.getTextPotion();
                   //计算ocr返回的一共几组数据
                   int textBody = textPotion.size() / 4;
                   //遍历每一组 规律:因为每一组四个坐标 所以每组之后乘每组的个数
                   for (int i = 0; i < textBody; i++) {
                       //计算每组数据的中心坐标
                       int textCenter = (textPotion.get(i * 4).x + textPotion.get(i * 4 + 1).x) / 2;
                       //判断每组数据的中心坐标  是否在规定的中心区域内  因为有误差 所以设置的是屏幕中心的-10/+10
                       if ((cenScreenW - 10) < textCenter && textCenter < (cenScreenW + 10)) {
                           if (correctSuccess) {
                               if (AppUtils.isSmooth(textResult.get(i), text)) {
                                   text = textResult.get(i);
                                   ttsUtils.startSpeech(textResult.get(i));
                                   Log.i("TAG", "run: " + text);
                               }
                           } else {
                               if (AppUtils.isSmooth(textResult.get(i), text)) {
                                   text = textResult.get(i);
                                   ttsUtils.startSpeech(textResult.get(i));
                                   if (correctNum >= 10) {
                                       correctSuccess = true;
                                       textY = textPotion.get(i * 4).y - 20;
                                       textH = textPotion.get(i * 4 + 2).y + 20 - textY;
                                   } else {
                                       correctNum++;
                                   }
                                   Log.i("TAG", "run: " + text);
                               }
                           }
                       }
                   }
               }
           }
       }       
复制代码
  • 这里我写的很清楚了
  • ScreenUtils.getScreenW(getApplicationContext())
  • 获取屏幕宽度
  • predictor.getTextResult()
  • 获取OCR返回文字组,是个集合
  • predictor.getTextPotion()
  • 获取OCR返回的文字坐标,也是个Potion集合,每个potion是四个点的坐标。
  • ttsUtils.startSpeech(textResult.get(i));
  • 读取全部返回文字
  • TextUtils.isSmooth(textResult.get(i), newtextResult)
  • 判断返回字符是否符合逻辑
  • 由于是字幕 这里我判断了是否剧中 居中是上半屏还是下半屏
  • 然后第二次截取的是标记的上半屏/下半屏节约识别时间因为全屏识别每次的识别时间大于内置的计时器时间

六,无障碍服务的手势

 @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    protected boolean onGesture(int gestureId) {
        boolean isAction;
        if (gestureId == GESTURE_SWIPE_DOWN_AND_UP) {
            int h = AppUtils.isH(getApplicationContext());
            if (h == Surface.ROTATION_270 || h == Surface.ROTATION_90) {
                ttsUtils.startSpeech(AppUtils.getString(context,R.string.screenReadingStart));
                isAction = true;
            } else {
                ttsUtils.startSpeech(AppUtils.getString(context,R.string.screenReadingTip));
                isAction = false;
            }
            sndTimerCast(isAction);

            //保存识别图片
        } else if (gestureId == GESTURE_SWIPE_DOWN_AND_LEFT) {
            AppUtils.isSaveImg = !AppUtils.isSaveImg;
            if (AppUtils.isSaveImg) {
                ttsUtils.startSpeech(AppUtils.getString(context,R.string.notificationLife));
            } else {
                ttsUtils.startSpeech(AppUtils.getString(context,R.string.notificationNoLife));
            }
            sendCast();

            //过滤弹幕
        } else if (gestureId == GESTURE_SWIPE_DOWN_AND_RIGHT) {
            AppUtils.isDanMu = !AppUtils.isDanMu;
            if (AppUtils.isDanMu) {
                ttsUtils.startSpeech(AppUtils.getString(context,R.string.notificationRight));
            } else {
                ttsUtils.startSpeech(AppUtils.getString(context,R.string.notificationNoRight));
            }
            sendCast();
        } else {
            ttsUtils.startSpeech(AppUtils.getString(context,R.string.notificationError));
        }
        return true;
    }
    
    /**
     * 通知mainActivity 更改Checkout的选中
     */
    private void sendCast() {
        Intent intent = new Intent(this, MainActivity.MyGuangBo.class);
        intent.putExtra("img", AppUtils.isSaveImg);
        intent.putExtra("danmu", AppUtils.isDanMu);
        sendBroadcast(intent);
    }

   /**
     * 通知MediaProjrctService的开始截屏和停止截屏
     * @param action
     */
    private void sndTimerCast(boolean action) {
        AppUtils.correctSuccess = false;
        AppUtils.correctNum = 0;

        Intent intent = new Intent();
        intent.setAction("ssjAction");
        intent.putExtra("isAction", action);
        sendBroadcast(intent);
    }

复制代码
  • 手势氛围三类
  • 1:下滑后上滑 (开始执行识别字幕)
  • 2:下滑后左滑 (打开保存的识别图片)
  • 3:下滑后右滑 (打开过滤视频弹幕)

七,工具类

  • 所有的工具都在这
public class AppUtils {

    /*ocr的asset加载文件*/
    public static final String assetModelDirPath = "models/ocr_v2_for_cpu";
    public static final String assetlabelFilePath = "labels/ppocr_keys_v1.txt";
    public static final int PERMISSION_CODE = 100;

    //是否保存识别的图片或者识别弹幕
    public static boolean isSaveImg = false;
    public static boolean isDanMu = false;

    public static boolean correctSuccess = false;
    public static int correctNum = 0;

    private static MediaProjection mediaProjection;
    @SuppressLint("StaticFieldLeak")
    private static Activity activity;

    public static MediaProjection getMediaProjection() {
        return mediaProjection;
    }

    public static void setMediaProjection(MediaProjection mediaProjection) {
        AppUtils.mediaProjection = mediaProjection;
    }

    public static Activity getActivity() {
        return activity;
    }

    public static void setActivity(Activity activity) {
        AppUtils.activity = activity;
    }

    public static String getString(Context context, int id){
        return context.getString(id);
    }

    /**
     * 判断是否开启
     * @return
     */
    public static boolean isAccessibilitySettingsOn(Context context, String className) {
        if (context == null) {
            return false;
        }
        ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        if (activityManager != null) {
            List<ActivityManager.RunningServiceInfo> runningServices =
                    activityManager.getRunningServices(100);// 获取正在运行的服务列表
            if (runningServices.size() < 0) {
                return false;
            }
            for (int i = 0; i < runningServices.size(); i++) {
                ComponentName service = runningServices.get(i).service;
                if (service.getClassName().equals(className)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 跳转到无障碍设置页面
     */
    public static void goToSettingPage(Context context) {
        Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
        context.startActivity(intent);
    }

    public static void saveBitmap2file(Bitmap bmp, Context context, String num, String sdPath) {
        String savePath;
        String fileName = num + ".JPEG";
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            savePath = Environment.getExternalStorageDirectory().getPath() + sdPath;
        } else {
            Toast.makeText(context, "保存失败!", Toast.LENGTH_SHORT).show();
            return ;
        }
        File filePic = new File(savePath + fileName);
        try {
            if (!filePic.exists()) {
                filePic.getParentFile().mkdirs();
                filePic.createNewFile();
            }
            FileOutputStream fos = new FileOutputStream(filePic);
            bmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            fos.flush();
            fos.close();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        // 最后通知图库更新
        context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + savePath+fileName)));
    }



    /**
     * 裁剪一定高度保留下面
     *
     * @param srcBitmap  需要裁剪的bitmap
     * @param needHeight 需要裁剪出来的高
     */
    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);
    }


    public static Bitmap cropBitmapCustom(Bitmap srcBitmap, int firstPixelX, int firstPixelY, int needWidth, int needHeight) {

        if(firstPixelX + needWidth > srcBitmap.getWidth()){
            needWidth = srcBitmap.getWidth() - firstPixelX;
        }

        if(firstPixelY + needHeight > srcBitmap.getHeight()){
            needHeight = srcBitmap.getHeight() - firstPixelY;
        }

        /**裁剪关键步骤*/
        Bitmap cropBitmap = Bitmap.createBitmap(srcBitmap, firstPixelX, firstPixelY, needWidth, needHeight);
        return cropBitmap;
    }

    /**
     * 获取的是屏幕
     *
     * @param context
     */
    private static DisplayMetrics getDisplayMetrics(Context context) {
        DisplayMetrics metrics = new DisplayMetrics();
        getDispaly(context).getMetrics(metrics);
        return metrics;
    }

    /**
     * 获取屏幕管理器
     *
     * @param context
     */
    private static Display getDispaly(Context context) {
        WindowManager systemService =
                (WindowManager) (context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE));
        return systemService.getDefaultDisplay();
    }

    /**
     * 获取屏幕宽度
     *
     * @param context
     */
    public static int getScreenW(Context context) {
        return getDisplayMetrics(context).widthPixels;
    }

    /**
     * 获取屏幕高度
     * 不包含顶部状态栏和底部navigationBar的高度
     *
     * @param context
     */
    public static int getScreenH(Context context) {
        return getDisplayMetrics(context).heightPixels;
    }

    /**
     * 屏幕旋转角度
     * 如果屏幕旋转90°或者270°是判断为横屏
     */
    public static int isH(Context context) {
        return ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
    }

    /**
     * 获取字符串相等的个数
     *
     * @param s1
     * @param s2
     * @return
     */
    public static boolean getEquals(String s1, String s2) {
        if (isEmpty(s1) && isEmpty(s2)) {
            int percent = getPercent(s1, s2);
            return percent <= 50;
        }else {
            return false;
        }
    }

    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) {
        return s == null && s.equals("");
    }

    /**
     * 判断是否包含特殊字符
     * @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();
    }

    /**
     * 表示开头是英文
     *
     * @param s
     * @return
     */
    public static boolean isFristAZ(String s) {
        String frist = s.substring(0, 1);
        boolean matches = frist.matches("[a-zA-Z]+");
        return matches;
    }


    /**
     * 表示开头是数字
     *
     * @param s
     * @return
     */
    public static boolean isFristNum(String s) {
        String frist = s.substring(0, 1);
        return frist.matches("[0-9]+");
    }


    /**
     * @param s1  ocr返回的数据
     * @param s2  表示与第二次ocr返回的数据 对比的 也就是第一次的ocr的数据
     * @return
     */
    public static boolean isSmooth(String s1, String s2) {
        if ( !isFristNum(s1)  && !isFristAZ(s1) &&  !getEquals(s1,s2)  && !isSpecialChar(s1)  && !s1.equals(s2)) {
            return true;
        }else {
            return false;
        }
    }
}

复制代码

八,MainActivity的实现

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private MediaProjectionManager systemService;
    private TtsUtils ttsUtils;
    @SuppressLint("StaticFieldLeak")
    private static Switch saveImg;
    @SuppressLint("StaticFieldLeak")
    private static Switch danmu;
    private final String[] permissions = new String[]{READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE};
    private Context context;

    /**
     * 自定义广播接收 手势操作开关
     */
    public static class MyGuangBo extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            boolean isImg = intent.getBooleanExtra("img", false);
            boolean isDanMu = intent.getBooleanExtra("danmu", false);
            setCheckOut(isImg, isDanMu);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context = getApplicationContext();
        getPermission();
        initView();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.toolbar_menu, menu);
        return true;
    }


    @SuppressLint("NonConstantResourceId")
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        if (item.getItemId() == R.id.helper) {
            ttsUtils.startSpeech(AppUtils.getString(context, R.string.usehelpr));
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("帮助");
            builder.setMessage(AppUtils.getString(context, R.string.usehelpr));
            builder.setPositiveButton("确定", null);
            builder.show();
        }
        return super.onOptionsItemSelected(item);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    private void initView() {
        Button bu_runder1 = findViewById(R.id.bu_runder1);
        Button bu_runder2 = findViewById(R.id.bu_runder2);
        Toolbar toolbar = findViewById(R.id.toolbar);
        bu_runder1.setOnClickListener(this);
        bu_runder2.setOnClickListener(this);
        ttsUtils = TtsUtils.initSpeech(this);
        setSupportActionBar(toolbar);
        saveImg = (Switch) findViewById(R.id.save_img);
        danmu = (Switch) findViewById(R.id.danmu);

        saveImg.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                AppUtils.isSaveImg = isChecked;
                if (getPermission()) {
                    ttsUtils.startSpeech("暂时没有获取权限");
                    return;
                }
                if (isChecked) {
                    ttsUtils.startSpeech(AppUtils.getString(context,R.string.notificationLife));
                } else {
                    ttsUtils.startSpeech(AppUtils.getString(context,R.string.notificationNoLife));
                }
            }
        });

        danmu.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                AppUtils.isDanMu = isChecked;
                if (isChecked) {
                    ttsUtils.startSpeech(AppUtils.getString(context,R.string.notificationRight));
                } else {
                    ttsUtils.startSpeech(AppUtils.getString(context,R.string.notificationNoRight));
                }
            }
        });
    }


    @Override
    protected void onResume() {
        setCheckOut(AppUtils.isSaveImg, AppUtils.isDanMu);
        super.onResume();
    }

    /**
     * 设置开关的选中状态
     */
    private static void setCheckOut(boolean isImg, boolean isDanMu) {
        if (saveImg == null || danmu == null) {
            return;
        }
        saveImg.setChecked(isImg);
        danmu.setChecked(isDanMu);
    }

    /**
     * 检查无障碍服务是否开启
     */
    @SuppressLint("NonConstantResourceId")
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.bu_runder1:
                if (systemService == null) {
                    initProject();
                } else {
                    ttsUtils.startSpeech(AppUtils.getString(context,R.string.screenCatOnInit));
                }
                break;
            case R.id.bu_runder2:
                initAccess();
                break;
        }
    }

    /**
     * 获取截屏服务
     */
    public void initProject() {
        systemService = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
        Intent screenCaptureIntent = systemService.createScreenCaptureIntent();
        startActivityForResult(screenCaptureIntent, AppUtils.PERMISSION_CODE);
    }

    /**
     * 无障碍判断是否已经打开
     */
    private void initAccess() {
        boolean accessibilitySettingsOn = AppUtils.isAccessibilitySettingsOn(MainActivity.this, AccessibilityServices.class.getName());
        if (!accessibilitySettingsOn) {
            AppUtils.goToSettingPage(MainActivity.this);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode != AppUtils.PERMISSION_CODE) {
            return;
        }
        if (resultCode != RESULT_OK) {
            return;
        }
        MediaProjection mediaProjection = systemService.getMediaProjection(resultCode, data);
        if (mediaProjection != null) {
            Intent intent = new Intent(this, MediaProjrctService.class);
            AppUtils.setActivity(this);
            AppUtils.setMediaProjection(mediaProjection);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                startForegroundService(intent);
            } else {
                startService(intent);
            }
            ttsUtils.startSpeech(AppUtils.getString(context,R.string.screenCatTextSussce));
        } else {
            ttsUtils.startSpeech(AppUtils.getString(context,R.string.screenCatInitError));
        }
    }

    private boolean getPermission() {
        int READ = ContextCompat.checkSelfPermission(getApplicationContext(), permissions[0]);
        int WRIT = ContextCompat.checkSelfPermission(getApplicationContext(), permissions[1]);
        if (READ != PackageManager.PERMISSION_GRANTED && WRIT != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, permissions, 200);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == 200 && grantResults[0] == PackageManager.PERMISSION_GRANTED && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
            ttsUtils.startSpeech(AppUtils.getString(context,R.string.pressionOk));
        } else {
            ttsUtils.startSpeech(AppUtils.getString(context,R.string.pressionNO));
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    @Override
    protected void onDestroy() {
        if (systemService != null) {
            systemService = null;
        }
        super.onDestroy();
    }
}
复制代码
  • mainActivity主要有三个地方
  • 1:获取截屏前台系统服务
  • 2:打开系统服务的辅助服务
  • 3:获取保存图片的权限
  • 4:接收无障碍服务发送的服务
总结
  • 每个类之间的通信
  • 切换线程为了防止混淆 又改变为handler
  • 在上一个版本后觉的手势操作比较符合操作。
软件架构
  • 最好的设计就是没有设计,本项目是mvc架构。主要就是服务层和数据层获取到相关内容通知Activity进行更新。
  • 没有过多的页面绘制,主要的全部在逻辑后台。纯离线方式的实时字幕识别。
使用说明
  • 1,打开软件
  • 2,点击获取两个权限
  • 3,保持后台运行
  • 4,打开相关视频app进入播放页面使用手势开始读取字幕
  • 未再横屏会提示先横屏在次操作就可以了

九,资源文件

  • 主要是怕有些人不知道代码中的意思 所以全部放出来
<resources>
    <string name="app_name">字幕播报</string>
    <string name="accessblity">字幕播报</string>
    <string name="initAgin">初始化无障碍</string>
    <string name="initScreen">初始化读屏</string>
    <string name="action">开始运行</string>
    <string name="dupingpromising">初始化读屏所需要的权限,若不允许将会不可使用</string>
    <string name="wuzhangaipromising">初始无障碍所需要的权限,若不允许将会不可使用</string>
    <string name="save_img">保存识别图片</string>
    <string name="guolv_danmu">过滤弹幕文字</string>
    <string name="window">实时悬浮窗</string>
    <string name="guolv_text">过滤满屏的弹幕信息,防止扰乱识别字幕准确。</string>
    <string name="save_text">保存识别字幕过程中的图片。</string>
    <string name="helper">帮助</string>

    <string name="pressionOk">权限获取成功</string>
    <string name="pressionNO">权限获取失败</string>

    <string name="screenReadingStart">已经开始读屏</string>
    <string name="screenReadingPaused">已进入竖屏,读屏已经暂停</string>
    <string name="screenReadingTip">当前竖屏状态,请进入视频播放页面横屏下使用读屏</string>
    <string name="screenReadingAction">已进入横屏,请使用手势开始读屏</string>
    <string name="screenCatDestroyError">服务异常退出,请检查服务权限后重新使用手势开始读屏。</string>
    <string name="notificationTitle">字幕识别服务正在运行。</string>
    <string name="notificationText">可使用手势启用字幕播报。</string>
    <string name="notificationError">手势错误。</string>
    <string name="notificationLife">打开保存识别图片。</string>
    <string name="notificationNoLife">关闭保存识别图片。</string>
    <string name="notificationRight">打开过滤弹幕。</string>
    <string name="notificationNoRight">关闭过滤弹幕。</string>
    <string name="screenCatTextNoInit">字幕服务暂时没有初始化。</string>
    <string name="screenCatTextSussce">字幕服务初始化成功。</string>
    <string name="screenCatOnInit">字幕服务已经初始化。</string>
    <string name="screenCatInitError">字幕服务初始化失败。</string>
    <string name="usehelpr">再打开无障碍功能后,在任何界面使用手势向下向左是打开保存识别图片,向下向右是过滤视频弹幕,向下向上是开始读取视频屏幕(仅限横屏使用)。</string>
</resources>
复制代码

工作心得

说一下吧:我是一个应届毕业生,因为自己是专科,所以选择了一个工资高的小公司,当初南京最高楼叫什么鹰的工作我没去,那时开的工资5000虽然低但是公司特别大,我现在后悔没有去,去年导致我在小公司被裁员。今年我又踏入新的小公司,虽说工资高但是需求改变的是真快,我的网名就是因为这改的,还有一些无理的需求,代码是2015的耦合度高的离谱。这个项目就是公司要求做的 ,至今我无法检测到弹幕的位置,字幕我耗费了好久解决了,经理和我说:这个简单会很快,我想说对于我这样的没接触过NDK,也没接触过AI的android开发来说确实“很快” 两个星期写好,性能很差,后台一秒钟截一次图,然后交给NDK操作,这不耗费手机性能耗费什么,华为荣耀30我测试的时候卡关机好多次 ,而且短时间内测试不出来效果,测试就是一个很浪费时间的问题,开始和我说识别视频字幕,后来又和我说识别美国大片英文字幕,好离谱。没有一个固定的需求,需求很乱,后来又说你慢。下次宁愿饿死街头也要找个需求稳定差不多的公司。 我不管谁看到,我也不管你是我领导还是经理,我什么都不怕,能耐就开我。

说到经理,我们公司没有产品经理,需求经理,总是靠嘴,如果靠嘴可以的话,我CV工程师可以靠嘴吗?


           ** 选对,比选的好重要,可以节省好多弯路,甚至时间。**
复制代码

猜你喜欢

转载自juejin.im/post/7111209192551088136