Drawing the Difference of two Images to Canvas
Currently flutter does not support what you are trying to do. Based on my research, flutter handles canvas paint on the native level. You can see this in action from this snippet:
/// Fills the canvas with the given [Paint].
///
/// To fill the canvas with a solid color and blend mode, consider
/// [drawColor] instead.
void drawPaint(Paint paint) {
assert(paint != null);
_drawPaint(paint._objects, paint._data);
}
void _drawPaint(List<dynamic> paintObjects, ByteData paintData) native 'Canvas_drawPaint';
This is from the canvas implementation on the latest version of flutter (1.9.2). Any pixel manipulation that you can do without async code must be done utilizing flutter BlendMode
or ImageFilter
which lacks personalized masks support.
The only way to do what you intend to do is to actually implement native code, which I don't really think you want to do. I spent the last 6 hours trying to figure out if there wasn't anything out of the official flutter API docs that would do what you want it to do, but the thing is, most of things in flutter are just intended to be async.
If you are going to venture into implementing this natively, why don't you ask to be implemented as a feature in the official flutter github? I mean, so much work, don't let it be wasted.
Just because I didn't wanted to leave empty handed, I have written precisely what you wanted with the image lib but using async:
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:image/image.dart' as im;
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MyApp();
}
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
ByteBuffer imgBytes;
double canvasHeight = 500;
double canvasWidth = 300;
ui.PictureRecorder recorder1;
Canvas canvas1;
ui.PictureRecorder recorder2;
Canvas canvas2;
ui.Picture picture1;
ui.Picture picture2;
@override
void initState() {
// Canvas1
recorder1 = ui.PictureRecorder();
canvas1 = Canvas(recorder1,
Rect.fromPoints(Offset(0.0, 0.0), Offset(canvasWidth, canvasHeight)));
// Draw red background
var paint = Paint();
paint.style = PaintingStyle.fill;
paint.color = Colors.red;
canvas1.drawRect(Offset(0, 0) & Size(canvasWidth, canvasHeight), paint);
// Draw black square
paint = Paint();
paint.style = PaintingStyle.fill;
paint.color = Colors.black;
canvas1.drawRect(
Offset(canvasWidth * 0.5 / 4, canvasHeight / 2) &
Size(canvasHeight / 8, canvasHeight / 8),
paint);
picture1 = recorder1.endRecording();
// Canvas2
recorder2 = ui.PictureRecorder();
canvas2 = Canvas(recorder2,
Rect.fromPoints(Offset(0.0, 0.0), Offset(canvasWidth, canvasHeight)));
// Draw red background
paint = Paint();
paint.style = PaintingStyle.fill;
paint.color = Colors.red;
canvas2.drawRect(Offset(0, 0) & Size(canvasWidth, canvasHeight), paint);
// Draw blue square
paint = Paint();
paint.style = PaintingStyle.fill;
paint.color = Colors.blue;
canvas2.drawRect(
Offset(canvasWidth * 2.5 / 4, canvasHeight / 2) &
Size(canvasHeight / 8, canvasHeight / 8),
paint);
picture2 = recorder2.endRecording();
(() async {
ui.Image img1 =
await picture1.toImage(canvasWidth.toInt(), canvasHeight.toInt());
ByteData byteData1 =
await img1.toByteData(format: ui.ImageByteFormat.png);
im.Image decodedPng1 = im.decodePng(byteData1.buffer.asUint8List());
ui.Image img2 =
await picture2.toImage(canvasWidth.toInt(), canvasHeight.toInt());
ByteData byteData2 =
await img2.toByteData(format: ui.ImageByteFormat.png);
im.Image decodedPng2 = im.decodePng(byteData2.buffer.asUint8List());
for (int i = 0; i < canvasHeight; i += 1) {
for (int j = 0; j < canvasWidth; j += 1) {
int pixel1 = decodedPng1.getPixel(j, i);
int r1 = pixel1 & 0xff;
int g1 = (pixel1 >> 8) & 0xff;
int b1 = (pixel1 >> 16) & 0xff;
int a1 = (pixel1 >> 24) & 0xff;
int pixel2 = decodedPng2.getPixel(j, i);
int r2 = pixel2 & 0xff;
int g2 = (pixel2 >> 8) & 0xff;
int b2 = (pixel2 >> 16) & 0xff;
int a2 = (pixel2 >> 24) & 0xff;
if (r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2) {
// Draw black transparent
decodedPng2.setPixel(j, i, 0);
} else {
// Leave as is
}
}
}
setState(() {
imgBytes = Uint8List.fromList(im.encodePng(decodedPng2)).buffer;
});
})();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Dummy'),
),
body: imgBytes != null
? Center(
child: Image.memory(
Uint8List.view(imgBytes),
width: canvasWidth,
height: canvasHeight,
),
)
: Container(),
),
);
}
}
creativecreatorormaybenot
Updated on December 15, 2022Comments
-
creativecreatorormaybenot over 1 year
I am trying to draw the difference of two
Image
s I have to theCanvas
in Flutter and what I really mean is that I only want to draw what is different (you can see what I expect to see below).
It needs to happen synchronously, i.e. in apaint
call!In this answer, the two images are referred to as
imageA
andimageB
.Code
canvas.drawImage( imageA, offset, Paint(), ); canvas.drawImage( imageB, offset, Paint() ..blendMode = BlendMode.difference, );
Visualization
imageA
andimageB
drawn separately.Difference between
imageA
andimageB
. You can see what I expect to see left and what I actually get using the code above right.
The gray/white background color in the left image is supposed to be transparent, i.e. alpha should equal zero where the two images are the same. Additionally, the black shadow around the red square in theexpected
image is an artifact and not what I want in my app.Problem
I am not saying that the
expected
image is what I expect to get fromBlendMode.difference
, but I want to know how I can get theexpected
output, i.e. how I achieve only drawing what is different between two images.
This means that I want to only render pixels fromimageB
that are different fromimageA
and otherwise nothing, i.e. alpha value 0.Explanation
I will try to explain it a bit more clearly again:
If the color is the same, draw transparent pixel (remove source pixel).
If the color is different, draw destination pixel.
-
rmtmckenzie over 4 yearsDoes this need to happen in real time? It's possible to do something like what you're asking for, but you're going to need to use the image library which is quite slow. Essentially the issue at hand is that blend modes won't actually get to what you need - if you think about it, it makes sense because how would it decide what rgba(10, 0, 0, 255) - rgba(0, 10, 0, 255) should be without special logic.
-
creativecreatorormaybenot over 4 years@rmtmckenzie It needs to happen in real-time, i.e. in the
paint
method as I mentioned in the question (it does not accept asynchronous code). I can easily do the job manually asynchronously by comparing individual pixels and encoding a new image, but this will cause lag as it does not work inpaint
and is too slow (btw, I am a contributor to theimage
library, but I do not think that it offers functions for this). -
creativecreatorormaybenot over 4 years@rmtmckenzie It is not difficult: if the color is the same, remove it (transparent pixels), otherwise draw the destination image. I think there could actually be a blend mode for this, but I could not find one that does it with color, only with shapes.
-
szotp over 4 yearsWhat if difference is tiny, for example src: #ffffff, dst: #fffffe, should it still draw destination pixel?
-
creativecreatorormaybenot over 4 years@szotp Yes! That is the idea. Always draw destination if not the same.
-
Kent over 4 yearswhy did left change from black to red but right stayed blue? What is your thinking for the algorithm that would make this result?
-
creativecreatorormaybenot over 4 years@kent It is just the destination image (
imageB
). So when source and destination are different, destination should be drawn. This means that no complicated algorithm is even required. If it was different, then we would need some kind of complicated algorithm. The part that is black inimageA
is red inimageB
, hence, it is red in the result. A square that is red inimageA
is red but blue inimageB
, hence the result shows the blue square. The rest of the images is identical, hence, it is transparent in the result. -
Kent over 4 yearsok I think I get it. if(A = B) then Transparent else if (A != B) then B. This can be done by loading the raw pixels into ram and then doing a compare of each pixel. It might be expensive depending on the size of the image.
-
creativecreatorormaybenot over 4 years@kent I mentioned a few times here that this is what I do at the moment, but I need a synchronous way for performance reasons..
-
Kent over 4 years@creativecreatorormaybenot Just double checking you need an async way of doing this? because the way your previous message reads it seems like you are saying you want an sync way. Can you provide the code you have that is working? code shown is the actual (bottom right) correct?
-
creativecreatorormaybenot over 4 years@kent Where do I say I need an
async
way :D? I need async
, i.e. synchronous, way. You can find example code in one of the answers (I do it like this as well). To the last question: incorrect. The bottom right shows an attempt of making it synchronous usingBlendMode
, but it obviously does not work. The bottom left shows what I currently have usingasync
functions. -
Kent over 4 years@creativecreatorormaybenot Thats why I was clarifying. Because usually when asking for more performance in code we use async not sync.
-
Kent over 4 yearsLet us continue this discussion in chat.
-
creativecreatorormaybenot over 4 years
-
creativecreatorormaybenot over 4 yearsThank you so much for your answer - the problem is that I have this already. It is hard for me to imagine that this is not possible using regular
Canvas
operations - I think that I will resort to my previousasync
implementation. I kinda have to useasync
code anyway because Flutter does not allow encoding images synchronously. See this issue. -
Nicolas Caous over 4 yearsAbout the possibility, it is 100% possible, the thing is that no one bothered to implement this feature on the native canvas implementation of the flutter engine.
-
creativecreatorormaybenot over 4 yearsI mean non-native code. I might look into adjusting the engine after doing some other optimizations using
dart:ffi
, maybe. -
Nicolas Caous over 4 yearsBut anyway, why do you need to optimize this? Are you working with huge images? Also, there is the [
flutter_ffmpeg
][pub.dev/packages/flutter_ffmpeg] package that might be helpful. -
creativecreatorormaybenot over 4 yearsBecause I need to paint pixel data - I doubt
ffmpeg
can help me with that. You might not be aware of just how slow this is in Flutter. It is basically not usable - I cannot go further than 50k pixels if I want to encode pixels in real time. See the issue I linked before or this. -
Nicolas Caous over 4 yearsI see now what you mean.
-
Nicolas Caous over 4 yearsSo, I have been thinking about this problem for quite a while now. I remembered that you can achieve the desired results quite easily using opencv (some bitwise operations and a mask should be a piece of cake). Maybe you could create a opencv binary and statically link it to your
dart:ffi
plugin. The only down side to this is that a statically linked opencv c++ binary is huge (From past experience doing something similar, it used to be 30Mb). But I don't think that this is your only binary manipulation problem, given the nature of your work. Maybe opencv would be handy to you. -
creativecreatorormaybenot over 4 yearsThat will not work because you cannot decode an image synchronously in Dart. That is what the issue I linked is about.
-
Nicolas Caous over 4 yearsDo you mind sharing what is the functionality that you want to do? Are you getting the camera feed and showing it on the screen with the desired transformation? Or is your stream of images coming from another source? Maybe you could avoid using ui.Image altogether.
-
Nicolas Caous over 4 yearsAlso, I want to apologize because, reading back our conversation, it seems that I'm not paying attention to what you are sending me. I have read all the links, it's just that english isn't my first language and sometimes communication gets a little fuzzy. I mean well :)
-
creativecreatorormaybenot over 4 yearsI am happy to share it with you: I have a list of pixels that can be edited in real-time (by the user dragging) and I need to manipulate the bytes, i.e. the pixels individually, so I cannot use lines. Consequently, I need to be able to draw this list of pixels as an image in real-time and the only reasonable fast way to do this that I know of is decoding to an
Image
an drawing that. I tried different approaches already (see here), but I could not find any more performant way. -
Nicolas Caous over 4 yearsThis list of pixels is a RGBA list encoded using a
Uint8List
or is it encoded in a file format? What i'm thinking is: since you have the raw pixels, you could write a personalized png encoder (using dart:ffi) that takes in the pixels in whatever format you have and spit out a buffer containing the png image that you can use inImage.memory(buffer)
. That way, this would all be sync (since ffi calls are sync) and you would only need asetState
to update the image that the user is editing. -
creativecreatorormaybenot over 4 yearsWowow, I actually did not know about
Image.memory
- this might be huge. I store it inUint8List
and I planned to manipulate it usingdart:ffi
anyway as explained previously. Before I discovereddecodeImageFromPixels
, I actually encoded my own BMP by hand (ouch :D) and then used theasync
way to generate anImage
from that (see an old question, the "Alternative" part). I will make sure to get back to you once I triedImage.memory
. I might also try to use my old BMP conversion (which is sync) to test a slower Dart version of it first. -
creativecreatorormaybenot over 4 yearsWell, I just realized that you are referring to the widget
Image
and itsmemory
constructor. I was quite sceptical about the idea that I had not been aware of a synchronous way to constructdart:ui
Image
s and I was correct becauseImage.memory
is just a wrapper for asynchronously converting to aui.Image
and displaying that. I need a synchronous way to draw pixel data to aCanvas
. -
Nicolas Caous over 4 yearsO shoot, I did not knew that Image.memory was async. Damm
-
Nicolas Caous over 4 yearsThe
Image.memory
boils down toString _instantiateImageCodec(Uint8List list, _Callback<Codec> callback, _ImageInfo imageInfo, int targetWidth, int targetHeight) native 'instantiateImageCodec';
which is async because it has a callback. :'(