和明天说你好!OvO
影像处理学习指南

目录

  1. 1. 电子相机成像原理
  2. 2. 像素
  3. 3. 曝光三要素
  4. 4. 曝光补偿
  5. 5. 焦距
    1. 5.1. 视角构图
    2. 5.2. 透视效果
    3. 5.3. 景深控制
  6. 6. 图像处理算法
    1. 6.1. 去马赛克算法
      1. 6.1.1. 拜尔滤色阵列
      2. 6.1.2. 最邻近插值
      3. 6.1.3. 双线性插值
    2. 6.2. 白平衡(WB)
  7. 7. 情景模式
  8. 8. Android CameraX
    1. 8.1. 简介
    2. 8.2. 使用事项
    3. 8.3. 使用案例

电子相机成像原理

电子相机中有一个类似于我们人类感官细胞的元件,感光元件(如 CCD、CMOS 传感器)。感光元件上面有大量的像素,像素会利用光电效应将照射到其上的光子转化为电子,从而形成电荷,这些电荷的数量与光子的数量(即光的亮度)成比例。这样,感光元件就将光亮度的强弱转化为了电信号的强弱。接下来,这些电信号会被传递到相机内部的图像处理芯片(如 ISP,即图像信号处理器)。图像处理芯片会将电信号的强弱转化为数字值的大小,再通过图像处理算法处理成不同的像素亮度和颜色,最终形成图像文件。

备注:

感光元件通常采用拜耳滤色阵列,每个像素只捕捉一种颜色(红、绿、蓝)。

像素

像素是感光元件上每个接收光子并转化为电子的最小单位。更多的像素意味着更高的图像分辨率,而更大的像素意味着每个像素能够捕捉到的光线、容纳的电子越多,噪声更少动态范围更大。越大的感光元件能够拥有更多的像素和更大的像素,这也是“底大一级压死人”的原因。

曝光三要素

曝光的多少影响了成像画面的亮暗,由曝光三要素决定。除此之外,光圈值会影响景深快门速度影响画面的凝固程度感光度影响画面细腻程度

  • 光圈值(F):光圈大小与光圈值成反比。焦距一定时,光圈值越小光圈直径越大,进入的光越多,光亮度越强,电信号越强,画面就越亮,同时,光线会聚焦到一个较小的区域,导致焦点前后迅速失焦,造成浅景深

  • 快门速度(S):快门速度越大,曝光时间越长,接收光子的数量越多,光亮度越强,电信号越强,画面就越亮,同时,感光元件接受曝光的时间越长,物体在曝光期间移动的距离也越长,画面凝固程度越低

  • 感光度(ISO):感光度越大,对像素中的电信号的增益也越大,画面就越亮,同时,噪声也增多,动态范围降低,再加上放大是非线性的,会造成更多的细节损失,画面细腻程度越差

备注:

  1. 景深是指在图像中,前景和背景之间能够保持清晰的范围,也就是对焦区域的深度

  2. 光圈值的计算公式:
    $$
    光圈值=\frac{焦距}{光圈直径}
    $$

  3. 快门速度越大时,设备晃动所产生的画面模糊越严重。安全快门指的就是画面不会因为设备晃动产生模糊的最大快门速度。

  4. 胶片相机中,感光度描述的是胶片对光的敏感度;电子相机中,感光度使用的是电子信号放大实现的。

  5. 高感光度的作用是为了在噪声增多的代价下,在暗光环境下拍摄图像。

曝光补偿

曝光补偿主要在自动曝光模式下使用。通过调整曝光补偿,相机会自动调整快门速度、光圈、感光度来实现人为的增加、渐少曝光量

曝光补偿通常以 EV(曝光值)为单位表示,通常以 1/3、1/2、1 EV 为调节单位,例如 IQOO Neo7 竞速版的原相机的调节单位为 1/3 EV。

焦距

焦距决定了镜头的视角(即画面中的场景宽窄),还会影响图像的透视效果、景深,以及被摄物体的视觉比例。

视角构图

