概述:在Vue中调用摄像头拍照进行银行卡识别。

流程:vue中调用媒体摄像头(无前后摄像头切换功能)——>根据规定的框(前端自己定制)放置银行卡——>将识别的图片通过接口传到后台,后台识别出返回给前端银行卡的相关数据。

原理:调用媒体摄像头,应用的是navigator.mediaDevices.getUserMedia功能(API网站)调用摄像头拍摄视频流,将录像流放到video中,再绘制成canvas图像,将此刻的canvas图像生成base64位图片返回并存放到image中(存放到image中后,后续暂时没有用到,暂时只做存放用),定时将此刻返回的图片通过接口传输到后台识别,直到识别成功。

步骤:

注:代码为手打,已检查,可能还会存在单词拼写方面的问题,如报错,请检测拼写。

一. 调用媒体对象打开摄像头拍摄的相关函数封装(参考了网上的一些方法,之后查看了官网进行了优化)

新建js文件:Camera.js

/**
* 组件: 调用摄像头拍摄的构造函数
* @params {Object} options 参数如下:
*             video {DOM} video元素
*             width {Number} 画布宽度
*             height {Number} 画布高度
*             onShoot {Function} 录像回调函数
*             onError {Function} error回调函数
*         {Object} canvasOption 参数如下:
*             marginTop {Number} 银行卡可视区域距离屏幕顶部的距离,用作画布坐标
*             marginLeft {Number} 银行卡可视区域距离屏幕左侧的距离,用作画布坐标
*             width {Number} 银行卡可视区域宽度
*             height {Number} 银行卡可视区域高度
*调用:
*    Camera.create(options, canvasOption)
*/
function Camera(options, canvasOption) {
    this.video = options.video;
    this.width = options.width || 640;
    this.height = options.height || 480;
    this.shadeMarginTop = canvasOption.marginTop;
    this.shadeMarginLeft = canvasOption.marginLeft;
    this.shadeWidth = canvasOption.width;
    this.shadeHeight = canvasOption.height;
    this.onError = options.onError;
    this.onShoot = options.onShoot;
    this.mediaStreamTrack = null; // 存放视频流
}

