IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 基于飞浆paddle的Android硬字幕提取 -> 正文阅读

[移动开发]基于飞浆paddle的Android硬字幕提取

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

介绍

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

需要自动化执行的可以去看我另一篇文章,主要代码无区别,这个主要是为了更好的简单操作但是又不想适配所以增加了一个操作手势进行打开操作。

下面是演示视频:

  • 基于paddle的文字互转语音

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

  • 一视频播放页面无效文字太多,比如左上角影名,右下角广告,右上角动态广告并且这些不是固定的也就是说有的视频有有的没有。
  • 二视频字幕不固定,比如:综艺节目演员字幕在左下角,导演字幕在中间,还有后期剪辑上去的歪歪扭扭的字幕,电影在中间,还有在上部分屏幕中间,还有在视频右下角的不是字幕的版本号
  • 三如何去除无效文字,我们知道无效文字的位置但是机器不知道,并且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联系我拉你进去

点击我第一篇文字识别方式

一,对接PaddleLite编译相关文件

1,下载paddle官网的相关demo

官网文档
https://github.com/PaddlePaddle/PaddleOCR

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 abstract class TimerAction {

    abstract void initTimer();

    abstract void starDefaultTimer();

    abstract void stopDefaultTimer();

    abstract void setTimer(long delay,long period);

}
  • 这里由于我后期需要和主包进行合并,所以我把相关函数抽象出来,交给子类去实现。
  • 具体的实现
    /**
     * 开始执行默认任务
     */
    @Override
    public void starDefaultTimer() {
        run();
        initTimer();
        timers.schedule(timerTask, timerDelay == 0 ? 1000 : timerDelay, timerPeriod == 0 ? 1000 : timerPeriod);
    }

    /**
     * 一旦销毁 所有的就结束
     */
    @Override
    public void stopDefaultTimer() {
        if (timers != null) {
            timers.purge();
            timers.cancel();
            timers = null;
        }
        if (timerTask != null) {
            timerTask.cancel();
            timerTask = null;
        }
    }
  • 这是子类实现的部分方法,主要就是开启定时器执行下一步骤的截屏
