3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

画像にモザイク処理を行う

Posted at

作った理由

AGSLを使えばShaderを使ってエフェクトを書けることができ、下記の記事のようにAGSLを使ってモザイクをかける処理を実装されている方もいます。
AGSLで遊んでみる

しかし、AGSLはAndroid13以降でないと対応しておらず、プロジェクト作成時に確認できるデバイスの割合も33%程度は留まっており、別の方法で実装可能か確認がてら作ってみました。

実装例

今回はこちらのページを参考に、カスタムペインタとして実装しています。特筆すべきことも無いので、処理の内容はコメントを見てください。
カスタム ペインタ

MosaicPainter.kt
class MosaicPainter(
    private val image: ImageBitmap,
    private val blockSize: Int
) : Painter() {

    override val intrinsicSize: Size get() = Size(image.width.toFloat(), image.height.toFloat())

    override fun DrawScope.onDraw() {

        // 画像と画面のサイズ比を取得
        val scale = image.width / size.width

        // ある程度ブロックサイズが大きい場合にモザイク処理を行う
        if (blockSize > 5) {
            for (x in 0 until size.width.roundToInt() step blockSize) {
                for (y in 0 until size.height.roundToInt() step blockSize) {

                    // 画像内での値に変換
                    val imageX = (x * scale).roundToInt()
                    val imageY = (y * scale).roundToInt()
                    val imageBlockSize = (blockSize * scale).roundToInt()

                    // 読み込む範囲を画像サイズ内に収める
                    val blockWidth = if (imageX + imageBlockSize < image.width) {
                        imageBlockSize
                    } else {
                        image.width - imageX
                    }
                    val blockHeight = if (imageY + imageBlockSize < image.height) {
                        imageBlockSize
                    } else {
                        image.height - imageY
                    }

                    // BitmapImageから指定範囲を読み込み
                    val buffer = IntArray(blockWidth * blockHeight)
                    image.readPixels(
                        buffer = buffer,
                        startX = imageX,
                        startY = imageY,
                        width = blockWidth,
                        height = blockHeight
                    )

                    // 範囲内の色を平均化しブロックを描画
                    drawRect(
                        averagingColor(buffer),
                        Offset(x.toFloat(), y.toFloat()),
                        Size(blockSize.toFloat(), blockSize.toFloat())
                    )
                }
            }
        } else {
            // ブロックサイズが小さい場合は画像をそのまま描画
            drawImage(
                image = image,
                dstSize = IntSize(
                    size.width.roundToInt(),
                    size.height.roundToInt()
                )
            )
        }
    }
}

/**
 * 平均化したColorを返す
 */
private fun averagingColor(
    buffer: IntArray
): Color {
    var r = 0L
    var g = 0L
    var b = 0L
    buffer.forEach {
        r += (it shr 16) and 0xff
        g += (it shr 8) and 0xff
        b += it and 0xff
    }
    return Color(
        (r / buffer.size).toInt(),
        (g / buffer.size).toInt(),
        (b / buffer.size).toInt())
}

呼び出し方

MainContent.kt
@Composable
fun MainContent() {

    val bitmap = ImageBitmap.imageResource(R.drawable.gundam)
    var blockSize by remember { mutableIntStateOf(0) }
    val mosaicPainter = remember(blockSize) {
        MosaicPainter(bitmap, blockSize)
    }

    Box() {
        Image(
            modifier = Modifier.align(Alignment.Center),
            painter = mosaicPainter,
            contentDescription = null
        )
        Box(
            modifier = Modifier
                .wrapContentSize()
                .align(Alignment.BottomCenter)
                .padding(all = 32.dp)
                .clip(shape = RoundedCornerShape(50))
                .background(color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f))
        ) {
            Slider(
                modifier = Modifier
                    .align(Alignment.Center)
                    .padding(horizontal = 16.dp),
                value = blockSize.toFloat(),
                valueRange = 0f..100f,
                steps = 9,
                onValueChange = { blockSize = it.roundToInt() }
            )
        }
    }
}

実際の動作

画像はららぽーとで撮った𝛎ガンダムです。

Screen_recording_20241226_145104-ezgif.com-video-to-gif-converter.gif

作ってみて

ブロックが小さくなると若干もたつく感じはするものの、思ってたよりはスムーズに動作しました。ただ、カスタム ペインタのページに下記の注意書きがあるため、DrawModifierを使った実装も試してみたいと思います。

測定またはレイアウトに影響を与える必要がある場合は、Painter を使用する必要があります。指定の境界内でしかレンダリングを行わない場合は、DrawModifier を使用してください。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?