-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
e7501d7
commit 27eb1b1
Showing
5 changed files
with
310 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import 'dart:async'; | ||
import 'dart:math'; | ||
import 'dart:ui' as ui; | ||
|
||
import 'package:flutter/gestures.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:inference/annotation.dart'; | ||
import 'package:inference/widgets/canvas/canvas_painter.dart'; | ||
import 'package:vector_math/vector_math_64.dart' show Vector3; | ||
import 'package:inference/project.dart' as project; | ||
|
||
class Canvas extends StatefulWidget { | ||
|
||
final ui.Image image; | ||
final List<Annotation>? annotations; | ||
final List<project.Label> labelDefinitions; | ||
|
||
const Canvas({required this.image, this.annotations, required this.labelDefinitions, super.key}); | ||
|
||
@override | ||
State<Canvas> createState() => _CanvasState(); | ||
} | ||
|
||
class _CanvasState extends State<Canvas> { | ||
|
||
double prevScale = 1; | ||
Matrix4 matrix = Matrix4.identity() | ||
..scale(0.9); | ||
Matrix4 inverse = Matrix4.identity(); | ||
bool done = false; | ||
|
||
@override | ||
void initState() { | ||
super.initState(); | ||
|
||
Future.delayed(Duration.zero).then((_) { | ||
setState(() { | ||
matrix = setTransformToFit(widget.image); | ||
}); | ||
}); | ||
} | ||
|
||
Matrix4 setTransformToFit(ui.Image image) { | ||
if (context.size == null) { | ||
return Matrix4.identity(); | ||
} | ||
final imageSize = Size(image.width.toDouble(), image.height.toDouble()); | ||
final canvasSize = context.size!; | ||
|
||
final ratio = Size(imageSize.width / canvasSize.width, imageSize.height / canvasSize.height); | ||
|
||
final scale = 1 / max(ratio.width, ratio.height) * 0.9; | ||
final offset = (canvasSize - imageSize * scale as Offset) / 2; | ||
|
||
return matrix = Matrix4.identity() | ||
..translate(offset.dx, offset.dy, 0.0) | ||
..scale(scale); | ||
} | ||
|
||
void scaleCanvas(Vector3 localPosition, double scale) { | ||
inverse.copyInverse(matrix); | ||
final position = inverse * localPosition; | ||
final mScale = 1 - scale; | ||
setState(() { | ||
matrix *= Matrix4( // row major or column major | ||
scale, 0, 0, 0, | ||
0, scale, 0, 0, | ||
0, 0, scale, 0, | ||
mScale * position.x, mScale * position.y, 0, 1); | ||
}); | ||
} | ||
|
||
@override | ||
Widget build(BuildContext context) { | ||
return NotificationListener<SizeChangedLayoutNotification>( | ||
onNotification: (f) { | ||
WidgetsBinding.instance.addPostFrameCallback((_) { | ||
setState(() { | ||
matrix = setTransformToFit(widget.image); | ||
}); | ||
}); | ||
return false; | ||
}, | ||
child: SizeChangedLayoutNotifier( | ||
child: SizedBox.expand( | ||
child: Container( | ||
clipBehavior: Clip.hardEdge, | ||
decoration: const BoxDecoration(shape: BoxShape.rectangle), | ||
child: GestureDetector( | ||
behavior: HitTestBehavior.translucent, | ||
onScaleStart: (_) { | ||
prevScale = 1; | ||
}, | ||
onDoubleTap: () { | ||
setState(() { | ||
matrix = setTransformToFit(widget.image); | ||
}); | ||
}, | ||
onScaleUpdate: (ScaleUpdateDetails d) { | ||
final scale = 1 - (prevScale - d.scale); | ||
prevScale = d.scale; | ||
final zoom = matrix.getMaxScaleOnAxis(); | ||
scaleCanvas(Vector3(d.localFocalPoint.dx, d.localFocalPoint.dy, 0), scale); | ||
setState(() { | ||
matrix.translate(d.focalPointDelta.dx / zoom, d.focalPointDelta.dy / zoom, 0.0); | ||
}); | ||
}, | ||
child: Listener( | ||
behavior: HitTestBehavior.translucent, | ||
onPointerSignal: (p) { | ||
if (p is PointerScrollEvent) { | ||
final scale = p.scrollDelta.dy > 0 ? 0.95 : 1.05; // lazy solution, perhaps an animation depending on the scrollDelta? | ||
scaleCanvas(Vector3(p.localPosition.dx, p.localPosition.dy, 0.0), scale); | ||
} | ||
}, | ||
child: Transform( | ||
transform: matrix, | ||
alignment: FractionalOffset.topLeft, | ||
child: Builder( | ||
builder: (context) { | ||
return CustomPaint( | ||
painter: CanvasPainter(widget.image, widget.annotations, widget.labelDefinitions, matrix.getMaxScaleOnAxis()), | ||
child: Container(), | ||
); | ||
} | ||
) | ||
), | ||
), | ||
), | ||
), | ||
), | ||
), | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import 'dart:math'; | ||
import 'dart:ui' as ui; | ||
|
||
import 'package:collection/collection.dart'; | ||
import 'package:flutter/material.dart'; | ||
import 'package:inference/annotation.dart'; | ||
import 'package:inference/color.dart'; | ||
import 'package:inference/project.dart' as project; | ||
import 'package:vector_math/vector_math_64.dart' show Vector3; | ||
|
||
Color getColorByLabelID(String labelId, List<project.Label> labelDefinitions) { | ||
final label = labelDefinitions.firstWhereOrNull((project.Label b) => b.id == labelId); | ||
if (label == null) { | ||
throw "Label not found"; | ||
} | ||
return HexColor.fromHex(label.color.substring(0, 7)); | ||
} | ||
|
||
class CanvasPainter extends CustomPainter { | ||
final ui.Image image; | ||
final List<Annotation>? annotations; | ||
final List<project.Label> labelDefinitions; | ||
final double scale; | ||
|
||
CanvasPainter(this.image, this.annotations, this.labelDefinitions, this.scale); | ||
|
||
@override | ||
void paint(Canvas canvas, Size size) { | ||
paintImage( | ||
alignment: Alignment.topLeft, | ||
canvas: canvas, | ||
rect: Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()), | ||
fit: BoxFit.scaleDown, | ||
image: image, | ||
); | ||
for (final annotation in annotations ?? []) { | ||
final firstLabelColor = getColorByLabelID(annotation.labels[0].id, labelDefinitions); | ||
Paint paint = Paint() | ||
..color = firstLabelColor | ||
..strokeWidth = 2 | ||
..style = PaintingStyle.stroke; | ||
|
||
Paint transparent = Paint() | ||
..color = Color.fromARGB(102, firstLabelColor.red, firstLabelColor.green, firstLabelColor.blue); | ||
|
||
if (annotation.shape is Rectangle) { | ||
drawRectangle(canvas, size, paint, transparent, annotation); | ||
} | ||
if (annotation.shape is Polygon) { | ||
drawPolygon(canvas, size, paint, transparent, annotation); | ||
} | ||
if (annotation.shape is RotatedRectangle) { | ||
drawRotatedRectangle(canvas, size, paint, transparent, annotation); | ||
} | ||
} | ||
} | ||
|
||
void drawRectangle(Canvas canvas, Size size, Paint paint, Paint transparent, Annotation annotation){ | ||
final imageSize = Size(image.width.toDouble(), image.height.toDouble()); | ||
final rect = (annotation.shape as Rectangle).toRect(); | ||
canvas.drawRect(rect, paint); | ||
if (rect.size != imageSize) { | ||
canvas.drawRect(rect, transparent); | ||
} | ||
var position = rect.topLeft; | ||
for (final label in annotation.labels) { | ||
final labelSize = drawLabel(canvas, size, label, position); | ||
position += Offset(labelSize.width, 0); | ||
} | ||
} | ||
|
||
|
||
void drawPolygon(Canvas canvas, Size size, Paint paint, Paint transparent, Annotation annotation) { | ||
final path = ui.Path(); | ||
final shape = (annotation.shape as Polygon); | ||
path.addPolygon(shape.points, true); | ||
|
||
canvas.drawPath(path, paint); | ||
canvas.drawPath(path, transparent); | ||
final rect = shape.rectangle.toRect(); | ||
final topCenter = rect.topCenter - const Offset(0, 30.0); | ||
canvas.drawLine(rect.center, topCenter, paint); | ||
|
||
var position = topCenter; | ||
for (final label in annotation.labels) { | ||
final labelSize = drawLabel(canvas, size, label, position); | ||
position += Offset(labelSize.width, 0); | ||
} | ||
} | ||
|
||
void drawRotatedRectangle(Canvas canvas, Size size, Paint paint, Paint transparent, Annotation annotation) { | ||
final shape = (annotation.shape as RotatedRectangle); | ||
final path = ui.Path(); | ||
final rect = ui.Rect.fromCenter(center: ui.Offset.zero, width: shape.width, height: shape.height); | ||
path.addRect(rect); | ||
final matrix = Matrix4.identity() | ||
..rotateZ(shape.angleInRadians) | ||
..setTranslationRaw(shape.centerX, shape.centerY, 0.0); | ||
|
||
final corners = [rect.topLeft, rect.topRight, rect.bottomRight, rect. bottomLeft]; | ||
|
||
|
||
|
||
final rotatedPath = path.transform(matrix.storage); | ||
canvas.drawPath(rotatedPath, paint); | ||
canvas.drawPath(rotatedPath, transparent); | ||
|
||
double labelPosition = double.infinity; | ||
for (final corner in corners) { | ||
final transformedCorner = (matrix * Vector3(corner.dx, corner.dy, 0)) as Vector3; | ||
labelPosition = min(transformedCorner.y, labelPosition); | ||
} | ||
|
||
var position = Offset(shape.centerX, labelPosition - 30); | ||
canvas.drawLine(Offset(shape.centerX, shape.centerY), position, paint); | ||
for (final label in annotation.labels) { | ||
final labelSize = drawLabel(canvas, size, label, position); | ||
position += Offset(labelSize.width, 0); | ||
} | ||
} | ||
|
||
Size drawLabel(Canvas canvas, Size size, Label label, Offset position) { | ||
final color = getColorByLabelID(label.id, labelDefinitions); | ||
Paint paint = Paint() | ||
..color = color; | ||
final textStyle = TextStyle( | ||
color: foregroundColorByLuminance(color), | ||
fontFamily: 'IntelOne', | ||
fontSize: 14 / scale, | ||
); | ||
final textSpan = TextSpan( | ||
text: "${label.name} ${(label.probability * 100).toStringAsFixed(1)}%", | ||
style: textStyle, | ||
); | ||
final textPainter = TextPainter( | ||
text: textSpan, | ||
textDirection: TextDirection.ltr, | ||
); | ||
textPainter.layout( | ||
minWidth: 0, | ||
maxWidth: size.width, | ||
); | ||
canvas.drawRect(ui.Rect.fromLTWH(position.dx - 1, position.dy - textPainter.height - 1, textPainter.width + 2, textPainter.height), paint); | ||
textPainter.paint(canvas, position - Offset(0, textPainter.height)); | ||
return textPainter.size + const Offset(3, 0); | ||
} | ||
|
||
@override | ||
bool shouldRepaint(CanvasPainter oldDelegate) { | ||
return false; | ||
} | ||
@override | ||
bool shouldRebuildSemantics(CanvasPainter oldDelegate) => false; | ||
} |