Android 使用Camera1实现相机预览、拍照、录像

2023-09-17 22:27:48

1. 前言

本文介绍如何从零开始,在Android中实现Camera1的接入,并在文末提供Camera1Manager工具类,可以用于快速接入Camera1
Android Camera1 API虽然已经被Google废弃,但有些场景下不得不使用。
并且Camera1返回的帧数据是NV21,不像Camera2CameraX那样,需要自己再转一层,才能得到NV21
Camera1的API调用也比Camera2简单不少,和CameraX的简单程度差不多,所以在一定的场景下,Camera1还是有其用途的。

2. 前置操作

2.1 添加权限

AndroidManifest中添加如下权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

2.2 申请权限

别忘了申请权限

ActivityCompat.requestPermissions(
    this@WelComeActivity,
    arrayOf(
        android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
        android.Manifest.permission.RECORD_AUDIO,
        android.Manifest.permission.CAMERA
    ),
    123
)

2.3 声明XML布局

新建一个Activity,在其XML中声明SurfaceView

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
	xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:background="@color/black"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintDimensionRatio="9:16"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

3. 实现预览功能

3.1 添加SurfaceView的回调

binding.surfaceView.holder.addCallback(surfaceCallback)

private var surfaceCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
	// Surface创建时
    override fun surfaceCreated(holder: SurfaceHolder) {
    }

	// Surface改变时
    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
    }

	// Surface销毁时
    override fun surfaceDestroyed(holder: SurfaceHolder) {
    }
}

3.2 打开相机

Surface创建时,也就是在surfaceCreated的时候,打开相机

