
Android之相册开发,图片美化处理,基于图片创作影片,是现代Android社交中的一项重要的功能。
一、前言
Android图片美化处理的意义主要体现在以下几个方面:
- 增强用户体验:通过美化图片,可以显著提升用户的使用体验。美化后的图片在构图、色彩和细节上都能得到优化,使得整体画面通透、明亮,提升了视觉享受。
- 展现个性:美化处理的图片能够增加社交媒体垂直账号的独特个性,增强归属感,还能展示用户的审美品味和兴趣爱好。通过选择不同的壁纸背景,用户可以在社交场合中留下深刻印象1。
- 提升照片质量:通过美图功能,可以对照片进行裁剪、滤镜调节、亮度对比度调整等操作,从而提升照片的整体美观和质量。即使在光线条件不佳的情况下,也能轻松处理拍摄出的照片,使其焕发出炫目的艺术魅力2。
- 社会心理影响:在社交媒体上,经过精心修饰的照片往往能吸引更多的关注和点赞,成为个人形象和社交资本的一部分。这种对"精修"的追求反映了人们对于视觉享受和社会认同的需求。
- 技术进步的体现:随着智能手机摄像头的升级和修图软件的普及,人们开始有能力对自己的照片进行"精修",从简单的磨皮美白到复杂的场景替换、风格转换,修图技术让每一张照片都有可能变成一幅艺术品3
本文重点从以下4个维度
对图片处理进行介绍:
- 多张图片拼合成一张
- 多张图片合成视频影片
- 图片的裁剪
- 图片大小缩放
- 图片的滤镜:色调,饱和度,亮度调整
二、多张图片拼合成一张
- Android 原生对图片合并处理主要是对突变bitmap进行处理:
如下代码:是从左到右依次拼接
,
当然可以调整成:
从上到下依次拼接
,
上下规则形网格,或者不规则形网格自定义拼接
kotlin
private fun mergeImages(bitmaps: List<Bitmap>): Bitmap {
var totalWidth = 0
var maxHeight = 0
bitmaps.forEach { bitmap ->
totalWidth += bitmap.width //从左到右依次拼接
if (bitmap.height > maxHeight) maxHeight = bitmap.height
}
val result = Bitmap.createBitmap(totalWidth, maxHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
var currentX = 0
bitmaps.forEach { bitmap ->
canvas.drawBitmap(bitmap, currentX.toFloat(), 0f, null)
currentX += bitmap.width
}
return result
}
- 上面返回成bitmap 可以直接展示预览使用,也可以通过如下代码保存起来,
注意文件读写权限
kotlin
private fun saveToLocal(bitmap: Bitmap) {
val path = Environment.getExternalStorageDirectory().absolutePath + "/merged.jpg"
FileOutputStream(path).use { fos ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos)
}
}
三、多张图片生成视频:
Android中通过图片生成视频步骤:
-
将图片先转成bitmap,然后再将bitmap转成YUV420(它是图像色彩存储的一种常见格式,如需要深入需要对视频编码原理进行自我相关知识补充)
-
通过原生
MediaCodec
对图片处理后的YUV420数据进行编码,编码结束通过MediaMuxer
对编码后的数据进行封装成Mp4格式,也可以添加相关音频文件作为背景音乐。 -
关于
MediaCodec
和MediaMuxer
相关Api功能介绍,请参考我之前的文章: -
需要注意的是
:每张图片的宽高需要保持一致,如果大小不一,可以先自行进行相关放大缩小处理 -
具体代码如下:
scss
class VideoEncoder(
private val outputPath: String, //视频输出路径
private val bitmaps: List<Bitmap>,//图片集
private val deltime: Int = 3000000, //每隔3秒切换一张图片
private val frameRate: Int = 30 //视频帧率1秒30帧
) {
private lateinit var mediaCodec: MediaCodec
private lateinit var mediaMuxer: MediaMuxer
private var trackIndex = -1
private var isMuxerStarted = false
private val timeoutUs = 10000L
private var mPresentationTime: Long = 0
private val KEY_I_FRAME_INTERVAL = 15
private val ptdivt = 1000000L
fun encode() {
val width = bitmaps.first().width
val height = bitmaps.first().height
// 1. 视频编码器相关配置
val mediaFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height).apply {
setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible)
// 视频码率 width heigth 帧 码率
setInteger(MediaFormat.KEY_BIT_RATE, width * height)
// 视频帧率 1秒 30帧
setInteger(MediaFormat.KEY_FRAME_RATE, frameRate)
// 视频每隔15帧一个I帧
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, KEY_I_FRAME_INTERVAL)
}
// 2. 初始化组件
mediaCodec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC).apply {
configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
start()
}
mediaMuxer = MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
val count = deltime / (ptdivt / frameRate)
bitmaps.forEachIndexed { index, bitmap ->
val yuvData = bitmap.convertToYuv420()
for (i in 0 until count) {
mPresentationTime = computePresentationTime((count * index + i) * 1L)
// 输入缓冲区处理
mediaCodec.dequeueInputBuffer(timeoutUs).takeIf { it >= 0 }?.also { idx ->
mediaCodec.getInputBuffer(idx)?.put(yuvData)
mediaCodec.queueInputBuffer(idx, 0, yuvData.size, mPresentationTime, 0)
}
writeOutput()
}
}
// 5. 释放资源
mediaCodec.stop()
mediaCodec.release()
if (isMuxerStarted) mediaMuxer.stop()
mediaMuxer.release()
}
private fun writeOutput() {
while (true) {
val bufferInfo = MediaCodec.BufferInfo()
// 输出缓冲区处理
val bufferIndex = mediaCodec!!.dequeueOutputBuffer(bufferInfo, timeoutUs)
if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
trackIndex = mediaMuxer.addTrack(mediaCodec!!.outputFormat)
mediaMuxer.start()
continue
}
if (bufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
break
}
if (trackIndex < 0) return
val buff = mediaCodec!!.getOutputBuffer(bufferIndex)
if (!((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0 && bufferInfo!!.size != 0)) {
mediaMuxer.writeSampleData(trackIndex, buff!!, bufferInfo)
}
mediaCodec!!.releaseOutputBuffer(bufferIndex, false)
}
}
//计算出每张图片PresentationTime的视频编码的时间
private fun computePresentationTime(index: Long): Long {
return index * ptdivt / frameRate
}
// Bitmap转YUV420(关键实现)
private fun Bitmap.convertToYuv420(): ByteArray {
val yuv = ByteArray((width * height * 1.5).toInt())
val rgba = IntArray(width * height)
getPixels(rgba, 0, width, 0, 0, width, height)
var yIndex = 0
var uvIndex = width * height
for (i in 0 until height) {
for (j in 0 until width) {
val pixel = rgba[i * width + j]
val r = (pixel shr 16) and 0xFF
val g = (pixel shr 8) and 0xFF
val b = pixel and 0xFF
// Y分量
yuv[yIndex++] = (((66 * r + 129 * g + 25 * b + 128) shr 8) + 16).toByte()
// UV分量(每2x2像素采样一次)
if (i % 2 == 0 && j % 2 == 0) {
yuv[uvIndex++] = (((-38 * r - 74 * g + 112 * b + 128) shr 8) + 128).toByte()
yuv[uvIndex++] = (((112 * r - 94 * g - 18 * b + 128) shr 8) + 128).toByte()
}
}
}
return yuv
}
}
- 需要注意的是上面这个位置: 理论上是不需要对同一张图片的yuv数据进行重复编码, 如果图片比较少情况下,根据每张图片索引
index 计算出的时间 computePresentationTime
会很短,可能都不到1秒时间,这样视频时长很短,播放不了。
scss
for (i in 0 until count) {
mPresentationTime = computePresentationTime((count * index + i) * 1L)
// 输入缓冲区处理
mediaCodec.dequeueInputBuffer(timeoutUs).takeIf { it >= 0 }?.also { idx ->
mediaCodec.getInputBuffer(idx)?.put(yuvData)
mediaCodec.queueInputBuffer(idx, 0, yuvData.size, mPresentationTime, 0)
}
writeOutput()
注意文件读写权限
四、图片裁剪
Android 原生对图片进行裁剪主要是对bitmap的处理,配置剪裁目标宽高:
kotlin
/**
* 图片裁剪
* @param originalBitmap: 原始图片bitmap
* @param x: x为起始点坐标
* @param y: y为起始点坐标
* @param width: 裁剪区域的宽
* @param height: 裁剪区域的高
*
*/
fun imageCropper(originalBitmap: Bitmap, x: Int, y: Int, width: Int, height: Int) {
var croppedBitmap: Bitmap = Bitmap.createBitmap(originalBitmap, x, y, width, height)
}
五、图片放大缩小成指定尺寸后保存
Android 图片缩放主要是对Bitmap.scale进行操作,指定缩放后目标图片宽高,注意文件读写权限
,代码如下
kotlin
/**图片放大缩小成指定尺寸后保存
* @param imagePath: 原始图片路径
* @param targetWidth: 缩放后的宽度
* @param targetHeight: 缩放后的高度
* @param outputPath: 图片输出路径
*/
fun resizeAndSaveImage(imagePath: String, targetWidth: Int, targetHeight: Int, outputPath: String) {
// 加载原始图片
val options = BitmapFactory.Options()
options.inPreferredConfig = Bitmap.Config.ARGB_8888
val originalBitmap = BitmapFactory.decodeFile(imagePath, options)
// 缩放图片
val resizedBitmap = originalBitmap.scale(targetWidth, targetHeight, false)
originalBitmap.recycle() // 回收原始位图资源
// 保存图片到指定路径
val outputFile = File(outputPath) // 可以根据需要更改文件名和格式
try {
FileOutputStream(outputFile).use { out ->
resizedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) // 100表示最高质量,可以根据需要调整
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
resizedBitmap.recycle() // 回收位图资源
}
}
六、图片滤镜色调,饱和度,亮度调整
Android 原始图片的滤镜也是对bitmap进行处理,结合ColorMatrixColorFilter
和ColorMatrix
使用
-
ColorMatrixColorFilter
: 是Android 开发中用于图形处理的一个重要工具,它通过颜色矩阵(ColorMatrix
)来实现复杂的颜色变换效果。ColorMatrixColorFilter
属于Paint API
的一部分,主要用于改变绘制的颜色,包括调整亮度、对比度、饱和度,或者应用特定的视觉效果如黑白、复古等 -
ColorMatrix
是一种用于图像处理的技术,主要通过调整图像的RGB(红、绿、蓝)通道来改变图像的颜色效果。ColorMatrix是一个5x4的矩阵
,可以通过修改矩阵中的元素来改变图像中RGBA各分量的值,从而实现各种颜色效果。 -
色调、饱和度、亮度 通过
ColorMatrix的PostConcat相乘
,对原始图片进行变换处理使用代码如下:
kotlin
/**
* 色调、饱和度、亮度 通过ColorMatrix的PostConcat相乘,对原始图片进行变换处理
* @param oriBmp
* @param hue 色调调节范围 -180度至180度
* @param saturation 取值范围是0到1 其中0表示灰度图像,1表示原图,取值越大,色彩饱和度越高
* @param lum
* @return
*/
fun handleImageEffect(oriBmp: Bitmap, hue: Float, saturation: Float, lum: Float): Bitmap {
val bmp = createBitmap(oriBmp.width, oriBmp.height)
val canvas = Canvas(bmp)
val paint: Paint = Paint()
//调节色调
val hueMatrix = ColorMatrix()
hueMatrix.setRotate(0, hue) //围绕red旋转 hue角度
hueMatrix.setRotate(1, hue) //围绕green旋转 hue角度
hueMatrix.setRotate(2, hue) //围绕blue旋转 hue角度
//调节饱和度
val saturationMatrix = ColorMatrix()
saturationMatrix.setSaturation(saturation)
//调节亮度
val lumMatrix = ColorMatrix()
//setScale方法接受四个参数:rScale、gScale、bScale和aScale,分别表示红色、绿色、蓝色和透明度的缩放比例
lumMatrix.setScale(lum, lum, lum, 1)
val imageMatrix = ColorMatrix()
imageMatrix.postConcat(hueMatrix)
imageMatrix.postConcat(saturationMatrix)
imageMatrix.postConcat(lumMatrix)
paint.setColorFilter(ColorMatrixColorFilter(imageMatrix))
canvas.drawBitmap(oriBmp, 0f, 0f, paint)
return bmp
}
4. ColorMatrix.setRotate
方法:用于设置图像的色调旋转。该方法接受两个参数:轴(axis
)和旋转角度(degree
)。
其中参数:
- axis:指定旋转的轴,可以是0、1或2,分别代表红色(R)、绿色(G)、蓝色(B)。
- degree:旋转的角度,取值范围为-180度至180度。
ColorMatrix的setSaturation
方法用于设置图像的饱和度。ColorMatrix
是一个4x5的矩阵,通过矩阵乘法和加法实现颜色的转换。setSaturation
方法:通过调整颜色矩阵中的元素来改变图像的饱和度。具体来说,setSaturation
方法会将颜色矩阵重置为一个初始状态,然后根据传入的饱和度参数调整R、G、B三个颜色的权重,从而实现饱和度的变化。
- 饱和度参数
sat
的取值范围是: 0到1,其中0表示灰度图像,1表示原图,取值越大,色彩饱和度越高
ColorMatrix.setScale
方法用于设置颜色矩阵的比例缩放值。
该方法接受四个参数:
rScale、gScale、bScale和aScale
,分别表示红色、绿色、蓝色和透明度的缩放比例
- 也可以直接通过
ColorMatrix.set(矩阵数组)
:比如:将矩阵中的某些值设置为0可以实现黑白效果
scss
黑白效果
val mBWMatrix = floatArrayOf(
1f, 0f, 0f, 0f, 0f,
0f, 1f, 0f, 0f, 0f,
0f, 0f, 1f, 0f, 0f,
0f, 0f, 0f, 1f, 0f
)
//设置矩阵数组
fun setImageMatrix(mBitmap:Bitmap,mColorMatrix: FloatArray) {
val bmp = createBitmap(mBitmap.getWidth(), mBitmap.getHeight())
val colorMatrix = ColorMatrix()
colorMatrix.set(mColorMatrix)
val canvas = Canvas(bmp)
val paint = Paint()
paint.setColorFilter(ColorMatrixColorFilter(colorMatrix))
canvas.drawBitmap(mBitmap, 0f, 0f, paint)
ivImage.setImageBitmap(bmp)
}
七、总结
本文重点介绍了:
- 如何实现多张图片拼合成一张
- 如何实现多张图片合成视频影片
- 如何实现图片的裁剪
- 如何实现图片大小缩放
- 如何实现图片的滤镜:色调,饱和度,亮度调整