Camera2 四拍照

2023-05-16

1 理解 Capture 工作流程

在正式介绍如何拍照之前,我们有必要深入理解几种不同模式的 Capture 的工作流程,只要理解它们的工作流程就很容易掌握各种拍照模式的实现原理,在第一章《Camera2 概览》 里我们介绍了 Capture 有以下几种不同模式:

  • 单次模式(One-shot):指的是只执行一次的 Capture 操作,例如设置闪光灯模式、对焦模式和拍一张照片等。多个单次模式的 Capture 会进入队列按顺序执行。

  • 多次模式(Burst):指的是连续多次执行指定的 Capture 操作,该模式和多次执行单次模式的最大区别是连续多次 Capture 期间不允许插入其他任何 Capture 操作,例如连续拍摄 100 张照片,在拍摄这 100 张照片期间任何新的 Capture 请求都会排队等待,直到拍完 100 张照片。多组多次模式的 Capture 会进入队列按顺序执行。

  • 重复模式(Repeating):指的是不断重复执行指定的 Capture 操作,当有其他模式的 Capture 提交时会暂停该模式,转而执行其他被模式的 Capture,当其他模式的 Capture 执行完毕后又会自动恢复继续执行该模式的 Capture,例如显示预览画面就是不断 Capture 获取每一帧画面。该模式的 Capture 是全局唯一的,也就是新提交的重复模式 Capture 会覆盖旧的重复模式 Capture。

我们举个例子来进一步说明上面三种模式,假设我们的相机应用程序开启了预览,所以会提交一个重复模式的 Capture 用于不断获取预览画面,然后我们提交一个单次模式的 Capture,接着我们又提交了一组连续三次的多次模式的 Capture,这些不同模式的 Capture 会按照下图所示被执行:

 Capture 工作原理

下面是几个重要的注意事项:

  1. 无论 Capture 以何种模式被提交,它们都是按顺序串行执行的,不存在并行执行的情况。

  2. 重复模式是一个比较特殊的模式,因为它会保留我们提交的 CaptureRequest 对象用于不断重复执行 Capture 操作,所以大多数情况下重复模式的 CaptureRequest 和其他模式的 CaptureRequest 是独立的,这就会导致重复模式的参数和其他模式的参数会有一定的差异,例如重复模式不会配置 CaptureRequest.AF_TRIGGER_START,因为这会导致相机不断触发对焦的操作。

  3. 如果某一次的 Capture 没有配置预览的 Surface,例如拍照的时候,就会导致本次 Capture 不会将画面输出到预览的 Surface 上,进而导致预览画面卡顿的情况,所以大部分情况下我们都会将预览的 Surface 添加到所有的 CaptureRequest 里。

2 如何拍摄单张照片

拍摄单张照片是最简单的拍照模式,它使用的就是单次模式的 Capture,我们会使用 ImageReader 创建一个接收照片的 Surface,并且把它添加到 CaptureRequest 里提交给相机进行拍照,最后通过 ImageReader 的回调获取 Image 对象,进而获取 JPEG 图像数据进行保存。

2.1 定义回调接口

当拍照完成的时候我们会得到两个数据对象,一个是通过 onImageAvailable() 回调给我们的存储图像数据的 Image,一个是通过 onCaptureCompleted() 回调给我们的存储拍照信息的 CaptureResult,它们是一一对应的,所以我们定义了如下两个回调接口:

private val captureResults: BlockingQueue<CaptureResult> = LinkedBlockingDeque()

private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {
    @MainThread
    override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
        super.onCaptureCompleted(session, request, result)
        captureResults.put(result)
    }
}
private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {
    @WorkerThread
    override fun onImageAvailable(imageReader: ImageReader) {
        val image = imageReader.acquireNextImage()
        val captureResult = captureResults.take()
        if (image != null && captureResult != null) {
            // Save image into sdcard.
        }
    }
}

2.2 创建 ImageReader