Camera.prototype = {
    init: function() {
        if(navigator.mediaDevices === undefined) {
            navigator.mediaDevices = {}
        }
        // 一些浏览器部分支持 mediaDevices。 我们不能直接给对象设置 getUserMedia
        // 因为这样可能会覆盖已有的属性。这里我们只会在没有getUserMedia属性的时候添加它。
        if (navigator.mediaDevices.getUserMedia === undefined) {
            navigator.mediaDevices.getUserMedia = function(constraints) {
                // 首先,如果有getUserMedia的话,就获取它
                var getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
                
                // 一些浏览器根本没实现它 — 那么就返回一个error到promise的reject来保持一个统一的接口
                if (!getUserMedia) {
                    return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
                }
            
                // 否则,为老的navigator.getUserMedia方法包裹一个Promise
                return new Promise(function(resolve, reject) {
                    getUserMedia.call(navigator, constraints, resolve, reject);
                });
            }
        }
        this.video.autoplay = 'true';
        this.canvasDom = document.createElement('canvas');
        this.canvasDom.width = this.width; 
        this.canvasDom.height = this.height;
        this.canvasDom.style.display = 'none';
        document.querySelector('body').appendChild(this.canvasDom);
    },
    
    // 检查摄像头是否可用
    canCameraUse: function() {
        return (navigator.mediaDevices.getUserMedia && window.URL);
    },

    // 获取录像流到video中
    shoot: function() {
        var self = this;
        var video = self.video;
        if (self.canCameraUse) {
            navigator.mediaDevices.getUserMedia({
                video: { facingMode: { exact: "environment"} } // 后置摄像头设置
            })
            .then(function(stream) {
                self.mediaStreamTrack = stream
                // 旧的浏览器可能没有srcObject
                if ("srcObject" in video) {
                    video.srcObject = stream;
                }else {
                    // 防止在新的浏览器里使用它,因为它已经不再支持了
                    video.src = window.URL.createObjectURL(stream);
                }
                video.onloadedmetadata = function(e) {
                    video.play();
                };
                video.addEventListener('error', function(error) {
                    stream.stop();
                    self.onError && self.onError(error);
                }, false);
            })
            .catch(function(err) {
                self.onError && self.onError(err)
            })
        }
    },
    
    // 将录像绘制到canvas
    drawVideo: function() {
        var canvasDom = this.canvasDom;
        var ctx = canvasDom.getContext('2d');
        ctx.fillStyle = "#000000";
        // 视频可能不会充满屏幕,这时候,需要按照视频缩放的比例来剪切所需要的部分
        let videoHeight = this.video.videoHeight // 视频本身的高度
        let videoWidth = this.video.videoWidth // 视频本身的宽度
        let videoWidthScale = 1
        if (videoWidth && this.width !== videoWidth) { // 计算当前视频的缩放比例
            videoWidthScale = videoWidth / this.width
        }
        let newHeight = videoHeight / videoWidthScale // 当前显示的视频高度
        let videoMarginTop = this.video.clientHeight - newHeight > 0 ?
                             (this.video.clientHeight - newHeight) / 2 : 0 // 当前视频距离顶部高度
        let x = Math.floor(this.shadeMarginLeft * videoWidthScale) // 计算剪切视频的x坐标
        let y = Math.floor((this.shadeMarginTop - videoMarginTop) * videoWidthScale) // 计算剪切视频的y坐标
        let w = videoWidth - x * 2, h = Math.ceil(videoHeight - y * 2) // 计算剪切视频的宽高
        ctx.fillRect(this.shadeMarginLeft, this.shadeMarginTop, this.shadeWidth, this.shadeHeight); // 填充画布
        ctx.drawImage(this.video, x, y, w, h, this.shadeMarginLeft, this.shadeMarginTop, this.shadeWidth, this.shadeHeight) // 剪切视频并绘制成图片
        this.ctx = ctx
    },
    
    // 录像事件绑定
    addShootEvent: function() {
        let self = this;
        let video = self.video;
        // 正在录像
        video.addEventListener('timeupdate', function() {
            self.drawVideo();
            self.onShoot && self.onShoot()
        }, false)
    },

    // 将录像成图片
    snapshot: function(cb, imageType) {
        let self = this
        let canvas = self.canvasDom;
        imageType = imageType || 'jpeg';
        let imageSrc = canvas.toDataURL('image/' + imageType);
        cb && cb(imageSrc);
    },

    // 开始录像
    play: function() {
        this.video.play();
    },

    // 停止录像
    pause: function() {
        this.video.pause();
        let self = this
        // 关闭摄像头
        self.mediaStreamTrack.getVideoTracks().forEach(function(track) {
            track.stop();
            self.ctx.clearRect(0, 0, self.canvasDom.width, self.canvasDom.height); // 清除画布
        });
    },
    
    render: function() { // 初始化
        this.init();
        this.shoot();
        this.drawVideo();
        this.addShootEvent();
    }
    
};

Camera.create = function(options, canvasOption) {
    var camera = new Camera(options, canvasOption.canvasStyle);
    camera.render();
    return camera;
};

export default Camera

二. 在Vue中写识别银行卡界面:

1. HTML(img为触发图片,点击img触发银行卡识别来调用摄像头):

<img src=" ./assets/scan.svg" @click="scanBank" />

<!-- 扫描界面html -->
<div class="camera" v-show="isScan">
    <!-- 遮罩层 -->
    <canvas class="shade" id="canvasMaskShade" ref="videoShade"></canvas>
    <!-- 视频存放处 -->
    <video id="video" class="video"></video>
    <!-- 取消识别按钮 -->
    <button id="shootBtn" @click="cancelScan" class="shootBtn"><span>取消</span></button>
    <!-- 存放扫描的图片 -->
    <div id="imageBox"></div>