private var camera: Camera? = null
private fun openCamera(holder: SurfaceHolder) {
    try {
        camera = Camera.open(cameraId)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

3.3 开始预览

当我们打开相机后,就可以开始预览了
这里首先将设置camera1预览的尺寸,一般来说,通过camera!!.parameters.supportedPreviewSizes获取到的列表中,第一项就是最推荐的尺寸了。

private fun setPreviewSize() {
    //获取摄像头支持的宽、高
    val supportedPreviewSizes: List<Camera.Size> = camera!!.parameters.supportedPreviewSizes
    supportedPreviewSizes.forEach {
        Log.i("ZZZZ", "${it.width}*${it.height}")
    }
    val parameters = camera?.parameters
    val size = supportedPreviewSizes[0]
    parameters?.setPreviewSize(size.width, size.height)
    camera?.setParameters(parameters)
}

接着,将SurfaceHolder设置到camera中。setPreviewDisplay接受一个SurfaceHolder对象作为参数,该对象表示预览显示的表面。通过调用setPreviewDisplay方法,可以将相机的预览数据输出到指定的表面对象上,从而在预览界面中显示出相机的拍摄画面。

camera?.setPreviewDisplay(holder)

接着调用setDisplayOrientation方法来设置相机的预览方向。该方法接受一个参数,即预览方向的度数。例如,如果要在竖直模式下使用相机,而默认的预览方向是水平的,那么就可以通过调用setDisplayOrientation方法将预览方向顺时针旋转90度。

camera?.setDisplayOrientation(90)

最后,调用startPreview()就可以启动相机的预览了

camera?.startPreview()

来看一下完整代码

private fun startPreview(holder: SurfaceHolder) {
    try {
        setPreviewSize()
        camera?.setPreviewDisplay(holder)
        camera?.setDisplayOrientation(90)
        camera?.startPreview()
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

3.4 效果如下

4. 实现拍照功能

4.1 调用拍照接口

要进行拍照,调用camera.takePicture即可,它共有3个回调参数

  • ShutterCallback shutter(捕获图片瞬间的回调):快门回调是在拍照时快门按下的瞬间调用的回调。它允许您在拍照时执行一些自定义操作,例如触发闪光灯或显示自定义的拍照界面。
  • PictureCallback raw(原始图像数据回调):原始图像数据回调是在拍照后,获取到原始未压缩的数据时调用的回调。您可以在这个回调中对图像数据进行处理或保存。
  • PictureCallback jpeg(JPEG图像数据回调):JPEG图像数据回调是在拍照后,获取到图像的JPEG格式数据时调用的回调。您可以在这个回调中对JPEG图像数据进行处理或保存。

这里我们只需要用到jpeg回调

private val threadPool = Executors.newCachedThreadPool()

binding.btnTakePicture.setOnClickListener {
    camera?.takePicture(
    	null,null,{ data, camera ->
            //jpeg回调
        })
}

4.2 在jpeg回调中保存图片

//MediaFileUtils类详见本文附录
val pictureFile: File = MediaFileUtils.getOutputMediaFile(MEDIA_TYPE_IMAGE)!!
try {
    val fos = FileOutputStream(pictureFile)
    fos.write(data)
    fos.close()
} catch (e: FileNotFoundException) {
    Log.d(TAG, "File not found: ${e.message}")
    errorCallBack.invoke(e)
} catch (e: IOException) {
    Log.d(TAG, "Error accessing file: ${e.message}")
    errorCallBack.invoke(e)
}

来查看下效果,可以看到图片已经被保存了,但是图片的方向目前是有问题的。
在这里插入图片描述

4.3 解决图片保存的方向问题

所以,我们需要先将图片转成bitmap,旋转角度后,再保存
修改代码为如下代码

//路径示例 : /storage/emulated/0/Pictures/MyCameraApp/IMG_20230726_135652.jpg
val pictureFile: File = MediaFileUtils.getOutputMediaFile(MediaFileUtils.MEDIA_TYPE_IMAGE)!!
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
val matrix = Matrix()
matrix.postRotate(270F)
val rotatedBitmap: Bitmap = Bitmap.createBitmap(
    bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
)
//ImageUtils需要依赖 implementation 'com.blankj:utilcodex:1.31.1'
ImageUtils.save(rotatedBitmap, pictureFile, Bitmap.CompressFormat.JPEG)

来看一下效果,可以看到现在图片方向是对了,但是图片左右的内容是相反的
在这里插入图片描述

4.4 解决图片保存镜像问题

要解决图片的镜像问题,就调用一下matrix.postScale左右水平变换就好了

matrix.postScale(-1F, 1F, bitmap.width / 2F, bitmap.height / 2F)

完整代码如下

val pictureFile: File =
MediaFileUtils.getOutputMediaFile(MediaFileUtils.MEDIA_TYPE_IMAGE)!!
//路径示例 : /storage/emulated/0/Pictures/MyCameraApp/IMG_20230726_135652.jpg
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
val matrix = Matrix()
matrix.postRotate(270F)
matrix.postScale(-1F, 1F, bitmap.width / 2F, bitmap.height / 2F)
val rotatedBitmap: Bitmap = Bitmap.createBitmap(
bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
)
//ImageUtils需要依赖 implementation 'com.blankj:utilcodex:1.31.1'
ImageUtils.save(rotatedBitmap, pictureFile, Bitmap.CompressFormat.JPEG)

5. 实现录像功能

要录制视频,需要使用MediaRecorder,若要使用 Camera1 拍摄视频,需要谨慎管理 CameraMediaRecorder,并且必须按特定顺序调用相应方法。您必须遵循以下顺序,才能使您的应用正常工作:

  • 打开相机。
  • 做好准备,并开始预览(如果您的应用会显示正在录制的视频,而通常情况下都是如此)。
  • 通过调用 Camera.unlock() 解锁相机,以供 MediaRecorder 使用。
  • 通过在 MediaRecorder 上调用以下方法来配置录制:
    • 通过 setCamera(camera) 关联您的 Camera 实例。
    • 调用 setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
    • 调用 setVideoSource(MediaRecorder.VideoSource.CAMERA)
    • 调用 setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) 以设置质量。
    • 调用 setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
    • 如果您的应用提供视频预览,请调用 setPreviewDisplay(preview?.holder?.surface)
    • 调用 setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
    • 调用 setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
    • 调用 setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
    • 调用 prepare() 以完成 MediaRecorder 配置。
  • 如需开始录制,请调用 MediaRecorder.start()
  • 如需停止录制,请按以下顺序调用以下方法:
    • 调用 MediaRecorder.stop()
    • (可选)通过调用 MediaRecorder.reset() 移除当前的 MediaRecorder 配置。
    • 调用 MediaRecorder.release()
    • 通过调用 Camera.lock() 锁定相机,以便将来的 MediaRecorder 会话可以使用它。
  • 如需停止预览,请调用 Camera.stopPreview()
  • 最后,如需释放 Camera 以供其他进程使用,请调用 Camera.release()

具体可以见 Camera1 录制视频

下面直接附上代码,直接如下代码就好了

5.1 开始录制

fun startVideo(holder: SurfaceHolder) {
	mediaRecorder = MediaRecorder()
	//解锁相机,以供 MediaRecorder 使用
	camera?.unlock()
	//设置要用于视频捕获的相机
	mediaRecorder.setCamera(camera)
	//设置音频源
	mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
	//设置视频源
	mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA)
	//设置视频的输出格式和编码
	mediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))
	//设置输出视频播放的方向
	mediaRecorder.setOrientationHint(270)
	//设置输出文件
	mediaRecorder.setOutputFile(getVideoFilePath(this))
	//指定 SurfaceView 预览布局元素
	mediaRecorder.setPreviewDisplay(holder.surface)
	
	try {
	    mediaRecorder.prepare()
	} catch (e: IOException) {
	    e.printStackTrace()
	    releaseMediaRecorder()
	}
	
	Handler().postDelayed({
	    try {
	        mediaRecorder.start()
	    } catch (e: IOException) {
	        e.printStackTrace()
	        releaseMediaRecorder()
	    }
	}, 10)
}