焦距决定了镜头的视角范围,这也决定了拍摄画面的宽窄程度。

  • 广角镜头(短焦距,如 18mm、24mm 等)有更广阔的视角,可以在画面中包含更多场景,适合拍摄风景、建筑和大场景的摄影。广角镜头让观众有一种身临其境的感觉。
  • 标准镜头(如 50mm 左右)具有接近人眼的自然视角,拍摄出的效果接近人眼的视觉感受,因此适合人像、街拍等日常场景。
  • 长焦镜头(长焦距,如 85mm、200mm 等)视角较窄,适合将远处的景物拉近,放大细节,常用于人像、体育摄影和野生动物摄影。长焦镜头可以很好地隔离主体与背景。

透视效果

焦距影响图像的透视效果,也就是说,焦距的长短会改变场景中物体的视觉距离感:

  • 广角镜头会产生夸张的透视效果,让前景显得特别大,背景显得特别小,拉远了物体之间的距离。这种效果可以使画面具有更强的纵深感,但如果用于人像拍摄,可能会造成失真,例如将面部夸大变形。
  • 长焦镜头会压缩透视效果,看起来像是将前景和背景拉得更近。比如拍摄人像时,背景会显得更为接近主体,从而使主体与背景更为贴近,形成“压缩”效果,通常可以产生更柔和的背景虚化。

景深控制

焦距还直接影响到图像的景深(即清晰对焦的范围)。在光圈相同的情况下,焦距越长,景深越浅,背景越容易被虚化;而焦距越短,景深越深,画面中更多的元素都能保持清晰。

  • 广角镜头通常有较深的景深,即便光圈开大,背景也不会过于模糊,适合需要整体清晰的拍摄场景,如风景摄影。
  • 长焦镜头的景深较浅,特别适合拍摄人像,因为它可以将背景虚化,从而使人物更突出,避免背景的干扰。

图像处理算法

去马赛克算法

感光元件通常采用拜耳滤色阵列的结构,每个像素只捕捉一种颜色(红、绿、蓝),因此需要通过去马赛克算法将这些单色信息转换为完整的 RGB 颜色,即根据周围像素的颜色信息插值补全,使每个像素都具备红、绿、蓝三色信息。常见的去马赛克算法有最近邻插值双线性插值和更复杂的自适应插值方法。

拜尔滤色阵列

拜耳滤色阵列是一种用于电子相机图像传感器的色彩滤光器阵列。

最常见的拜耳阵列是由一个 2x2 的重复结构组成的:

1
2
3
4
5
6
7
8
9
10
2x2 的重复结构:
G R
B G

拜尔阵列样例:
G R G R G R G R
B G B G B G B G
G R G R G R G R
B G B G B G B G
... ...

在拜尔阵列中,绿色滤光片的像素数量是最多的,占据了 50%,因为人眼对绿色更敏感,且绿色对图像的感知具有更重要的作用;红色和蓝色各占 25%,分布在图像传感器中的其他位置。

最邻近插值

最近邻插值是最简单的一种插值算法。它的基本思想是:对于需要某种颜色插值的像素点,直接填充为离它最近的该颜色像素的值(已知值)。由于只涉及了最近邻像素的值,使用最邻近插值生成的图像可能会产生明显的锯齿状或块状伪影,导致图像的细节丧失。

双线性插值

双线性插值是一种比最近邻插值更平滑的插值方法。它的基本思想是:对于需要某种颜色插值的像素点,将使用周围该颜色像素的加权平均值(只有已知值参与计算),能够有效避免锯齿效应,得到更平滑的图像。

白平衡(WB)

自动白平衡通过分析图像中的光源类型(如日光、荧光灯、阴影等),调整图像的色温来消除图像的偏色。

在选择低色温模式,如“白炽灯”时,会消除一定的暖光;在选择高色温模式,如“阴影”时,会消除一定的冷光。选择不同的白平衡模式可以营造不同的拍摄氛围

