Skip to content

Instantly share code, notes, and snippets.

@roipeker
Last active April 26, 2024 23:42
Show Gist options
  • Star 51 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save roipeker/9315aa25301f5c0362caaebd15876c2f to your computer and use it in GitHub Desktop.
Save roipeker/9315aa25301f5c0362caaebd15876c2f to your computer and use it in GitHub Desktop.
Basic image pixel color detection in Flutter (supports screenshots of the widget tree)
//////////////////////////////
//
// 2019, roipeker.com
// screencast - demo simple image:
// https://youtu.be/EJyRH4_pY8I
//
// screencast - demo snapshot:
// https://youtu.be/-LxPcL7T61E
//
//////////////////////////////
import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:image/image.dart' as img;
import 'package:flutter/services.dart' show rootBundle;
class ColorPickerWidget extends StatefulWidget {
@override
_ColorPickerWidgetState createState() => _ColorPickerWidgetState();
}
class _ColorPickerWidgetState extends State<ColorPickerWidget> {
String imagePath = 'assets/images/santorini.jpg';
GlobalKey imageKey = GlobalKey();
GlobalKey paintKey = GlobalKey();
// CHANGE THIS FLAG TO TEST BASIC IMAGE, AND SNAPSHOT.
bool useSnapshot = true;
// based on useSnapshot=true ? paintKey : imageKey ;
// this key is used in this example to keep the code shorter.
GlobalKey currentKey;
final StreamController<Color> _stateController = StreamController<Color>();
img.Image photo;
@override
void initState() {
currentKey = useSnapshot ? paintKey : imageKey;
super.initState();
}
@override
Widget build(BuildContext context) {
final String title = useSnapshot ? "snapshot" : "basic";
return Scaffold(
appBar: AppBar(title: Text("Color picker $title")),
body: StreamBuilder(
initialData: Colors.green[500],
stream: _stateController.stream,
builder: (buildContext, snapshot) {
Color selectedColor = snapshot.data ?? Colors.green;
return Stack(
children: <Widget>[
RepaintBoundary(
key: paintKey,
child: GestureDetector(
onPanDown: (details) {
searchPixel(details.globalPosition);
},
onPanUpdate: (details) {
searchPixel(details.globalPosition);
},
child: Center(
child: Image.asset(
imagePath,
key: imageKey,
//color: Colors.red,
//colorBlendMode: BlendMode.hue,
//alignment: Alignment.bottomRight,
fit: BoxFit.none,
//scale: .8,
),
),
),
),
Container(
margin: EdgeInsets.all(70),
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: selectedColor,
border: Border.all(width: 2.0, color: Colors.white),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 2))
]),
),
Positioned(
child: Text('${selectedColor}',
style: TextStyle(
color: Colors.white,
backgroundColor: Colors.black54)),
left: 114,
top: 95,
),
],
);
}),
);
}
void searchPixel(Offset globalPosition) async {
if (photo == null) {
await (useSnapshot ? loadSnapshotBytes() : loadImageBundleBytes());
}
_calculatePixel(globalPosition);
}
void _calculatePixel(Offset globalPosition) {
RenderBox box = currentKey.currentContext.findRenderObject();
Offset localPosition = box.globalToLocal(globalPosition);
double px = localPosition.dx;
double py = localPosition.dy;
if (!useSnapshot) {
double widgetScale = box.size.width / photo.width;
print(py);
px = (px / widgetScale);
py = (py / widgetScale);
}
int pixel32 = photo.getPixelSafe(px.toInt(), py.toInt());
int hex = abgrToArgb(pixel32);
_stateController.add(Color(hex));
}
Future<void> loadImageBundleBytes() async {
ByteData imageBytes = await rootBundle.load(imagePath);
setImageBytes(imageBytes);
}
Future<void> loadSnapshotBytes() async {
RenderRepaintBoundary boxPaint = paintKey.currentContext.findRenderObject();
ui.Image capture = await boxPaint.toImage();
ByteData imageBytes =
await capture.toByteData(format: ui.ImageByteFormat.png);
setImageBytes(imageBytes);
capture.dispose();
}
void setImageBytes(ByteData imageBytes) {
List<int> values = imageBytes.buffer.asUint8List();
photo = null;
photo = img.decodeImage(values);
}
}
// image lib uses uses KML color format, convert #AABBGGRR to regular #AARRGGBB
int abgrToArgb(int argbColor) {
int r = (argbColor >> 16) & 0xFF;
int b = argbColor & 0xFF;
return (argbColor & 0xFF00FF00) | (b << 16) | r;
}
@gputhige
Copy link

Thanks.
Will try and revert.

@ch-moez
Copy link

ch-moez commented Dec 3, 2022