fun getVideoFilePath(context: Context?): String {
    val filename = "VIDEO_${System.currentTimeMillis()}.mp4"
    val dir = context?.getExternalFilesDir("video")

    return "${dir!!.path}/$filename"
}

5.2 停止播放

fun stopVideo() {
    mediaRecorder.stop()
    mediaRecorder.release()
    camera?.lock()
}

5.3 释放资源

fun releaseMediaRecorder() {
    if (mediaRecorder != null) {
        mediaRecorder.reset() // 清除配置
        mediaRecorder.release()
        //mediaRecorder = null
        camera?.lock()
    }
}

6. CameraHelper工具类

可以直接使用这个工具类,来快速接入Camera1

class CameraHelper(
    private val activity: AppCompatActivity,
    private var cameraId: Int,
    private var width: Int = 720,
    private var height: Int = 1280,
) : Camera.PreviewCallback {

    private var surfaceHolder: SurfaceHolder? = null
    private var surfaceTexture: SurfaceTexture? = null
    private var mCamera: Camera? = null
    private var buffer: ByteArray? = null
    private var bytes: ByteArray? = null

    /**
     * 打开相机
     *
     * @param cameraId 后摄 Camera.CameraInfo.CAMERA_FACING_BACK
     *                 前摄 Camera.CameraInfo.CAMERA_FACING_FRONT
     */
    private fun open(cameraId: Int) {
        //获得camera对象
        mCamera = Camera.open(cameraId)

        mCamera?.let { camera ->
            //配置camera的属性
            val parameters = camera.parameters
            //设置预览数据格式为nv21
            parameters.previewFormat = ImageFormat.NV21
            //这是摄像头宽、高
            setPreviewSize(parameters!!)
            // 设置摄像头 图像传感器的角度、方向
            setPreviewOrientation(cameraId)
            camera.parameters = parameters
        }
    }

    /**
     * 切换摄像头
     */
    fun switchCamera() {
        val cameraId = if (cameraId == Camera.CameraInfo.CAMERA_FACING_BACK) {
            Camera.CameraInfo.CAMERA_FACING_FRONT
        } else {
            Camera.CameraInfo.CAMERA_FACING_BACK
        }
        switchCamera(cameraId)
    }

    /**
     * 切换摄像头
     * @param cameraId 指定摄像头ID
     */
    fun switchCamera(cameraId: Int) {
        this.cameraId = cameraId
        previewAlign()
    }

    private fun previewAlign() {
        stopPreview()
        if (surfaceHolder != null) {
            startPreview(surfaceHolder!!)
        } else {
            startPreview(surfaceTexture!!)
        }
    }

    /**
     * 停止预览
     */
    fun stopPreview() {
        if (mCamera != null) {
            mCamera?.setPreviewCallback(null)
            mCamera?.stopPreview()
            mCamera?.release()
            mCamera = null
        }
    }

    /**
     * 开始预览
     */
    fun startPreview(surfaceHolder: SurfaceHolder) {
        open(cameraId)
        this.surfaceHolder = surfaceHolder
        buffer = ByteArray(width * height * 3 / 2)
        bytes = ByteArray(buffer!!.size)
        //数据缓存区
        mCamera?.addCallbackBuffer(buffer)
        mCamera?.setPreviewCallbackWithBuffer(this)
        //设置预览画面
        mCamera?.setPreviewDisplay(surfaceHolder)
        mCamera?.startPreview()
    }

    fun startPreview(surfaceTexture: SurfaceTexture) {
        open(cameraId)
        this.surfaceTexture = surfaceTexture
        buffer = ByteArray(width * height * 3 / 2)
        bytes = ByteArray(buffer!!.size)
        //数据缓存区
        mCamera?.addCallbackBuffer(buffer)
        mCamera?.setPreviewCallbackWithBuffer(this)
        //设置预览画面
        mCamera?.setPreviewTexture(surfaceTexture)
        mCamera?.startPreview()
    }

    private val threadPool = Executors.newCachedThreadPool()

    /**
     * 拍摄照片
     */
    fun takePicture(completedCallBack: () -> Unit, errorCallBack: (Exception) -> Unit) {
        mCamera?.takePicture(null, null, object : Camera.PictureCallback {
            override fun onPictureTaken(data: ByteArray?, camera: Camera?) {
                previewAlign()

                threadPool.execute {
                    val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE)!!
                    //路径示例 : /storage/emulated/0/Pictures/MyCameraApp/IMG_20230726_135652.jpg
                    val bitmap = BitmapFactory.decodeByteArray(data, 0, data!!.size)
                    val matrix = Matrix()
                    //修正图片方向,这里只是示例,需要根据实际手机方位来决定图片角度
                    matrix.postRotate(if (cameraId==1) 270F else 90F)
                    if (cameraId==1) {
                        //postScale在矩阵变换之后进行缩放
                        matrix.postScale(-1F, 1F, bitmap.width / 2F, bitmap.height / 2F)
                    }
                    val rotatedBitmap: Bitmap = Bitmap.createBitmap(
                        bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true
                    )
                    ImageUtils.save(rotatedBitmap, pictureFile, Bitmap.CompressFormat.JPEG)
                    completedCallBack.invoke()
                }
            }
        })
    }

    override fun onPreviewFrame(data: ByteArray, camera: Camera?) {
        camera!!.addCallbackBuffer(data)
    }

    private fun setPreviewSize(parameters: Camera.Parameters) {
        //获取摄像头支持的宽、高
        val supportedPreviewSizes = parameters.supportedPreviewSizes
        var size = supportedPreviewSizes[0]
        Log.d(TAG, "Camera支持: " + size.width + "x" + size.height)
        //选择一个与设置的差距最小的支持分辨率
        var m: Int = Math.abs(size.height * size.width - width * height)
        supportedPreviewSizes.removeAt(0)
        val iterator: Iterator<Camera.Size> = supportedPreviewSizes.iterator()
        //遍历
        while (iterator.hasNext()) {
            val next = iterator.next()
            Log.d(TAG, "支持 " + next.width + "x" + next.height)
            val n: Int = Math.abs(next.height * next.width - width * height)
            if (n < m) {
                m = n
                size = next
            }
        }
        width = size.width
        height = size.height
        parameters.setPreviewSize(width, height)
        Log.d(TAG, "预览分辨率 width:" + size.width + " height:" + size.height)
    }

    private val mOnChangedSizeListener: OnChangedSizeListener? = null

    private fun setPreviewOrientation(cameraId: Int) {
        val info = CameraInfo()
        Camera.getCameraInfo(cameraId, info)
        val rotation = activity.windowManager.defaultDisplay.rotation
        var degrees = 0
        when (rotation) {
            Surface.ROTATION_0 -> {
                degrees = 0
                mOnChangedSizeListener?.onChanged(height, width)
            }

            Surface.ROTATION_90 -> {
                degrees = 90
                mOnChangedSizeListener?.onChanged(width, height)
            }

            Surface.ROTATION_180 -> {
                degrees = 180
                mOnChangedSizeListener?.onChanged(height, width)
            }

            Surface.ROTATION_270 -> {
                degrees = 270
                mOnChangedSizeListener?.onChanged(width, height)
            }
        }
        var result: Int
        if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
            result = (info.orientation + degrees) % 360
            result = (360 - result) % 360 // compensate the mirror
        } else { // back-facing
            result = (info.orientation - degrees + 360) % 360
        }
        //设置角度, 参考源码注释
        mCamera!!.setDisplayOrientation(result)
    }

    private lateinit var mediaRecorder: MediaRecorder
    private val handle = Handler(Looper.getMainLooper())

    /**
     * 开始录像
     */
    fun startVideo(path: String) {
        mediaRecorder = MediaRecorder()
        //解锁相机,以供 MediaRecorder 使用
        mCamera?.unlock()
        //设置要用于视频捕获的相机
        mediaRecorder.setCamera(mCamera)
        //设置音频源
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
        //设置视频源
        mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA)
        //设置视频的输出格式和编码
        mediaRecorder.setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH))
        //设置输出视频播放的方向,这里只是示例,需要根据实际手机方位来决定角度
        mediaRecorder.setOrientationHint(if (cameraId == 1) 270 else 90)
        //设置输出文件
        mediaRecorder.setOutputFile(path)
        //指定 SurfaceView 预览布局元素
        mediaRecorder.setPreviewDisplay(surfaceHolder!!.surface)
        try {
            mediaRecorder.prepare()
        } catch (e: IOException) {
            e.printStackTrace()
            releaseMediaRecorder()
        }

        handle.postDelayed({
            try {
                mediaRecorder.start()
            } catch (e: IOException) {
                e.printStackTrace()
                releaseMediaRecorder()
            }
        }, 10)
    }

    /**
     * 释放资源
     */
    fun releaseMediaRecorder() {
        if (mediaRecorder != null) {
            mediaRecorder.reset() // 清除配置
            mediaRecorder.release()
            //mediaRecorder = null
            mCamera?.lock()
        }
    }

    /**
     * 停止录像
     */
    fun stopVideo() {
        mediaRecorder.stop()
        mediaRecorder.release()
        mCamera?.lock()
    }

    interface OnChangedSizeListener {
        fun onChanged(width: Int, height: Int)
    }

    companion object {
        private const val TAG = "CAMERA_HELPER"
    }
}

