detection.dart 17 KB

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