`//////////////////////////////
//
// 2019, roipeker.com
// screencast - demo simple image:
// https://youtu.be/EJyRH4_pY8I
//
// screencast - demo snapshot:
// https://youtu.be/-LxPcL7T61E
//
//////////////////////////////

import 'dart:async';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:image/image.dart' as img;
import 'package:flutter/services.dart' show rootBundle;

void main() => runApp(const MaterialApp(home: MyApp()));

class MyApp extends StatefulWidget {
const MyApp({Key? key}) : super(key: key);

@OverRide
State createState() => _MyAppState();
}

class _MyAppState extends State {
String imagePath = 'assets/12.jpg';
GlobalKey imageKey = GlobalKey();
GlobalKey paintKey = GlobalKey();

// CHANGE THIS FLAG TO TEST BASIC IMAGE, AND SNAPSHOT.
bool useSnapshot = true;

// based on useSnapshot=true ? paintKey : imageKey ;
// this key is used in this example to keep the code shorter.
late GlobalKey currentKey;

final StreamController _stateController = StreamController();
//late img.Image photo ;
img.Image? photo;

@OverRide
void initState() {
currentKey = useSnapshot ? paintKey : imageKey;
super.initState();
}

@OverRide
Widget build(BuildContext context) {
final String title = useSnapshot ? "snapshot" : "basic";
return SafeArea(
child: Scaffold(
appBar: AppBar(title: Text("Color picker $title")),
body: StreamBuilder(
initialData: Colors.green[500],
stream: _stateController.stream,
builder: (buildContext, snapshot) {
Color selectedColor = snapshot.data as Color ?? Colors.green;
return Stack(
children: [
RepaintBoundary(
key: paintKey,
child: GestureDetector(
onPanDown: (details) {
searchPixel(details.globalPosition);
},
onPanUpdate: (details) {
searchPixel(details.globalPosition);
},
child: Center(
child: Image.asset(
imagePath,
key: imageKey,
//color: Colors.red,
//colorBlendMode: BlendMode.hue,
//alignment: Alignment.bottomRight,
fit: BoxFit.contain,
//scale: .8,
),
),
),
),
Container(
margin: const EdgeInsets.all(70),
width: 50,
height: 50,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: selectedColor!,
border: Border.all(width: 2.0, color: Colors.white),
boxShadow: [
const BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: Offset(0, 2))
]),
),
Positioned(
child: Text('${selectedColor}',
style: const TextStyle(
color: Colors.white,
backgroundColor: Colors.black54)),
left: 114,
top: 95,
),
],
);
}),
),
);
}

void searchPixel(Offset globalPosition) async {
if (photo == null) {
await (useSnapshot ? loadSnapshotBytes() : loadImageBundleBytes());
}
_calculatePixel(globalPosition);
}

void _calculatePixel(Offset globalPosition) {
RenderBox box = currentKey.currentContext!.findRenderObject() as RenderBox;
Offset localPosition = box.globalToLocal(globalPosition);

double px = localPosition.dx;
double py = localPosition.dy;

if (!useSnapshot) {
  double widgetScale = box.size.width / photo!.width;
  print(py);
  px = (px / widgetScale);
  py = (py / widgetScale);
}

int pixel32 = photo!.getPixelSafe(px.toInt(), py.toInt());
int hex = abgrToArgb(pixel32);

_stateController.add(Color(hex));

}

Future loadImageBundleBytes() async {
ByteData imageBytes = await rootBundle.load(imagePath);
setImageBytes(imageBytes);
}

Future loadSnapshotBytes() async {
RenderRepaintBoundary boxPaint =
paintKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
//RenderObject? boxPaint = paintKey.currentContext.findRenderObject();
ui.Image capture = await boxPaint.toImage();

ByteData? imageBytes =
    await capture.toByteData(format: ui.ImageByteFormat.png);
setImageBytes(imageBytes!);
capture.dispose();

}

void setImageBytes(ByteData imageBytes) {
List values = imageBytes.buffer.asUint8List();
photo;
photo = img.decodeImage(values)!;
}
}

// image lib uses uses KML color format, convert #AABBGGRR to regular #AARRGGBB
int abgrToArgb(int argbColor) {
int r = (argbColor >> 16) & 0xFF;
int b = argbColor & 0xFF;
return (argbColor & 0xFF00FF00) | (b << 16) | r;
}
`

@nmchien
Copy link

nmchien commented Dec 26, 2022