7. 附录

7.1 MediaFileUtils

获取媒体文件路径的工具类

object MediaFileUtils {
    val MEDIA_TYPE_IMAGE = 1
    val MEDIA_TYPE_VIDEO = 2

    /** Create a file Uri for saving an image or video */
    fun getOutputMediaFileUri(type: Int): Uri {
        return Uri.fromFile(getOutputMediaFile(type))
    }

    /** Create a File for saving an image or video */
    fun getOutputMediaFile(type: Int): File? {
        // To be safe, you should check that the SDCard is mounted
        // using Environment.getExternalStorageState() before doing this.

        val mediaStorageDir = File(
            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
            "MyCameraApp"
        )
        // This location works best if you want the created images to be shared
        // between applications and persist after your app has been uninstalled.

        // Create the storage directory if it does not exist
        mediaStorageDir.apply {
            if (!exists()) {
                if (!mkdirs()) {
                    Log.d("MyCameraApp", "failed to create directory")
                    return null
                }
            }
        }

        // Create a media file name
        val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
        return when (type) {
            MEDIA_TYPE_IMAGE -> {
                File("${mediaStorageDir.path}${File.separator}IMG_$timeStamp.jpg")
            }
            MEDIA_TYPE_VIDEO -> {
                File("${mediaStorageDir.path}${File.separator}VID_$timeStamp.mp4")
            }
            else -> null
        }
    }
}