四,截屏的功能实现
  • 截屏的相关接口

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

    • 由于java基础薄弱,所以采用了单例模式进行封装
    • 主要对外开放的就是,开始截屏,和结束截屏
    • ImageReader.newInstance(screenW, screenH, 0x1, 1)
    • 主要是这个:这个主要是创建想要获取的屏幕大小,后面两个都一样不用多操心
    /**
     * 交给actvity 在mainactivity中的相同重载的方法中使用
     */
    @RequiresApi(api = Build.VERSION_CODES.M)
    public void onActivityReust(int requestCode, int resultCode, Intent data) {
        if (requestCode != VariableUtils.PERMISSION_CODE) {
            return;
        }
        if (resultCode != RESULT_OK) {
            return;
        }
        mediaProjection = systemService.getMediaProjection(resultCode, data);
    }

    private int screenW;
    private int screenH;

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void startScreenCapture(Bundle bundle) {
        if (mediaProjection != null) {
            screenW = ScreenUtils.getScreenW(activity);
            screenH = ScreenUtils.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表示一个虚拟显示,显示的内容render到 createVirtualDisplay()参数的Surface。
     * 因为virtual display内容render到应用程序提供的surface,所以当进程终止时,它将会自动释放,并且所以剩余的窗口都会被强制删除。但是,你仍然需要在使用完后显式地调用release()方法。
     * 此处的 with  和 hight 表示
     * 截图的宽和高
     * 但是由于不匹配会有黑色边框  所以加入三木
     * 如果此类的mhight == hight 则直接获取屏幕的高度 否则创建的截图宽和高是匹配的 但是截取的图片是全屏  然后导致有黑色边框
     * 反之直接创建屏幕的大小
     * <p>
     * 问题场景
     * 1 竖屏状态时需要截取的是视频view的宽和高  但是surface创建的是屏幕的宽和高 所以导创建的宽和高是匹配的 但是截取的却是整个屏幕 而我们需要的是视频播放的view 所以就不符合我们需求
     * 2 横屏状态下 直接截取当前屏幕的宽和高 。
     * <p>
     * 以上会有一个问题   就是长时间  横屏或者竖屏 突然横屏或者竖屏会导致img buff缓冲区不足
     */
    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 synchronized void recognitionText(Bitmap bitmap) {
            //标记的是否为第一次,因为第一次截取屏幕获取位置
            if (isFirstOcr) {
                //isToOrBottom : true 上   /false下
                Bitmap topOrBottomBitmap = Bitmap.createBitmap(bitmap, 0, isToOrBottom ? 0 : cenScreenH, cenScreenW * 2, cenScreenH);
                predictor.setInputImage(topOrBottomBitmap);
            } else {
                cenScreenW = ScreenUtils.getScreenW(getApplicationContext()) / 2;
                cenScreenH = ScreenUtils.getScreenH(getApplicationContext()) / 2;
    
                twoCenH = cenScreenH / 2;
                twoCenW = cenScreenW / 2;
    
                predictor.setInputImage(bitmap);
            }
            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)) {
                            Log.e(TAG, "recognitionText:中心 .......");
                            //第一次进来不执行 因为isFirst为false
                            if (isFirstOcr) {
                                Log.e(TAG, "recognitionText:第二次 .......");
                                if (TextUtils.isSmooth(textResult.get(i), newtextResult)) {
                                    newtextResult = textResult.get(i);
                                    ttsUtils.startSpeech(newtextResult);
                                    Log.i(TAG, "run: " + textResult.get(i));
                                }
                                //第一次进来走这里 当获取到剧中的字幕时在获取字幕位置(上/下),最后还要读取第一次的文字
                            } else {
                                int y0 = textPotion.get(i * 4).y;
                                int y2 = textPotion.get(i * 4 + 2).y;
                                //在上半屏
                                if (y0 < cenScreenH && y2 < cenScreenH) {
                                    Log.e(TAG, "recognitionText:在上半屏 .......");
                                    isToOrBottom = true;
                                    //在下半屏
                                } else if (y0 > cenScreenH && y2 > cenScreenH) {
                                    Log.e(TAG, "recognitionText:在下半屏 .......");
                                    isToOrBottom = false;
                                } else {
                                    Log.e(TAG, "recognitionText: 在屏幕中心区域不处理.......");
                                }
                                if (TextUtils.isSmooth(textResult.get(i), newtextResult)) {
                                    newtextResult = textResult.get(i);
                                    ttsUtils.startSpeech(newtextResult);
                                    isFirstOcr = true;
                                    Log.i(TAG, "run: " + textResult.get(i));
                                }
                            }
                        }
                    }
                } else {
                    Log.i(TAG, "handleMessage: 转换文字失败");
                }
            }
        }
  • 这里我写的很清楚了
  • ScreenUtils.getScreenW(getApplicationContext())
  • 获取屏幕宽度
  • predictor.getTextResult()
  • 获取OCR返回文字组,是个集合
  • predictor.getTextPotion()
  • 获取OCR返回的文字坐标,也是个Potion集合,每个potion是四个点的坐标。
  • ttsUtils.startSpeech(textResult.get(i));
  • 读取全部返回文字
  • TextUtils.isSmooth(textResult.get(i), newtextResult)
  • 判断返回字符是否符合逻辑
  • 由于是字幕 这里我判断了是否剧中

总结

  • 每个类之间的通信/线程通信/切换线程用的是EVENTBUS
  • 在上一个版本后觉的手势操作比较符合操作。

软件架构

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

使用说明

  • 1,打开软件
  • 2,点击获取两个权限
  • 3,保持后台运行
  • 4,打开相关视频app进入播放页面使用手势开始读取字幕
  • 未再横屏会提示先横屏在次操作就可以了

参与贡献

如果您需要,可在此项目上加入实时翻译功能。

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-04-04 12:22:54  更:2022-04-04 12:27:11 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 20:57:09-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码