创建 ImageReader 需要我们指定照片的大小,所以首先我们要获取支持的照片尺寸列表,并且从中筛选出合适的尺寸,假设我们要求照片的尺寸最大不能超过 4032x3024,并且比例必须是 4:3,所以会有如下筛选尺寸的代码片段:

@WorkerThread
private fun getOptimalSize(cameraCharacteristics: CameraCharacteristics, clazz: Class<*>, maxWidth: Int, maxHeight: Int): Size? {
    val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
    val supportedSizes = streamConfigurationMap?.getOutputSizes(clazz)
    return getOptimalSize(supportedSizes, maxWidth, maxHeight)
}

@AnyThread
private fun getOptimalSize(supportedSizes: Array<Size>?, maxWidth: Int, maxHeight: Int): Size? {
    val aspectRatio = maxWidth.toFloat() / maxHeight
    if (supportedSizes != null) {
        for (size in supportedSizes) {
            if (size.width.toFloat() / size.height == aspectRatio && size.height <= maxHeight && size.width <= maxWidth) {
                return size
            }
        }
    }
    return null
}

接着我们就可以筛选出合适的尺寸,然后创建一个图像格式是 JPEG 的 ImageReader 对象,并且获取它的 Surface:

val imageSize = getOptimalSize(cameraCharacteristics, ImageReader::class.java, maxWidth, maxHeight)!!
jpegImageReader = ImageReader.newInstance(imageSize.width, imageSize.height, ImageFormat.JPEG, 5)
jpegImageReader?.setOnImageAvailableListener(OnJpegImageAvailableListener(), cameraHandler)
jpegSurface = jpegImageReader?.surface

2.3 创建 CaptureRequest

接下来我们使用 TEMPLATE_STILL_CAPTURE 模板创建一个用于拍照的 CaptureRequest.Builder 对象,并且添加拍照的 Surface 和预览的 Surface 到其中:

captureImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureImageRequestBuilder.addTarget(previewDataSurface)
captureImageRequestBuilder.addTarget(jpegSurface)

你可能会疑问为什么拍照用的 CaptureRequest 对象需要添加预览的 Surface,这一点我们在前面有解释过了,如果某一次的 Capture 没有配置预览的 Surface,例如拍照的时候,就会导致本次 Capture 不会将画面输出到预览的 Surface 上,进而导致预览画面卡顿的情况,所以大部分情况下我们都会将预览的 Surface 添加到所有的 CaptureRequest 里。

2.4 矫正 JPEG 图片方向

摄像头传感器的方向很多时候都不是 0°,这就会导致我们拍出来的照片方向是错误的,例如手机摄像头传感器方向是 90° 的时候,垂直拿着手机拍出来的照片很可能是横着的:

 在进行图片方向矫正的时候,我们的目的是做到所见即所得,也就是用户在预览画面里看到的是什么样,输出的图片就是什么样。为了做到图片所见即所得,我们要同时考虑设备方向和摄像头传感器方向,下面是一段来自官方的图片矫正代码:

private fun getJpegOrientation(cameraCharacteristics: CameraCharacteristics, deviceOrientation: Int): Int {
    var myDeviceOrientation = deviceOrientation
    if (myDeviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) {
        return 0
    }
    val sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!

    // Round device orientation to a multiple of 90
    myDeviceOrientation = (myDeviceOrientation + 45) / 90 * 90

    // Reverse device orientation for front-facing cameras
    val facingFront = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
    if (facingFront) {
        myDeviceOrientation = -myDeviceOrientation
    }

    // Calculate desired JPEG orientation relative to camera orientation to make
    // the image upright relative to the device orientation
    return (sensorOrientation + myDeviceOrientation + 360) % 360
}

唯一特别的地方是前置摄像头输出的画面底层默认做了镜像的翻转才能保证我们在预览的时候看到的画面就想照镜子一样,所以前置摄像头给的 SENSOR_ORIENTATION 值也是经过镜像的,但是相机在输出 JPEG 的时候并没有进行镜像操作,所以在计算 JPEG 矫正角度的时候要对这个默认镜像的操作进行逆向镜像。