大部分电子相机都提供了多种预设的白平衡模式:

  • 自动白平衡:自动分析场景并选择最合适的色温调节,但在复杂光线条件下(如混合光源)可能会出现偏色。
  • 晴天:用于阳光明媚的日间拍摄。相机设置色温为大约 5500K,适合在自然光下拍摄,不会对色温进行太多调整。
  • 阴天:用于阴天或多云天气下拍摄。通常将色温设置为 6000K 或更高,以让图像看起来更暖和,弥补阴天光源的冷色调。
  • 白炽灯:用于白炽灯等产生暖色光源的环境。相机会调整色温,减少黄色和橙色的色偏,通常色温设置在 2800K 左右。

情景模式

以 IQOO Neo7 竞速版原相机为例。

  • 夜晚模式:夜晚模式下,快门时间默认更长,以获得更好的曝光。

  • 人像模式:人像模式下,默认使用标准镜头焦距和更的大光圈值(深景深),以拍摄更接近人眼视觉、更清晰的人像。

  • 抓拍模式:抓拍模式下,快门时间默认更短,以获得更好的画面凝固程度,清晰捕捉动作。

Android CameraX

简介

Android CameraX 中通过相机提供者(ProcessCameraProvider)和用例来操作相机。

CameraX 中的用例指的是相机的一种特定使用方式,它规定了如何捕获和处理图像数据。CameraX 提供了四种标准用例:

  • 预览(Preview):用于将相机的实时画面展示给用户,将图像数据直接传送到预览视图PreviewView 上。

  • 图像捕获(Image Capture):用于拍摄和保持照片,支持设置拍照分辨率、JPEG 质量、快门声等参数。

  • 图像分析(Image Analysis):用于实时处理每一帧图像数据,可以应用于实时图像分析任务,如二维码扫描、人脸识别或机器学习模型预测。此用例允许对每一帧进行处理(如识别、检测等),以支持复杂的图像处理需求。

  • 视频捕获(Video Capture):用于拍摄和保持视频,它支持多种编码格式及分辨率的设置。

使用事项

CameraX 所需的最低 SDK 级别为 API 21(Android 5.0,Lollipop),语言兼容性需要设置为 Java 1.8

兼容性设置:

1
2
3
4
5
6
7
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}

CameraX 依赖库如下:

1
2
3
4
5
6
androidx.camera:camera-core
androidx.camera:camera-camera2
androidx.camera:camera-lifecycle
androidx.camera:camera-video
androidx.camera:camera-view
androidx.camera:camera-extensions

使用案例

案例基于教程 CameraX 使用入门

案例需要使用 viewBinding 特性:

1
2
3
buildFeatures {
viewBinding = true
}

案例需要使用如下权限:

1
2
3
4
5
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />

核心代码如下:

activity_main.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?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"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />

<Button
android:id="@+id/image_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:layout_marginEnd="50dp"
android:elevation="2dp"
android:text="@string/take_photo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toStartOf="@id/vertical_centerline" />

<Button
android:id="@+id/video_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:layout_marginStart="50dp"
android:elevation="2dp"
android:text="@string/start_capture"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/vertical_centerline" />

<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_centerline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent=".50" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
package com.android.example.cameraxapp

import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.camera.video.VideoRecordEvent
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.PermissionChecker
import com.android.example.cameraxapp.databinding.ActivityMainBinding
import java.text.SimpleDateFormat
import java.util.Locale

class MainActivity : AppCompatActivity() {

// 等价于 static
companion object {
private const val TAG = "CameraXApp"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS =
mutableListOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}

// 视图绑定
private lateinit var viewBinding: ActivityMainBinding

// 图像捕获用例和视频捕获用例(以及录像实例)
private var imageCapture: ImageCapture? = null
private var videoCapture: VideoCapture<Recorder>? = null
private var recording: Recording? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)

// 请求权限
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}

// 按钮监听
viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
viewBinding.videoCaptureButton.setOnClickListener { captureVideo() }
}

// 请求权限回调
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(
this, "相机权限被拒绝!", Toast.LENGTH_SHORT
).show()
finish()
}
}
}