</div>

2. 样式(Less,层级(z-index)一定要注意):

.camera {
    position: relative;
    width: 100%;
    height: 100%;
    .shade {
        position: absolute;
        top: 0;
        left: 0;
        box-sizing: border-box;
        width: 100%;
        height: 100%;
        z-index: 10;
    }
    .video {
        position: absolute;
        top: 0;
        left: 0;
        height: 100%;
        width: 100%;
        z-index: 5;
        background: #000;
    }
    .shootBtn {
        position: absolute;
        top: 0.4rem;
        left: 0;
        z-index: 15;
        height: 0.6rem;
        padding-right: 0.2rem;
        background: rgba(255, 255, 255, 0.3);
        border-top-right-radius: 0.1rem;
        border-bottom-right-radius: 0.1rem;
        font-size: 0.32rem;
        color: #fff;
        text-align: center;
        display: flex;
        align-item: center;   
    }
}

 3. JS:

import Camera from './camera.js';

data() {
    return {
        camera: null,
        isScan: false,
        isDrawCanvas: false,
        takePictureInterval: null,
        bankNumber: '',
    }
},

methods: {
    /**
    * 点击扫描按钮触发函数,调用摄像头插件开启摄像头
    */
    scanBank() {
        this.isScan = !this.isScan // 显示银行卡识别的界面
        this.$nextTick(() => { // 渲染完成后执行
            if(this.isScan && !this.isDrawCanvas) {
                this.drawBankMask() // 如果没有画遮罩,将遮罩画出来
            }
            let canvasMask = document.getElementById("canvasMaskShade")
            let self = this
            // 调用插件开启摄像头,两个参数,第一个参数是video数据,第二个是遮罩层数据,用于视频剪切
            self.camera = Camera.create({
                video: document.querySelector('#video'),
                width: canvasMask.clientWidth,
                height: canvasMask.clientHeight,
                onError: function(error) {
                    console.log("调用扫描失败")
                    self.isScan = !self.isScan //调用失败后关闭识别页面
                }
            },{
                canvasStyle: {
                    "width": canvasMask.bankWidth,
                    "height": canvasMask.bankHeight,
                    "marginTop": canvasMask.bankMarginTop,
                    "marginLeft": canvasMask.bankMarginLeft
                }
            })
        })
        // 设置定时器,定时将此刻获取的图片传输到后台进行识别银行卡来完成自动识别功能,
        // 定时器将一直执行,直到用户取消识别或银行卡识别成功后,将定时器清除;
        // 如果调用摄像头失败或开始识别失败,则会清除定时器
        clearInterval(this.takePictureInterval)
        this.takePictureInterval = setInterval(() => {
            this.takePicture()
        }, 3000)
    },
    /**
    * 绘制遮罩层
    */
    drawBankMask() {
        let canvasMask = document.getElementById('canvasMaskShade')
        
        let screenWidth = canvasMask.clientWidth // 获得可视区域宽度
        let screenHeight = canvasMask.clientHeight // 获得可视区域高度
        canvasMask.width = screenWidth // 设置遮罩的width属性
        canvasMask.height = screenHeight // 设置遮罩的height属性
        let bankWidth = screenWidth - 40 // 设置银行卡框的宽度,规定距离左右屏幕边缘20px
        let bankHeight = Math.ceil(bankWidth / 1.6) // 根据银行卡宽高比设置银行卡框的高度,取整数
        let otherHeight = (screenHeight - bankHeight) / 2 // 设置银行卡框在屏幕中间位置,故这样计算其距离屏幕顶部的高度
        let bankMarginTop = Math.floor(otherHeight) // 设置银行卡框距离顶部距离,取整数
        canvasMask.bankMarginTop = bankMarginTop // 添加银行卡框距离屏幕上下两侧距离属性,用作camera插件参数:将视频流转换成canvas时的画布剪切y坐标计算参数
        canvasMask.bankMarginLeft = 20 // 添加银行卡框距离屏幕左右两侧距离属性,用作camera插件参数:将视频流转换成canvas时的画布剪切x坐标计算参数
        canvasMask.bankWidth = bankWidth // 添加银行卡框宽度属性,用作camera插件参数:将视频流转换成canvas时画布剪切宽度的计算参数
        canvasMask.bankHeight = bankHeight // 添加银行卡框高度属性,用作camera插件参数:将视频流转换成canvas时画布剪切高度的计算参数
        
        // 开始画遮罩层
        let ctx = canvasMask.getContext('2d');
        ctx.fillStyle = "rgba(51, 51, 51, 0.5)";
        ctx.fillRect(0, 0, screenWidth, screenHeight)
        
        // 银行卡框边框常量,画一个银行卡边框,将银行卡框凸显出来
        const lineWidth = 4
        const lineColor = "#DCDCDC"
        
        // 银行卡框上边框绘制
        ctx.beginPath();
        ctx.moveTo(17, bankMarginTop - 2);
        ctx.lineTo(bankWidth + 23, bankMarginTop - 2);
        ctx.closePath();
        ctx.lineWidth = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.stroke();

        // 银行卡框左边框绘制
        ctx.beginPath();
        ctx.moveTo(18, bankMarginTop - 3);
        ctx.lineTo(18, bankMarginTop + bankHeight + 3);
        ctx.closePath();
        ctx.lineWidth = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.stroke();

        // 银行卡框下边框绘制
        ctx.beginPath();
        ctx.moveTo(17, bankMarginTop + bankHeight + 2);
        ctx.lineTo(bankWidth + 23, bankMarginTop + bankHeight + 2);
        ctx.closePath();
        ctx.lineWidth = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.stroke();

        // 银行卡框右边框绘制
        ctx.beginPath();
        ctx.moveTo(bankWidth + 22, bankMarginTop - 3);
        ctx.lineTo(bankWidth + 22, bankMarginTop + bankHeight + 3);
        ctx.closePath();
        ctx.lineWidth = lineWidth;
        ctx.strokeStyle = lineColor;
        ctx.stroke();

        ctx.clearRect(20, bankMarginTop, bankWidth, bankHeight) // 清除银行卡框的遮罩,展示出银行卡应放的位置
        this.isDrawCanvas = true
    },
    /**
    * 将此刻得到的图片传输到后台进行银行卡验证
    */
    takePicture() {
        let self = this
        if(!self.isScan) { // 如果没有展示扫描页面或者调用扫描失败,则清除定时任务
            clearInterval(self.takePictureInterval)
            return
        }
        self.camera.snapshot(function(imageUrl) {
            let imageBox = document.querySelector('#imageBox');
            let image = imageBox.querySelector('img');
            if (!image) {
                image = document.createElement('img');
                image.src = imageUrl;
                document.querySelector('#imageBox').appendChild(image)
            }else {
                image.src = imageUrl;
            }
            // 请求接口识别
            let params = { // 接口参数
                data: image.src,
                type: 'jpeg'
            }
            this.axios.post(...).then( res => {
                // 如果请求成功,将定时器清除,关闭摄像头,关闭银行卡识别界面
                self.bankNumber = res.data.number // 将识别的银行卡号码赋值,假设识别出来的银行卡信息存放在data的number中
                clearInterval(self.takePictureInterval);
                self.camera.puse
                self.$nextTick(() => {
                    self.isScan = false
                })
            })
        })
    },
    /**
    * 手动取消银行卡识别
    */
    cancelScan() {
        this.camera.pause() // 关闭摄像头
        clearInterval(this.takePictureInterval) // 清除定时器
        this.isScan = false // 关闭银行卡识别页面
    },
}

银行卡识别效果图:

银行卡识别界面

完。

Logo

前往低代码交流专区

更多推荐