计算出图片的矫正角度后,我们要通过 CaptureRequest.JPEG_ORIENTATION 配置这个角度,相机在拍照输出 JPEG 图像的时候会参考这个角度值从以下两种方式选一种进行图像方向矫正:

  1. 直接对图像进行旋转,并且将 Exif 的 ORIENTATION 标签赋值为 0。
  2. 不对图像进行旋转,而是将旋转信息写入 Exif 的 ORIENTATION 标签里。

客户端在显示图片的时候一定要去检查 Exif 的ORIENTATION 标签的值,并且根据这个值对图片进行对应角度的旋转才能保证图片显示方向是正确的。

val deviceOrientation = deviceOrientationListener.orientation
val jpegOrientation = getJpegOrientation(cameraCharacteristics, deviceOrientation)
captureImageRequestBuilder[CaptureRequest.JPEG_ORIENTATION] = jpegOrientation

2.5 设置缩略图尺寸

相机在输出 JPEG 图片的时候,同时会根据我们通过 CaptureRequest.JPEG_THUMBNAIL_SZIE 配置的缩略图尺寸生成一张缩略图写入图片的 Exif 信息里。在设置缩略图尺寸之前,我们首先要获取相机支持哪些缩略图尺寸,与获取预览尺寸或照片尺寸列表方式不一样的是,缩略图尺寸列表是直接通过 CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES 获取的。配置缩略图尺寸的代码如下所示:

val availableThumbnailSizes = cameraCharacteristics[CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES]
val thumbnailSize = getOptimalSize(availableThumbnailSizes, maxWidth, maxHeight)

在获取图片缩略图的时候,我们不能总是假设图片一定会在 Exif 写入缩略图,当 Exif 里面没有缩略图数据的时候,我们要转而直接 Decode 原图获取缩略图,另外无论是原图还是缩略图,都要根据 Exif 的 ORIENTATION 角度进行角度矫正才能正确显示,下面是我们 Demo 中获取图片缩略图的代码:

@WorkerThread
private fun getThumbnail(jpegPath: String): Bitmap? {
    val exifInterface = ExifInterface(jpegPath)
    val orientationFlag = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
    val orientation = when (orientationFlag) {
        ExifInterface.ORIENTATION_NORMAL -> 0.0F
        ExifInterface.ORIENTATION_ROTATE_90 -> 90.0F
        ExifInterface.ORIENTATION_ROTATE_180 -> 180.0F
        ExifInterface.ORIENTATION_ROTATE_270 -> 270.0F
        else -> 0.0F
    }

    var thumbnail = if (exifInterface.hasThumbnail()) {
        exifInterface.thumbnailBitmap
    } else {
        val options = BitmapFactory.Options()
        options.inSampleSize = 16
        BitmapFactory.decodeFile(jpegPath, options)
    }

    if (orientation != 0.0F && thumbnail != null) {
        val matrix = Matrix()
        matrix.setRotate(orientation)
        thumbnail = Bitmap.createBitmap(thumbnail, 0, 0, thumbnail.width, thumbnail.height, matrix, true)
    }

    return thumbnail
}

2.6 设置定位信息

拍照的时候,通常都会在图片的 Exif 写入定位信息,我们可以通过 CaptureRequest.JPEG_GPS_LOCATION 配置定位信息,代码如下:

@WorkerThread
private fun getLocation(): Location? {
    val locationManager = getSystemService(LocationManager::class.java)
    if (locationManager != null && ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
        return locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
    }
    return null
}
val location = getLocation()
captureImageRequestBuilder[CaptureRequest.JPEG_GPS_LOCATION] = location

2.7 播放快门音效

在进行拍照之前,我们还需要配置拍照时播放的快门音效,因为 Camera2 和 Camera1 不一样,拍照时不会有任何声音,需要我们在适当的时候通过 MediaSoundPlayer 播放快门音效,通常情况我们是在 CaptureStateCallback.onCaptureStarted() 回调的时候播放快门音效:

private val mediaActionSound: MediaActionSound = MediaActionSound()

private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {

    @MainThread
    override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
        super.onCaptureStarted(session, request, timestamp, frameNumber)
        // Play the shutter click sound.
        cameraHandler?.post { mediaActionSound.play(MediaActionSound.SHUTTER_CLICK) }
    }

    @MainThread
    override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
        super.onCaptureCompleted(session, request, result)
        captureResults.put(result)
    }
}

2.8 拍照并保存图片

经过一连串的配置之后,我们终于可以开拍照了,直接调用 CameraCaptureSession.capture() 方法把 CaptureRequest 对象提交给相机就可以等待相机输出图片了,该方法要求我们设置三个参数:

  • request:本次 Capture 操作使用的 CaptureRequest 对象。
  • listener:监听 Capture 状态的回调接口。
  • handler:回调 Capture 状态监听接口的 Handler 对象。
captureSession.capture(captureImageRequest, CaptureImageStateCallback(), mainHandler)

如果一切顺利,相机在拍照完成的时候会通过 CaptureStateCallback.onCaptureCompleted() 回调一个 CaptureResult 对象给我们,里面包含了本次拍照的所有信息,另外还会通过 OnImageAvailableListener.onImageAvailable() 回调一个代表图像数据的 Image 对象给我们。在我们的 Demo 中,我们将获取到的 CaptureResult 对象保存到一个阻塞队列中,在 OnImageAvailableListener.onImageAvailable() 回调的时候就从这个阻塞队列获取 CaptureResult 对象,结合 Image 对象对图片进行保存操作,并且还会在图片保存完毕的时候获取图片的缩略图用于刷新 UI,代码如下所示:

private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {

    private val dateFormat: DateFormat = SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.getDefault())
    private val cameraDir: String = "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)}/Camera"

    @WorkerThread
    override fun onImageAvailable(imageReader: ImageReader) {
        val image = imageReader.acquireNextImage()
        val captureResult = captureResults.take()
        if (image != null && captureResult != null) {
            image.use {
                val jpegByteBuffer = it.planes[0].buffer// Jpeg image data only occupy the planes[0].
                val jpegByteArray = ByteArray(jpegByteBuffer.remaining())
                jpegByteBuffer.get(jpegByteArray)
                val width = it.width
                val height = it.height
                saveImageExecutor.execute {
                    val date = System.currentTimeMillis()
                    val title = "IMG_${dateFormat.format(date)}"// e.g. IMG_20190211100833786
                    val displayName = "$title.jpeg"// e.g. IMG_20190211100833786.jpeg
                    val path = "$cameraDir/$displayName"// e.g. /sdcard/DCIM/Camera/IMG_20190211100833786.jpeg
                    val orientation = captureResult[CaptureResult.JPEG_ORIENTATION]
                    val location = captureResult[CaptureResult.JPEG_GPS_LOCATION]
                    val longitude = location?.longitude ?: 0.0
                    val latitude = location?.latitude ?: 0.0

                    // Write the jpeg data into the specified file.
                    File(path).writeBytes(jpegByteArray)

                    // Insert the image information into the media store.
                    val values = ContentValues()
                    values.put(MediaStore.Images.ImageColumns.TITLE, title)
                    values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, displayName)
                    values.put(MediaStore.Images.ImageColumns.DATA, path)
                    values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, date)
                    values.put(MediaStore.Images.ImageColumns.WIDTH, width)
                    values.put(MediaStore.Images.ImageColumns.HEIGHT, height)
                    values.put(MediaStore.Images.ImageColumns.ORIENTATION, orientation)
                    values.put(MediaStore.Images.ImageColumns.LONGITUDE, longitude)
                    values.put(MediaStore.Images.ImageColumns.LATITUDE, latitude)
                    contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)

                    // Refresh the thumbnail of image.
                    val thumbnail = getThumbnail(path)
                    if (thumbnail != null) {
                        runOnUiThread {
                            thumbnailView.setImageBitmap(thumbnail)
                            thumbnailView.scaleX = 0.8F
                            thumbnailView.scaleY = 0.8F
                            thumbnailView.animate().setDuration(50).scaleX(1.0F).scaleY(1.0F).start()
                        }
                    }
                }
            }
        }
    }
}