/**
* 获取相机提供者实例,启动相机
*/
private fun startCamera() {
// 获取相机提供者实例
// 这个过程是异步的,使用了 ListenableFuture 处理获取相机提供者之后的逻辑
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// 相机提供者 ProcessCameraProvider
val cameraProvider = cameraProviderFuture.get()

// 创建预览用例
val preview = Preview.Builder().build().also {
it.surfaceProvider = viewBinding.viewFinder.surfaceProvider
}

// 创建图像捕获用例
imageCapture = ImageCapture.Builder().build()

// 创建视频捕获用例,设置输出
val recorder = Recorder.Builder().setQualitySelector(
QualitySelector.from(
Quality.HIGHEST, FallbackStrategy.higherQualityOrLowerThan(Quality.SD) // 回调策略
)
).build()
videoCapture = VideoCapture.withOutput(recorder)

// 相机选择器,默认选择后置
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

// 将相机用例绑定到 MainActivity 的生命周期
try {
// 每次绑定前清除已有的用例
cameraProvider.unbindAll()
// 将相机用例绑定到 MainActivity 的生命周期
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, videoCapture
)
} catch (exc: Exception) {
Log.e(TAG, "用例绑定失败", exc)
}

}, ContextCompat.getMainExecutor(this)) // 监听器需要在主线程上执行,可以更安全地更新 UI,做主线程才能做的操作
}

/**
* 使用图像捕获用例,实现拍照
*/
private fun takePhoto() {
Log.d(TAG, "准备拍照。")

// 确保图像捕获用例不为空
val imageCapture = imageCapture ?: return

// 设置图像文件的信息
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())
// ContentValues 是一个键值对集合,用于存储数据,这里用来存储有关图片的信息
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}

// 创建图像输出选项(输出文件 UIR、输出图像文件的信息)
val outputOptions = ImageCapture.OutputFileOptions.Builder(
contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues
).build()

// 拍照和回调
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this), // 需要在主线程上执行
object : ImageCapture.OnImageSavedCallback { // 回调函数
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "拍照失败:${exc.message}", exc)
}
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val msg = "拍照成功:${output.savedUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
})
}

/**
* 使用视频捕获用例,实现录像或结束录像
*/
private fun captureVideo() {
Log.d(TAG, "开始录像。")

// 确保视频捕获用例不为空,并临时禁用按钮
val videoCapture = this.videoCapture ?: return
viewBinding.videoCaptureButton.isEnabled = false

// 如果当前正在录像,那就结束录像
val currentRecording = recording
if (currentRecording != null) {
currentRecording.stop() // 结束录像,触发结束事件
recording = null
return
}

// 如果不在录像,那就准备录像
// 设置视频文件的基本属性
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
}
}

// 创建视频输出选项,使用 MediaStore 输出录像文件
val mediaStoreOutputOptions = MediaStoreOutputOptions.Builder(
contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI
).setContentValues(contentValues).build()

// 设置录像事件监听,并创建录像实例开始录像
recording = videoCapture.output.prepareRecording(this, mediaStoreOutputOptions).apply { // 准备
// 跟据录音权限决定是否录音
if (PermissionChecker.checkSelfPermission(
this@MainActivity, Manifest.permission.RECORD_AUDIO
) == PermissionChecker.PERMISSION_GRANTED
) {
withAudioEnabled()
}
}.start(ContextCompat.getMainExecutor(this)) { recordEvent -> // 设置录像事件监听并开始录像,触发开始事件
when (recordEvent) {
// 开始事件
is VideoRecordEvent.Start -> {
viewBinding.videoCaptureButton.apply { // 重新启用按钮
text = getString(R.string.stop_capture)
isEnabled = true
}
}
// 结束事件
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
val msg = "录像成功:" + "${recordEvent.outputResults.outputUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
} else {
recording?.close()
recording = null
Log.e(TAG, "录像出错: " + "${recordEvent.error}")
}
viewBinding.videoCaptureButton.apply { // 重新启用按钮
text = getString(R.string.start_capture)
isEnabled = true
}
}
}
}
}

private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}

}
影像处理
浅尝 010 Editor 的模板编写
© 2024 Lyana-nullptr
Powered by hexo | Theme is blank