7.2. 本文源码下载

Android Camera1 Demo - 实现预览、拍照、录制视频功能

更多推荐

阿里云无影云电脑详细介绍_无影优势价格和使用

什么是阿里云无影云电脑?无影云电脑(原云桌面)是一种快速构建、高效管理桌面办公环境,无影云电脑可用于远程办公、多分支机构、安全OA、短期使用、专业制图等使用场景,阿里云百科分享无影云桌面的详细介绍、租用价格、云电脑的优势、使用场景、网络架构、无影云电脑与云服务器的区别以及关于无影云电脑的常见问题解答FAQ:目录阿里云无

Windows平台Qt6中UTF8与GBK文本编码互相转换、理解文本编码本质

快速答案UTF8转GBKQStringutf8_str="中UTF文";std::stringgbk_str(utf8_str.toLocal8Bit().data());GBK转UTF8std::stringgbk_str_given_by_somewhere="中GBK文";QStringutf8_str=QStr

GMAC & PHY介绍

1.1PHY接口发展(1)MII支持10M/100Mbps,一个接口由14根线组成,它的支持还是比较灵活的,但是有一个缺点是因为它一个端口用的信号线太多。参考芯片:DP83848、DM900A(该芯片内部集成了MAC和PHY接口)。DP83848芯片只支持10、100兆网络通信速度,采用4/5B编码。(2)RMII是简