`///////////////////////////////////// 2019, roipeker.com // screencast - demo hình ảnh Đơn giản: // https://youtu.be/EJyRH4_pY8I // // ghi màn hình - ảnh chụp demo: // https://youtu.be/-LxPcL7T61E // ///////// / //////////////////

nhập 'phi tiêu: không đồng bộ'; nhập 'phi tiêu:typed_data'; nhập 'phi tiêu: ui' dưới dạng ui; nhập 'phi tiêu: toán học';

nhập 'gói: rung/liệu.dart'; nhập 'gói: rung/rendering.dart'; nhập 'gói: image/image.dart' dưới img; nhập 'gói: rung/services.dart' hiển thị rootBundle;

void main() => runApp(const MaterialApp(home: MyApp()));

Lớp MyApp mở rộng StatefulWidget { const MyApp({Key? key}): super(key: key);

@OverRide Trạng thái createState() => _MyAppState(); }

Lớp _MyAppState mở rộng Trạng thái { string imagePath = 'nội dung/12.jpg'; GlobalKey imageKey = GlobalKey(); GlobalKey paintKey = GlobalKey();

// THAY ĐỔI CỜ NÀY ĐỂ KIỂM TRA ẢNH CƠ BẢN VÀ ẢNH CHỤP. bool useSnapshot = true;

// dựa trên useSnapshot=true ? paintKey : imageKey ; // phím này đã được sử dụng trong ví dụ này để giữ mã ngắn hơn. GlobalKey currentKey lần lượt;

StreamController cuối cùng _stateController = StreamController(); //muộn img.Ảnh ; img.Hình ảnh? tấm hình;

@OverRide void initState() { currentKey = useSnapshot ? paintKey : imageKey; super.initState(); }

@OverRide Bản dựng tiện ích con (Bối cảnh BuildContext) { tiêu đề Chuỗi cuối cùng = useSnapshot? "ảnh chụp nhanh" : "cơ bản"; return SafeArea( con: Scaffold( appBar: AppBar(title: Text("Bộ chọn màu $title")), body: StreamBuilder( initData : Colors.green[500], stream: _stateController.stream, builder: (buildContext, snapshot ) { Color selectColor = snapshot.data as Color ?? Colors.green; return Stack( children: [ RepaintBoundary( key: paintKey, child: GestureDetector( onPanDown: (details) { searchPixel(details.globalPosition); }, onPanUpdate: ( chi tiết) ) { searchPixel(details.globalPosition); }, con: Center( con: Image.asset( imagePath, key: imageKey, //color: Colors.red, //colorBlendMode: BlendMode.hue, //alignment: Alignment.bottomRight, fit: BoxFit.contain, // scale: .8, ), ), ), ), Khu vực chứa( lề: const EdgeInsets.all(70), chiều rộng: 50, chiều cao: 50, trang trí: BoxDecoration( format: BoxShape.circle, color : đã chọnColor!, đường viền: Đường viền.tất cả(chiều rộng: 2.0, màu:Colors.white), boxShadow: [ const BoxShadow( color: Colors.black12, blurRadius: 4, offset: Offset(0, 2)) ]), ), Đã định vị( con: Văn bản('${selectedColor}', type: const TextStyle( color: Colors.white , backgroundColor: Colors.black54)), left: 114, top: 95, ), ], ); }), ), ); }

void searchPixel(Offset globalPosition) async { if (photo == null) { đang chờ (useSnapshot ? loadSnapshotBytes() : loadImageBundleBytes()); } _calculatePixel(globalPosition); }

void _calculatePixel(Offset globalPosition) { RenderBox box = currentKey.currentContext!.findRenderObject() làm RenderBox; Bù đắp localPosition = box.globalToLocal(globalPosition);

double px = localPosition.dx;
double py = localPosition.dy;

if (!useSnapshot) {
  double widgetScale = box.size.width / photo!.width;
  print(py);
  px = (px / widgetScale);
  py = (py / widgetScale);
}

int pixel32 = photo!.getPixelSafe(px.toInt(), py.toInt());
int hex = abgrToArgb(pixel32);

_stateController.add(Color(hex));

}

Tương lai loadImageBundleBytes() async { ByteData imageBytes = đang chờ rootBundle.load(imagePath); setImageBytes(imageBytes); }

Tương lai loadSnapshotBytes() async { RenderRepaintBoundary boxPaint = paintKey.currentContext!.findRenderObject() doing RenderRepaintBoundary; //RenderObject? boxPaint = paintKey.currentContext.findRenderObject(); ui.Chụp ảnh = đang chờ hộpPaint.toImage();

ByteData? imageBytes =
    await capture.toByteData(format: ui.ImageByteFormat.png);
setImageBytes(imageBytes!);
capture.dispose();

}

void setImageBytes(ByteData imageBytes) { Danh sách giá trị = imageBytes.buffer.asUint8List(); tấm hình; ảnh = img.decodeImage(giá trị)!; } }

// lib lib image sieu used used format KML color, convert #AABBGGRR to #AARRGGBB normal int abgrToArgb(int argbColor) { int r = (argbColor >> 16) & 0xFF; int b = argbColor & 0xFF; trả về (argbColor & 0xFF00FF00) | (b<<16) | r; } `

can you send me the image.dart file code? thanks

@ch-moez
Copy link

ch-moez commented Dec 26, 2022

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

@gyoussef55
Copy link

@roipeker I tried using the code and placed an image inside an InteractiveViewer widget. However, when I zoom in, the color I obtain by tapping on the image is not accurate.
What can I do ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment