detection.dart 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. import 'dart:math' show sqrt, pow;
  2. import "package:photos/face/model/dimension.dart";
  3. abstract class Detection {
  4. final double score;
  5. Detection({required this.score});
  6. const Detection.empty() : score = 0;
  7. get width;
  8. get height;
  9. @override
  10. String toString();
  11. }
  12. extension BBoxExtension on List<double> {
  13. void roundBoxToDouble() {
  14. final widthRounded = (this[2] - this[0]).roundToDouble();
  15. final heightRounded = (this[3] - this[1]).roundToDouble();
  16. this[0] = this[0].roundToDouble();
  17. this[1] = this[1].roundToDouble();
  18. this[2] = this[0] + widthRounded;
  19. this[3] = this[1] + heightRounded;
  20. }
  21. // double get xMinBox =>
  22. // isNotEmpty ? this[0] : throw IndexError.withLength(0, length);
  23. // double get yMinBox =>
  24. // length >= 2 ? this[1] : throw IndexError.withLength(1, length);
  25. // double get xMaxBox =>
  26. // length >= 3 ? this[2] : throw IndexError.withLength(2, length);
  27. // double get yMaxBox =>
  28. // length >= 4 ? this[3] : throw IndexError.withLength(3, length);
  29. }
  30. /// This class represents a face detection with relative coordinates in the range [0, 1].
  31. /// The coordinates are relative to the image size. The pattern for the coordinates is always [x, y], where x is the horizontal coordinate and y is the vertical coordinate.
  32. ///
  33. /// The [score] attribute is a double representing the confidence of the face detection.
  34. ///
  35. /// The [box] attribute is a list of 4 doubles, representing the coordinates of the bounding box of the face detection.
  36. /// The four values of the box in order are: [xMinBox, yMinBox, xMaxBox, yMaxBox].
  37. ///
  38. /// The [allKeypoints] attribute is a list of 6 lists of 2 doubles, representing the coordinates of the keypoints of the face detection.
  39. /// The six lists of two values in order are: [leftEye, rightEye, nose, mouth, leftEar, rightEar]. Again, all in [x, y] order.
  40. class FaceDetectionRelative extends Detection {
  41. final List<double> box;
  42. final List<List<double>> allKeypoints;
  43. double get xMinBox => box[0];
  44. double get yMinBox => box[1];
  45. double get xMaxBox => box[2];
  46. double get yMaxBox => box[3];
  47. List<double> get leftEye => allKeypoints[0];
  48. List<double> get rightEye => allKeypoints[1];
  49. List<double> get nose => allKeypoints[2];
  50. List<double> get leftMouth => allKeypoints[3];
  51. List<double> get rightMouth => allKeypoints[4];
  52. FaceDetectionRelative({
  53. required double score,
  54. required List<double> box,
  55. required List<List<double>> allKeypoints,
  56. }) : assert(
  57. box.every((e) => e >= -0.1 && e <= 1.1),
  58. "Bounding box values must be in the range [0, 1], with only a small margin of error allowed.",
  59. ),
  60. assert(
  61. allKeypoints
  62. .every((sublist) => sublist.every((e) => e >= -0.1 && e <= 1.1)),
  63. "All keypoints must be in the range [0, 1], with only a small margin of error allowed.",
  64. ),
  65. box = List<double>.from(box.map((e) => e.clamp(0.0, 1.0))),
  66. allKeypoints = allKeypoints
  67. .map(
  68. (sublist) =>
  69. List<double>.from(sublist.map((e) => e.clamp(0.0, 1.0))),
  70. )
  71. .toList(),
  72. super(score: score);
  73. factory FaceDetectionRelative.zero() {
  74. return FaceDetectionRelative(
  75. score: 0,
  76. box: <double>[0, 0, 0, 0],
  77. allKeypoints: <List<double>>[
  78. [0, 0],
  79. [0, 0],
  80. [0, 0],
  81. [0, 0],
  82. [0, 0],
  83. ],
  84. );
  85. }
  86. /// This is used to initialize the FaceDetectionRelative object with default values.
  87. /// This constructor is useful because it can be used to initialize a FaceDetectionRelative object as a constant.
  88. /// Contrary to the `FaceDetectionRelative.zero()` constructor, this one gives immutable attributes [box] and [allKeypoints].
  89. FaceDetectionRelative.defaultInitialization()
  90. : box = const <double>[0, 0, 0, 0],
  91. allKeypoints = const <List<double>>[
  92. [0, 0],
  93. [0, 0],
  94. [0, 0],
  95. [0, 0],
  96. [0, 0],
  97. ],
  98. super.empty();
  99. FaceDetectionRelative getNearestDetection(
  100. List<FaceDetectionRelative> detections,
  101. ) {
  102. if (detections.isEmpty) {
  103. throw ArgumentError("The detection list cannot be empty.");
  104. }
  105. var nearestDetection = detections[0];
  106. var minDistance = double.infinity;
  107. // Calculate the center of the current instance
  108. final centerX1 = (xMinBox + xMaxBox) / 2;
  109. final centerY1 = (yMinBox + yMaxBox) / 2;
  110. for (var detection in detections) {
  111. final centerX2 = (detection.xMinBox + detection.xMaxBox) / 2;
  112. final centerY2 = (detection.yMinBox + detection.yMaxBox) / 2;
  113. final distance =
  114. sqrt(pow(centerX2 - centerX1, 2) + pow(centerY2 - centerY1, 2));
  115. if (distance < minDistance) {
  116. minDistance = distance;
  117. nearestDetection = detection;
  118. }
  119. }
  120. return nearestDetection;
  121. }
  122. void transformRelativeToOriginalImage(
  123. List<double> fromBox, // [xMin, yMin, xMax, yMax]
  124. List<double> toBox, // [xMin, yMin, xMax, yMax]
  125. ) {
  126. // Return if all elements of fromBox and toBox are equal
  127. for (int i = 0; i < fromBox.length; i++) {
  128. if (fromBox[i] != toBox[i]) {
  129. break;
  130. }
  131. if (i == fromBox.length - 1) {
  132. return;
  133. }
  134. }
  135. // Account for padding
  136. final double paddingXRatio =
  137. (fromBox[0] - toBox[0]) / (toBox[2] - toBox[0]);
  138. final double paddingYRatio =
  139. (fromBox[1] - toBox[1]) / (toBox[3] - toBox[1]);
  140. // Calculate the scaling and translation
  141. final double scaleX = (fromBox[2] - fromBox[0]) / (1 - 2 * paddingXRatio);
  142. final double scaleY = (fromBox[3] - fromBox[1]) / (1 - 2 * paddingYRatio);
  143. final double translateX = fromBox[0] - paddingXRatio * scaleX;
  144. final double translateY = fromBox[1] - paddingYRatio * scaleY;
  145. // Transform Box
  146. _transformBox(box, scaleX, scaleY, translateX, translateY);
  147. // Transform All Keypoints
  148. for (int i = 0; i < allKeypoints.length; i++) {
  149. allKeypoints[i] = _transformPoint(
  150. allKeypoints[i],
  151. scaleX,
  152. scaleY,
  153. translateX,
  154. translateY,
  155. );
  156. }
  157. }
  158. void correctForMaintainedAspectRatio(
  159. Dimensions originalSize,
  160. Dimensions newSize,
  161. ) {
  162. // Return if both are the same size, meaning no scaling was done on both width and height
  163. if (originalSize == newSize) {
  164. return;
  165. }
  166. // Calculate the scaling
  167. final double scaleX = originalSize.width / newSize.width;
  168. final double scaleY = originalSize.height / newSize.height;
  169. const double translateX = 0;
  170. const double translateY = 0;
  171. // Transform Box
  172. _transformBox(box, scaleX, scaleY, translateX, translateY);
  173. // Transform All Keypoints
  174. for (int i = 0; i < allKeypoints.length; i++) {
  175. allKeypoints[i] = _transformPoint(
  176. allKeypoints[i],
  177. scaleX,
  178. scaleY,
  179. translateX,
  180. translateY,
  181. );
  182. }
  183. }
  184. void _transformBox(
  185. List<double> box,
  186. double scaleX,
  187. double scaleY,
  188. double translateX,
  189. double translateY,
  190. ) {
  191. box[0] = (box[0] * scaleX + translateX).clamp(0.0, 1.0);
  192. box[1] = (box[1] * scaleY + translateY).clamp(0.0, 1.0);
  193. box[2] = (box[2] * scaleX + translateX).clamp(0.0, 1.0);
  194. box[3] = (box[3] * scaleY + translateY).clamp(0.0, 1.0);
  195. }
  196. List<double> _transformPoint(
  197. List<double> point,
  198. double scaleX,
  199. double scaleY,
  200. double translateX,
  201. double translateY,
  202. ) {
  203. return [
  204. (point[0] * scaleX + translateX).clamp(0.0, 1.0),
  205. (point[1] * scaleY + translateY).clamp(0.0, 1.0),
  206. ];
  207. }
  208. FaceDetectionAbsolute toAbsolute({
  209. required int imageWidth,
  210. required int imageHeight,
  211. }) {
  212. final scoreCopy = score;
  213. final boxCopy = List<double>.from(box, growable: false);
  214. final allKeypointsCopy = allKeypoints
  215. .map((sublist) => List<double>.from(sublist, growable: false))
  216. .toList();
  217. boxCopy[0] *= imageWidth;
  218. boxCopy[1] *= imageHeight;
  219. boxCopy[2] *= imageWidth;
  220. boxCopy[3] *= imageHeight;
  221. // final intbox = boxCopy.map((e) => e.toInt()).toList();
  222. for (List<double> keypoint in allKeypointsCopy) {
  223. keypoint[0] *= imageWidth;
  224. keypoint[1] *= imageHeight;
  225. }
  226. // final intKeypoints =
  227. // allKeypointsCopy.map((e) => e.map((e) => e.toInt()).toList()).toList();
  228. return FaceDetectionAbsolute(
  229. score: scoreCopy,
  230. box: boxCopy,
  231. allKeypoints: allKeypointsCopy,
  232. );
  233. }
  234. String toFaceID({required int fileID}) {
  235. // Assert that the values are within the expected range
  236. assert(
  237. (xMinBox >= 0 && xMinBox <= 1) &&
  238. (yMinBox >= 0 && yMinBox <= 1) &&
  239. (xMaxBox >= 0 && xMaxBox <= 1) &&
  240. (yMaxBox >= 0 && yMaxBox <= 1),
  241. "Bounding box values must be in the range [0, 1]",
  242. );
  243. // Extract bounding box values
  244. final String xMin =
  245. xMinBox.clamp(0.0, 0.999999).toStringAsFixed(5).substring(2);
  246. final String yMin =
  247. yMinBox.clamp(0.0, 0.999999).toStringAsFixed(5).substring(2);
  248. final String xMax =
  249. xMaxBox.clamp(0.0, 0.999999).toStringAsFixed(5).substring(2);
  250. final String yMax =
  251. yMaxBox.clamp(0.0, 0.999999).toStringAsFixed(5).substring(2);
  252. // Convert the bounding box values to string and concatenate
  253. final String rawID = "${xMin}_${yMin}_${xMax}_$yMax";
  254. final faceID = fileID.toString() + '_' + rawID.toString();
  255. // Return the hexadecimal representation of the hash
  256. return faceID;
  257. }
  258. /// This method is used to generate a faceID for a face detection that was manually added by the user.
  259. static String toFaceIDEmpty({required int fileID}) {
  260. return fileID.toString() + '_0';
  261. }
  262. /// This method is used to check if a faceID corresponds to a manually added face detection and not an actual face detection.
  263. static bool isFaceIDEmpty(String faceID) {
  264. return faceID.split('_')[1] == '0';
  265. }
  266. @override
  267. String toString() {
  268. return 'FaceDetectionRelative( with relative coordinates: \n score: $score \n Box: xMinBox: $xMinBox, yMinBox: $yMinBox, xMaxBox: $xMaxBox, yMaxBox: $yMaxBox, \n Keypoints: leftEye: $leftEye, rightEye: $rightEye, nose: $nose, leftMouth: $leftMouth, rightMouth: $rightMouth \n )';
  269. }
  270. Map<String, dynamic> toJson() {
  271. return {
  272. 'score': score,
  273. 'box': box,
  274. 'allKeypoints': allKeypoints,
  275. };
  276. }
  277. factory FaceDetectionRelative.fromJson(Map<String, dynamic> json) {
  278. return FaceDetectionRelative(
  279. score: (json['score'] as num).toDouble(),
  280. box: List<double>.from(json['box']),
  281. allKeypoints: (json['allKeypoints'] as List)
  282. .map((item) => List<double>.from(item))
  283. .toList(),
  284. );
  285. }
  286. @override
  287. /// The width of the bounding box of the face detection, in relative range [0, 1].
  288. double get width => xMaxBox - xMinBox;
  289. @override
  290. /// The height of the bounding box of the face detection, in relative range [0, 1].
  291. double get height => yMaxBox - yMinBox;
  292. }
  293. /// This class represents a face detection with absolute coordinates in pixels, in the range [0, imageWidth] for the horizontal coordinates and [0, imageHeight] for the vertical coordinates.
  294. /// The pattern for the coordinates is always [x, y], where x is the horizontal coordinate and y is the vertical coordinate.
  295. ///
  296. /// The [score] attribute is a double representing the confidence of the face detection.
  297. ///
  298. /// The [box] attribute is a list of 4 integers, representing the coordinates of the bounding box of the face detection.
  299. /// The four values of the box in order are: [xMinBox, yMinBox, xMaxBox, yMaxBox].
  300. ///
  301. /// The [allKeypoints] attribute is a list of 6 lists of 2 integers, representing the coordinates of the keypoints of the face detection.
  302. /// The six lists of two values in order are: [leftEye, rightEye, nose, mouth, leftEar, rightEar]. Again, all in [x, y] order.
  303. class FaceDetectionAbsolute extends Detection {
  304. final List<double> box;
  305. final List<List<double>> allKeypoints;
  306. double get xMinBox => box[0];
  307. double get yMinBox => box[1];
  308. double get xMaxBox => box[2];
  309. double get yMaxBox => box[3];
  310. List<double> get leftEye => allKeypoints[0];
  311. List<double> get rightEye => allKeypoints[1];
  312. List<double> get nose => allKeypoints[2];
  313. List<double> get leftMouth => allKeypoints[3];
  314. List<double> get rightMouth => allKeypoints[4];
  315. FaceDetectionAbsolute({
  316. required double score,
  317. required this.box,
  318. required this.allKeypoints,
  319. }) : super(score: score);
  320. factory FaceDetectionAbsolute._zero() {
  321. return FaceDetectionAbsolute(
  322. score: 0,
  323. box: <double>[0, 0, 0, 0],
  324. allKeypoints: <List<double>>[
  325. [0, 0],
  326. [0, 0],
  327. [0, 0],
  328. [0, 0],
  329. [0, 0],
  330. ],
  331. );
  332. }
  333. FaceDetectionAbsolute.defaultInitialization()
  334. : box = const <double>[0, 0, 0, 0],
  335. allKeypoints = const <List<double>>[
  336. [0, 0],
  337. [0, 0],
  338. [0, 0],
  339. [0, 0],
  340. [0, 0],
  341. ],
  342. super.empty();
  343. @override
  344. String toString() {
  345. return 'FaceDetectionAbsolute( with absolute coordinates: \n score: $score \n Box: xMinBox: $xMinBox, yMinBox: $yMinBox, xMaxBox: $xMaxBox, yMaxBox: $yMaxBox, \n Keypoints: leftEye: $leftEye, rightEye: $rightEye, nose: $nose, leftMouth: $leftMouth, rightMouth: $rightMouth \n )';
  346. }
  347. Map<String, dynamic> toJson() {
  348. return {
  349. 'score': score,
  350. 'box': box,
  351. 'allKeypoints': allKeypoints,
  352. };
  353. }
  354. factory FaceDetectionAbsolute.fromJson(Map<String, dynamic> json) {
  355. return FaceDetectionAbsolute(
  356. score: (json['score'] as num).toDouble(),
  357. box: List<double>.from(json['box']),
  358. allKeypoints: (json['allKeypoints'] as List)
  359. .map((item) => List<double>.from(item))
  360. .toList(),
  361. );
  362. }
  363. static FaceDetectionAbsolute empty = FaceDetectionAbsolute._zero();
  364. @override
  365. /// The width of the bounding box of the face detection, in number of pixels, range [0, imageWidth].
  366. double get width => xMaxBox - xMinBox;
  367. @override
  368. /// The height of the bounding box of the face detection, in number of pixels, range [0, imageHeight].
  369. double get height => yMaxBox - yMinBox;
  370. }
  371. List<FaceDetectionAbsolute> relativeToAbsoluteDetections({
  372. required List<FaceDetectionRelative> relativeDetections,
  373. required int imageWidth,
  374. required int imageHeight,
  375. }) {
  376. final numberOfDetections = relativeDetections.length;
  377. final absoluteDetections = List<FaceDetectionAbsolute>.filled(
  378. numberOfDetections,
  379. FaceDetectionAbsolute._zero(),
  380. );
  381. for (var i = 0; i < relativeDetections.length; i++) {
  382. final relativeDetection = relativeDetections[i];
  383. final absoluteDetection = relativeDetection.toAbsolute(
  384. imageWidth: imageWidth,
  385. imageHeight: imageHeight,
  386. );
  387. absoluteDetections[i] = absoluteDetection;
  388. }
  389. return absoluteDetections;
  390. }
  391. /// Returns an enlarged version of the [box] by a factor of [factor].
  392. List<double> getEnlargedRelativeBox(List<double> box, [double factor = 2]) {
  393. final boxCopy = List<double>.from(box, growable: false);
  394. // The four values of the box in order are: [xMinBox, yMinBox, xMaxBox, yMaxBox].
  395. final width = boxCopy[2] - boxCopy[0];
  396. final height = boxCopy[3] - boxCopy[1];
  397. boxCopy[0] -= width * (factor - 1) / 2;
  398. boxCopy[1] -= height * (factor - 1) / 2;
  399. boxCopy[2] += width * (factor - 1) / 2;
  400. boxCopy[3] += height * (factor - 1) / 2;
  401. return boxCopy;
  402. }