Android图片处理:多合一,多张生成视频,裁剪,放大缩小,滤镜色调,饱和度,亮度调整

Android之相册开发,图片美化处理,基于图片创作影片,是现代Android社交中的一项重要的功能。

一、前言

Android图片美化处理的意义‌主要体现在以下几个方面:

  1. 增强用户体验‌:通过美化图片,可以显著提升用户的使用体验。美化后的图片在构图、色彩和细节上都能得到优化,使得整体画面通透、明亮,提升了视觉享受‌。
  2. 展现个性‌:美化处理的图片能够增加社交媒体垂直账号的独特个性,增强归属感,还能展示用户的审美品味和兴趣爱好。通过选择不同的壁纸背景,用户可以在社交场合中留下深刻印象‌1。
  3. 提升照片质量‌:通过美图功能,可以对照片进行裁剪、滤镜调节、亮度对比度调整等操作,从而提升照片的整体美观和质量。即使在光线条件不佳的情况下,也能轻松处理拍摄出的照片,使其焕发出炫目的艺术魅力‌2。
  4. 社会心理影响‌:在社交媒体上,经过精心修饰的照片往往能吸引更多的关注和点赞,成为个人形象和社交资本的一部分。这种对"精修"的追求反映了人们对于视觉享受和社会认同的需求‌。
  5. 技术进步的体现‌:随着智能手机摄像头的升级和修图软件的普及,人们开始有能力对自己的照片进行"精修",从简单的磨皮美白到复杂的场景替换、风格转换,修图技术让每一张照片都有可能变成一幅艺术品‌3

本文重点从以下4个维度 对图片处理进行介绍:

  1. 多张图片拼合成一张
  2. 多张图片合成视频影片
  3. 图片的裁剪
  4. 图片大小缩放
  5. 图片的滤镜:色调,饱和度,亮度调整

二、多张图片拼合成一张

  1. 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
}
  1. 上面返回成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中通过图片生成视频步骤:

  1. 将图片先转成bitmap,然后再将bitmap转成YUV420(它是图像色彩存储的一种常见格式,如需要深入需要对视频编码原理进行自我相关知识补充)

  2. 通过原生MediaCodec对图片处理后的YUV420数据进行编码,编码结束通过MediaMuxer对编码后的数据进行封装成Mp4格式,也可以添加相关音频文件作为背景音乐。

  3. 关于MediaCodecMediaMuxer相关Api功能介绍,请参考我之前的文章:

    (一)如何拦截其他Android应用程序播放器的原始音频数据自定义保存下来?

    (二)Android拦截其它播放声音:内录音,外录音,录屏,剪辑,混音,一键制作大片全搞定

  4. 需要注意的是:每张图片的宽高需要保持一致,如果大小不一,可以先自行进行相关放大缩小处理

  5. 具体代码如下:

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
    }
}
  1. 需要注意的是上面这个位置: 理论上是不需要对同一张图片的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()
  1. 注意文件读写权限

四、图片裁剪

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进行处理,结合ColorMatrixColorFilterColorMatrix使用

  1. ColorMatrixColorFilter : 是Android 开发中用于图形处理的一个重要工具,它通过颜色矩阵(ColorMatrix)来实现复杂的颜色变换效果。ColorMatrixColorFilter 属于Paint API的一部分,主要用于改变绘制的颜色,包括调整亮度、对比度、饱和度,或者应用特定的视觉效果如黑白、复古等‌

  2. ColorMatrix 是一种用于图像处理的技术,主要通过调整图像的RGB(红、绿、蓝)通道来改变图像的颜色效果。ColorMatrix是一个5x4的矩阵,可以通过修改矩阵中的元素来改变图像中RGBA各分量的值,从而实现各种颜色效果。

  3. 色调、饱和度、亮度 通过 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,分别表示红色、绿色、蓝色和透明度的缩放比例
  1. 也可以直接通过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)
}

七、总结

本文重点介绍了:

  1. 如何实现多张图片拼合成一张
  2. 如何实现多张图片合成视频影片
  3. 如何实现图片的裁剪
  4. 如何实现图片大小缩放
  5. 如何实现图片的滤镜:色调,饱和度,亮度调整

相信对你会有一定帮助

感谢阅读:

欢迎用你发财的小手 关注,点赞、收藏

这里你会学到不一样的东西

相关推荐
jiaxingcode2 分钟前
MAC系统下完全卸载Android Studio
android·macos·android studio
张力尹8 分钟前
「架构篇 1」认识 MVC / MVP / MVVM / MVI
android·面试·架构
张力尹12 分钟前
「架构篇 2」认识 MVC / MVP / MVVM / MVI
android·面试·架构
天天扭码31 分钟前
RESTful API 接口设计小白入门:从概念到实战全解析
面试·架构·restful
不穿铠甲的穿山甲32 分钟前
Android-KeyStore安全的存储系统
android·安全
老马95271 小时前
8.snail-job的重试任务
后端·架构
dora2 小时前
Flutter的屏幕适配
android·flutter
SoulKuyan2 小时前
android 发送onkey广播,Android 添加键值并上报从驱动到上层
android
Yang-Never2 小时前
ADB->查看某个应用的版本信息
android·adb·android studio