机器学习第九课--随机森林

一.什么是集成模型对于几乎所有的分类问题(图像识别除外,因为对于图像识别问题,目前深度学习是标配),集成模型很多时候是我们的首选。比如构建一个评分卡系统,业界的标配是GBDT或者XGBoost等集成模型,主要因为它的效果确实好,而且稳定。还有一点是这些模型的可解释性也很好,不像深度学习模型就像个黑盒子。那为什么集成模型

ChatGpt介绍和国产ChatGpt对比

1.ChatGPT是美国OpenAI研发的聊天机器人程序,2022年11月30日发布。ChatGPT是人工智能技术驱动的自然语言处理工具,它能够通过理解和学习人类的语言来进行对话。2.ChatGPT是一种基于自然语言处理的聊天机器人程序。它使用深度学习技术,通过对大量语料库的学习和训练,可以生成类似人类语言的回复。Ch

java版工程管理系统Spring Cloud+Spring Boot+Mybatis实现工程管理系统源码

工程项目管理软件(工程项目管理系统)对建设工程项目管理组织建设、项目策划决策、规划设计、施工建设到竣工交付、总结评估、运维运营,全过程、全方位的对项目进行综合管理工程项目各模块及其功能点清单一、系统管理1、数据字典:实现对数据字典标签的增删改查操作2、编码管理:实现对系统编码的增删改查操作3、用户管理:管理和查看用户角

《数字图像处理-OpenCV/Python》连载(2)目录

《数字图像处理-OpenCV/Python》连载(2)目录本书京东优惠购书链接:https://item.jd.com/14098452.html本书CSDN独家连载专栏:https://blog.csdn.net/youcans/category_12418787.html第一部分OpenCV-Python的基本操作

opencv滤波技术

文章目录前言一、均值滤波二、中值滤波三、高斯滤波四、双边滤波五、自适应滤波六、滤波器大小总结前言在OpenCV中,有多种滤波技术可以用于图像处理和图像增强。下面我将介绍五种常见的滤波技术,包括均值滤波、中值滤波、高斯滤波、双边滤波和自适应滤波,并提供相应的函数和使用方法。一、均值滤波均值滤波(MeanFiltering

rust迭代器

迭代器用来遍历容器。迭代器就是把容器中的所有元素按照顺序一个接一个的传递给处理逻辑。Rust中的迭代器标准库中定义了Iterator特性traitIterator{typeItem;fnnext(&mutself)->Option<Self::Item>;}实现了Iterator特性的结构体就是迭代器。很多类型都有it

线程、进程和管程

一、线程1.1定义线程:线程是进程中的实体,一个进程可以拥有多个线程,一个线程必须有一个父进程。线程有时被称为轻量级进程,是程序执行流的最小单元。线程的组成部分:1.线程ID:线程标识符2.当前指令指针(PC)3.寄存器集合:存储单元寄存器的集合4.堆栈:堆栈是两种数据结构。堆栈都是一种数据项按序排列的数据结构,只能在

【Linux】自动化构建工具-make/Makefile详解

前言大家好吖,欢迎来到YY滴Linux系列,热烈欢迎!本章主要内容面向接触过Linux的老铁,主要内容含欢迎订阅YY滴Linux专栏!更多干货持续更新!以下是传送门!订阅专栏阅读:YY的《Linux》系列❀❀❀❀❀【Linux系列-P1】Linux环境的搭建【Linux系列-P2】Linux的基本知识与指令【Linux

热文推荐