DartのImageライブラリを使って画像をバイト単位を弄る方法

概要

DartのImageライブラリを使えば、画像の切り抜きや合成、色味の編集、エンボス加工などさまざまな画像処理が行えます。 とはいえ用意されているメソッドは全てを網羅しているわけではなく、例えばクロマキーで透過画像を生成することなど、意外とできないことも多いです。

そのような用意されていないことをしたい場合にはバイト配列を取り出して自分で加工する必要があります。

公式のドキュメントには、画像から取り出した時のバイト配列の仕様など説明がなかったようなので、 本記事では、バイト配列を取り出して加工する方法について紹介します。

※ 具体的な画像の加工アルゴリズムについてはここでは紹介しません。

免責事項

実装中のコードから紹介できる形に加工しているだけなので、そのままコピペしても動かない可能性があります。 また、紹介しているプログラムや方法を流用した際のあらゆるトラブルについて責任を負いませんので、必ず記事やプログラムの内容を確認した上でご参考にしてください。

実行環境

name version
Dart 3.2.4
Flutter 3.16.7
image 4.1.4

pub.dev

手順

step0: Imageインスタンスを生成する

適当な方法で画像を読み込んでデコードします。

import 'package:image/image.dart' as img;

Future<img.Image?> readImage() async {
  final XFile? imageFile = await ImagePicker().pickImage(
    source: ImageSource.gallery,
    imageQuality: 100,
  );
  final bytes = await imageFile?.readAsBytes();
  if (bytes == null) {
    return null;
  }

  return img.decodeImage(bytes)?.convert(numChannels: 4);
}

step1: バイト配列を取り出す

orderを渡すことで、バイト配列の並びを指定できます。 rgba以外にもbgraなどもあるそうです。 いつ使うんだろう。

ChannelOrder enum - image library - Dart API

rgbaを指定した場合はalphaを指定できます。 alphaチャンネルが必要なければ省略可能です。

Uint8List getBytes(img.Image image) {
  return image.getBytes(order: img.ChannelOrder.rgba, alpha: 255);
}

step2: 加工する

得られたバイト配列はサイズが(width * height) * channelSizeUint8Listになります。 配列の中にはヘッダなどは含まれず、各画素の色情報のみが含まれます。 各画素は、色情報が色チャンネル数分連続して並んでいて、左上の画素から右下の画素まで順番に並んでいます。

ChannelOrder.rgbaUint8Listからある画素(x, y)の色を取り出したい場合は、下記のような実装になります。

int getIndex({
  required int x,
  required int y,
  required int width,
  required int height,
}) =>
    y * width * 4 + x * 4;

img.ColorUint8 getColor({
  required Uint8List bytes,
  required int index,
}) {
  final int r = bytes[index];
  final int g = bytes[index + 1];
  final int b = bytes[index + 2];
  final int a = bytes[index + 3];

  return img.ColorUint8.rgba(r, g, b, a);
}

上記メソッドを使って例えばネガポジ反転を実装するとこうなります。

Uint8List invertColor({
  required Uint8List bytes,
  required int width,
  required int height,
}) {
  Uint8List inverted = Uint8List(bytes.length);
  for (var x = 0; x < width; x++) {
    for (var y = 0; y < height; y++) {
      final int index = getIndex(
        x: x,
        y: y,
        width: width,
        height: height,
      );

      final img.ColorUint8 color = getColor(
        bytes: bytes,
        index: index,
      );

      inverted[index] = 255 - color.r.toInt();
      inverted[index + 1] = 255 - color.g.toInt();
      inverted[index + 2] = 255 - color.b.toInt();
      inverted[index + 3] = color.a.toInt();
    }
  }
  return inverted;
}

step3: バイト配列からImageオブジェクトに変換する

最後に加工したbytesからImageに戻します。 注意すべき点はbytesをそのまま渡すのではなくByteBufferを取り出して渡さないといけない点です。

他は加工したbytesデータに合わせて設定すればOKです。

final img.Image invertedImage = img.Image.fromBytes(
  bytes: inverted.buffer,
  width: image.width,
  height: image.height,
  numChannels: 4,
  order: img.ChannelOrder.rgba,
  format: img.Format.uint8,
);

結果

付録

ソースコードはこちら。 iOSで動作確認しています。 github.com