2.9 前置摄像头拍照的镜像问题

如果你使用前置摄像头进行拍照,虽然照片的方向已经被我们矫正了,但是你会发现画面却是相反的,例如你在预览的时候人脸在左边,拍出来的照片人脸却是在右边。出现这个问题的原因是默认情况下相机不会对 JPEG 图像进行镜像操作,导致输出的原始画面是非镜像的。解决这个问题的一个办法是拿到 JPEG 数据之后再次对图像进行镜像操作,然后才保存图片。

3 如何连续拍摄多张图片

在我们的 Demo 中有一个特殊的拍照功能,就是当用户双击快门按钮的时候会连续拍摄 10 张照片,其实现原理就是采用了多次模式的 Capture,所有的配置流程和拍摄单张照片一样,唯一的区别是我们使用 CameraCaptureSession.captureBurst() 进行拍照,该方法要求我们传递一下三个参数:

  • requests:按顺序连续执行的 CaptureRequest 对象列表,每一个 CaptureRequest 对象都可以有自己的配置,在我们的 Demo 里出于简化的目的,10 个 CaptureRequest 对象实际上的都是同一个。
  • listener:监听 Capture 状态的回调接口,需要注意的是有多少个 CaptureRequest 对象就会回调该接口多少次。
  • handler:回调 Capture 状态监听接口的 Handler 对象。
val captureImageRequest = captureImageRequestBuilder.build()
val captureImageRequests = mutableListOf<CaptureRequest>()
for (i in 1..burstNumber) {
    captureImageRequests.add(captureImageRequest)
}
captureSession.captureBurst(captureImageRequests, CaptureImageStateCallback(), mainHandler)

接下来所有的流程就和拍摄单招照片一样了,每输出一张图片我们就将其保存到 SD 卡并且刷新媒体库和缩略图。

4 如何连拍

连拍这个功能在 Camera2 出现之前是不可能实现的,现在我们只需要使用重复模式的 Capture 就可以轻松实现连拍功能。重复模式的 Capture 来实现预览功能,而这一次我们不仅要用该模式进行预览,还要在预览的同时也输出照片,所以我们会使用 CameraCaptureSession.setRepeatingRequest() 方法开始进行连拍:

val captureImageRequest = captureImageRequestBuilder.build()
captureSession.setRepeatingRequest(captureImageRequest, CaptureImageStateCallback(), mainHandler)

停止连拍有以下两种方式:

  1. 调用 CameraCaptueSession.stopRepeating() 方法停止重复模式的 Capture,但是这会导致预览也停止。
  2. 调用 CameraCaptueSession.setRepeatingRequest() 方法并且使用预览的 CaptureRequest 对象,停止输出照片。

在我们的 Demo 里使用了第二种方式:

@MainThread
private fun stopCaptureImageContinuously() {
    // Restart preview to stop the continuous image capture.
    startPreview()
}

5 如何切换前后置摄像头

切换前后置摄像头是一个很常见的功能,虽然和本章的主要内容不相关,但是在 Demo 中已经实现,所以这里也顺便提一下。我们只要按照以下顺序进行操作就可以轻松实现前后置摄像头的切换:

  1. 关闭当前摄像头
  2. 开启新的摄像头
  3. 创建新的 Session
  4. 开启预览

下面是代码片段,详细代码大家可以自行查看 Demo 源码:

@MainThread
private fun switchCamera() {
    val cameraDevice = cameraDeviceFuture?.get()
    val oldCameraId = cameraDevice?.id
    val newCameraId = if (oldCameraId == frontCameraId) backCameraId else frontCameraId
    if (newCameraId != null) {
        closeCamera()
        openCamera(newCameraId)
        createCaptureRequestBuilders()
        setPreviewSize(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT)
        setImageSize(MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT)
        createSession()
        startPreview()
    }
}

6 总结

本章主要讲述了如何实现几种常见的拍照模式,其核心要领就是理解【重复模式】、【单词模式】和【多次模式】的工作流程,根据实际业务情况灵活运用,下面是几个小建议:

  1. 重复模式和多次模式都可以实现连拍功能,其中重复模式适合没有连拍上限的情况,而多次模式适合有连拍上限的情况。
  2. 一个 CaptureRequest 可以添加多个 Surface,这就意味着你可以同时拍摄多张照片。
  3. 拍照获取 CaptureResult 和 Image 对象走的是两个不同的回调接口,灵活运用子线程的阻塞操作可以简化你的代码逻辑。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Camera2 四拍照 的相关文章

  • rxjava - compose()操作符

    1 问题背景 想要给多个流重复应用 34 一系列 34 相同的操作符 该怎么办 比如 我们使用Rx 43 Retrofit进行网络请求时 都有遇到这样场景 要在io线程中请求数据 在主线程订阅 更新UI 所以必须频繁使用下面这样的代码 su
  • RxJava2 背压

    1 背压 在RxJava中 xff0c 会遇到被观察者发送消息太快以至于它的操作符或者订阅者不能及时处理相关的消息 xff0c 这就是典型的背压 Back Pressure 场景 BackPressure经常被翻译为背压 xff0c 背压的
  • MVVM实现与原理分析

    1 MVVM简介 1 1 MVC amp MVP amp MVVM MVP MVVM与MVP结构类似 xff0c MVP也是通过Presenter将View与Model解耦 不过MVVM是基于观察者模式 xff0c viewModel不持有
  • PCM数据格式

    什么是PCM PCM全称Pulse Code Modulation xff0c 翻译一下是脉冲调制编码 其实大可以不用关心英文释义 xff0c 之所以这么命名是因为一些历史原因 在音视频中 xff0c PCM是一种用数字表示采样模拟信号的方
  • 音频帧大小的计算

    音频aac mp3文档规定 xff1a AAC xff1a 帧大小1024个sample xff0c 采样率为44100Hz 帧播放时长 xff1a acc dur 61 1024 44100 61 0 02322s 61 23 22ms
  • Java多线程系列--join()

    1 join 介绍 join 定义在Thread java中 join 的作用 xff1a 让 主线程 等待 子线程 结束之后才能继续运行 这句话可能有点晦涩 xff0c 我们还是通过例子去理解 xff1a 主线程 public class
  • Android的设计模式-装饰者模式

    1 定义 动态地给一个对象添加一些额外的职责 就增加功能来说 xff0c 装饰模式相比生成子类更为灵活 2 介绍 装饰者模式属于结构型模式 装饰者模式在生活中应用实际上也非常广泛 xff0c 一如一间房 xff0c 放上厨具 xff0c 它
  • android音频焦点Audio Focus

    为了便于理解 xff0c 我们以android的8 0以前的版本为例 xff0c 8 0以后有一定改动 xff0c 但是基本思路一样 关于管理音频焦点 xff08 8 0以前和更高版本 xff09 的官方文档 xff1a https dev
  • Android音频通路的切换

    Android支持多种设备的的输出 一台正常的机子 xff0c 本身就自带话筒 xff0c 扬声器 xff0c 麦克风等多个声音输入输出设备 xff0c 再加上五花八门的外置设备 xff08 通过耳机 xff0c 蓝牙 xff0c wifi
  • Java实现基本数据结构——数组

    数组概念 所谓数组 xff0c 是有序的元素序列 若将有限个类型相同的变量的集合命名 xff0c 那么这个名称为数组名 在数据结构中 xff0c 数组是一种线性表 xff0c 就是数据排列成一条直线一样的结构 在内容空间中 xff0c 数组
  • 块元素居中方式

    1 定位 span class token doctype lt DOCTYPE html gt span span class token tag span class token tag span class token punctua
  • wireshark以太帧的分析

    首先应该明白 xff0c 封装以太帧的位于OSI七层模型的第二层 xff0c 也就是数据链路层 xff0c wireshark可以把完整的以太帧抓起来 xff0c 我们可以清楚的看到 打开wireshark找到自己ip对应的网卡 xff0c
  • 网络协议——七层、五层、四层协议概念及功能分析

    一 7层 7层是指OSI七层协议模型 xff0c 主要是 xff1a 应用层 xff08 Application xff09 表示层 xff08 Presentation xff09 会话层 xff08 Session xff09 传输层
  • 为什么 UDP 头只有 8 个字节

    为什么这么设计 xff08 Why s THE Design xff09 是一系列关于计算机领域中程序设计决策的文章 xff0c 我们在这个系列的每一篇文章中都会提出一个具体的问题并从不同的角度讨论这种设计的优缺点 对具体实现造成的影响 如
  • Java 泛型

    Java 泛型 xff08 generics xff09 是 JDK 5 中引入的一个新特性 泛型提供了编译时类型安全检测机制 xff0c 该机制允许程序员在编译时检测到非法的类型 泛型的本质是参数化类型 xff0c 也就是说所操作的数据类
  • Android 端处理 YUV 数据 - Libyuv 的编译与使用

    在 Android 系统上 Camera 输出的图像一般为 NV21 YUV420SP 系列 格式 当我们想进行录像处理时 会面临两个问题 问题 1 图像的旋转问题 后置镜头 需要旋转 90 前置镜头 需要旋转 270 然后再进行镜像处理
  • YUV420P旋转

    YUV420与YUV420P YUV 和我们熟知的 RGB 类似 xff0c 是一种颜色编码格式 它主要用于电视系统和模拟视频邻域 xff08 如 Camera 系统 xff09 YUV 包含三个分量 xff0c 其中 Y 表示明亮度 xf
  • android-camera方向

    1 概念解释 自然方向 xff1a 指当宽比高短时 xff0c 我们看到的手机的方向 xff08 竖屏 xff09 xff0c 就是自然方向 2 相机图像传感器采集图像的方向 由于手机Camera拍摄到的图片来自相机的图像传感器 xff0c
  • clion创建第一个C项目

    点击new project 选择C Executable 输入路径 Language standard C99 main c include lt stdio h gt void main char string 61 34 I love
  • VMware Ubuntu虚拟机忘记密码

    前言 xff1a 在VMware运行Ubuntu虚拟机时 xff0c 开机之后忘记密码怎么办 xff1f 环境 xff1a Ubuntu版本 xff1a ubuntu 16 04 6 server amd64 xff1b VMware版本

随机推荐

  • Ubuntu18.04安装matlabR2019A

    载安装包和破解文件 链接 https pan baidu com s 1X09GAchToEqyMRol3msGAA 密码 wak6 下载完成后解压 右击 iso镜像文件 xff0c 选择使用其他程序打开 选择磁盘映像挂载器 打开后会在桌面
  • 03-对抗样本攻击

    对抗样本攻击 Github xff1a https github com Gary11111 03 GAN 研究背景 尽管深度学习在很多计算机视觉领域的任务上表现出色 xff0c Szegedy第一次发现了深度神经网络在图像分类领域存在有意
  • 冒泡排序算法

    冒泡排序是非常好理解的 xff0c 以从小到大排序为例 xff0c 每一轮排序就找出未排序序列中最大值放在最后 设数组的长度为N xff1a xff08 1 xff09 比较前后相邻的二个数据 xff0c 如果前面数据大于后面的数据 xff
  • 将一个分支上的commit 转移到另一个分支上git cherry-pick <commit id>

    使用 cherry pick 根据git 文档 xff1a Apply the changes introduced by some existing commits 就是对已经存在的commit 进行apply 可以理解为再次提交 xff
  • Git 如何查看和修改用户名、邮箱

    用户名和邮箱地址是本地git客户端的一个变量 xff0c 不随git库而改变 每次commit都会用用户名和邮箱纪录 1 查看用户名和地址 git config user name git config user email 2 修改用户名
  • Android PendingIntent

    在Android中 xff0c 我们常常使用PendingIntent来表达一种 留待日后处理 的意思 从这个角度来说 xff0c PendingIntent可以被理解为一种特殊的异步处理机制 不过 xff0c 单就命名而言 xff0c P
  • .CR2格式文件怎么快速批量转换成JPG等格式

    打开需要转换的CR2文件夹 用FSViewer exe看图软件打开CR2文件 xff0c 然后再双击打开已打开的图片并选中所有需要转换的CR2文件 点击工具 选择批量转换选中的图像 若是右边窗口中没有文件 xff0c 则需要手动点击 全部添
  • 利用AlarmManager完成精准的轮询

    问题分析 想起轮询我们一般会想起利用Handler和Timer xff0c 然而AlarmManager相比于Handler和Timer有优势 xff0c 具体的分析我参考了一个大神的博客 xff1a 最近在做一个需求 xff1a 客户端按
  • Intellij IDEA junit 使用之org.junit不存在

    1 同理打开 xff1a File gt project Structure gt librabries gt 点击左上角 43 号 gt From Maven 2 输入 junit 4 12 3 确定即可 在build gradle文件中
  • Android studio显示你的主机中的软件中止了一个已建立的连接

    Android studio在run的时候显示你的主机中的软件中止了一个已建立的连接 出现的场景 是在run项目的时候出现了这样的 如下图 当我把热点关了就好了 再也不会出现类似的情况了
  • 设计模式、重构、编程规范等的经典书籍书籍推荐

    有关设计模式 重构 编程规范等的经典书籍很多 xff0c 有很多你应该已经听说过 甚至看过 今天 xff0c 我就结合我的经验 xff0c 对这些书籍进行一个整理和点评 你可以据此来选择适合你的书籍 xff0c 结合着专栏一块儿来学习 xf
  • Android打开或者关闭GPS

    打开和关闭gps 需要系统权限 android permission WRITE SECURE SETTINGS 打开或者关闭gps public void openGPS boolean open if Build VERSION SDK
  • 【Erro】安装homebrew报错curl: (7) Failed to connect to raw.githubusercontent.com port 443: Operation

    下载brew bin bash c 34 curl fsSL https raw githubusercontent com Homebrew install master install sh 34 出现这样的错误 xff1f curl
  • ChatGPT火爆科研圈,登上《Nature》《Science》正刊

    ChatGPT火出圈了 xff0c 几乎涉及到各行各业的每个领域 xff0c 科研圈更甚 Science 期刊主编H HOLDEN THORP发表关于ChatGPT的社论 xff1a ChatGPT is fun but not an au
  • 解决 Unable to determine application id: com.android.tools.idea.run.ApkProvisionException

    问题 xff1a Unable to determine application id com android tools idea run ApkProvisionException Error loading build artifac
  • Android SurfaceView预览变形完美解决方法

    这个问题百度上一搜一大把 xff0c 基本上都是说找到和SurfaceView的比例相近的camera预览尺寸 xff0c 但是发现预览时候还是差了点意思 xff0c 具体看下面这个回调就知道是为什么了 64 Override public
  • Camera2 教程 一概览

    从 Android 5 0 开始 xff0c Google 引入了一套全新的相机框架 Camera2 xff08 android hardware camera2 xff09 并且废弃了旧的相机框架 Camera1 xff08 androi
  • Camera2 二开关相机

    1 创建相机项目 正如前所说的 xff0c 我们会开发一个具有完整相机功能的应用程序 xff0c 所以第一步要做的就是创建一个相机项目 xff0c 这里我用 AS 创建了一个叫 Camera2Sample 的项目 xff0c 并且有一个 A
  • Camera2 三预览

    1 获取预览尺寸 CameraCharacteristics 是一个只读的相机信息提供者 xff0c 其内部携带大量的相机信息 xff0c 包括代表相机朝向的 LENS FACING xff1b 判断闪光灯是否可用的 FLASH INFO
  • Camera2 四拍照

    1 理解 Capture 工作流程 在正式介绍如何拍照之前 xff0c 我们有必要深入理解几种不同模式的 Capture 的工作流程 xff0c 只要理解它们的工作流程就很容易掌握各种拍照模式的实现原理 xff0c 在第一章 Camera2