main.dart 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:photo_classifier/photo_classifier.dart';
  4. import 'package:permission_handler/permission_handler.dart';
  5. import 'package:photo_classifier/models.dart';
  6. void main() {
  7. runApp(const MyApp());
  8. }
  9. class MyApp extends StatefulWidget {
  10. const MyApp({super.key});
  11. @override
  12. State<MyApp> createState() => _MyAppState();
  13. }
  14. class _MyAppState extends State<MyApp> {
  15. final _photoclassifier = PhotoClassifier();
  16. bool _hasPermission = false;
  17. StreamSubscription<ClassificationEvent?>? _subscription;
  18. ClassificationProgress? _progress;
  19. List<ClassifiedImageGroup> _similarResult = [];
  20. List<ClassifiedImage> _peopleResult = [];
  21. List<ClassifiedImage> _screenshotResult = [];
  22. List<ClassifiedImage> _blurryResult = [];
  23. bool _isClassifying = false;
  24. String _errorMessage = '';
  25. @override
  26. void initState() {
  27. super.initState();
  28. _checkPermissions();
  29. }
  30. Future<void> _checkPermissions() async {
  31. final status = await Permission.photos.status;
  32. setState(() {
  33. _hasPermission = status.isGranted;
  34. if (!status.isGranted) {
  35. _errorMessage = '需要相册权限才能进行照片分类,请点击上方按钮申请权限';
  36. }
  37. });
  38. }
  39. @override
  40. Widget build(BuildContext context) {
  41. return MaterialApp(
  42. home: Scaffold(
  43. appBar: AppBar(
  44. title: const Text('照片分类插件示例'),
  45. ),
  46. body: Center(
  47. child: Column(
  48. mainAxisAlignment: MainAxisAlignment.center,
  49. children: [
  50. if (!_hasPermission)
  51. ElevatedButton(
  52. onPressed: _requestPermission,
  53. child: const Text('请求照片库权限'),
  54. ),
  55. if (!_hasPermission)
  56. Padding(
  57. padding: const EdgeInsets.only(top: 16.0),
  58. child: ElevatedButton(
  59. onPressed: () => openAppSettings(),
  60. style: ElevatedButton.styleFrom(
  61. backgroundColor: Colors.orange,
  62. ),
  63. child: const Text('打开系统设置'),
  64. ),
  65. ),
  66. if (_hasPermission && !_isClassifying)
  67. ElevatedButton(
  68. onPressed: _startClassificationWithStream,
  69. child: const Text('开始分类'),
  70. ),
  71. if (_isClassifying)
  72. ElevatedButton(
  73. onPressed: _cancelClassification,
  74. child: const Text('取消分类'),
  75. ),
  76. if (_progress != null) ...[
  77. const SizedBox(height: 20),
  78. Text('进度: ${(_progress!.rate * 100).toStringAsFixed(1)}%',
  79. style: const TextStyle(fontSize: 18)),
  80. const SizedBox(height: 10),
  81. LinearProgressIndicator(value: _progress!.rate),
  82. Text('已处理: ${_progress!.currentBatch}/${_progress!.totalBatches}'),
  83. ],
  84. if (_errorMessage.isNotEmpty) ...[
  85. const SizedBox(height: 20),
  86. Container(
  87. padding: const EdgeInsets.all(12),
  88. decoration: BoxDecoration(
  89. color: Colors.red.withOpacity(0.1),
  90. borderRadius: BorderRadius.circular(8),
  91. border: Border.all(color: Colors.red.withOpacity(0.5)),
  92. ),
  93. child: Column(
  94. children: [
  95. const Icon(Icons.error_outline, color: Colors.red, size: 28),
  96. const SizedBox(height: 8),
  97. Text(
  98. _errorMessage,
  99. style: const TextStyle(color: Colors.red),
  100. textAlign: TextAlign.center,
  101. ),
  102. ],
  103. ),
  104. ),
  105. ],
  106. if (_progress != null) ...[
  107. const SizedBox(height: 20),
  108. const Text('分类结果:', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
  109. _buildResultSummary(),
  110. ],
  111. if (_progress?.isCompleted == true)
  112. ElevatedButton(
  113. onPressed: _resetAll,
  114. style: ElevatedButton.styleFrom(
  115. backgroundColor: Colors.blueGrey,
  116. ),
  117. child: const Text('重置'),
  118. ),
  119. ],
  120. ),
  121. ),
  122. ),
  123. );
  124. }
  125. Widget _buildResultSummary() {
  126. if (_similarResult.isEmpty && _peopleResult.isEmpty && _screenshotResult.isEmpty && _blurryResult.isEmpty) return const Text('无结果');
  127. return Expanded(
  128. child: ListView(
  129. children: [
  130. _buildResultItem('相似图片组', _similarResult.length),
  131. _buildResultItem('人物照片', _peopleResult.length),
  132. _buildResultItem('屏幕截图', _screenshotResult.length),
  133. _buildResultItem('模糊图片', _blurryResult.length),
  134. ],
  135. ),
  136. );
  137. }
  138. Widget _buildResultItem(String title, int itemsCount) {
  139. if (itemsCount == 0) return const SizedBox.shrink();
  140. return Padding(
  141. padding: const EdgeInsets.symmetric(vertical: 8.0),
  142. child: Column(
  143. crossAxisAlignment: CrossAxisAlignment.start,
  144. children: [
  145. Text('$title: $itemsCount',
  146. style: const TextStyle(fontWeight: FontWeight.bold)),
  147. // 这里可以添加图片预览
  148. ],
  149. ),
  150. );
  151. }
  152. Future<void> _requestPermission() async {
  153. try {
  154. // 使用permission_handler请求权限
  155. final status = await Permission.photos.request();
  156. final granted = status.isGranted;
  157. setState(() {
  158. _hasPermission = granted;
  159. if (!granted) {
  160. _errorMessage = '需要相册权限才能进行照片分类';
  161. } else {
  162. _errorMessage = '';
  163. }
  164. });
  165. // 如果被拒绝且不能再次请求,提示用户前往设置
  166. if (status.isPermanentlyDenied || status.isDenied) {
  167. setState(() {
  168. _errorMessage = '权限被拒绝,请点击下方按钮前往设置手动开启相册权限';
  169. });
  170. }
  171. } catch (e) {
  172. setState(() {
  173. _errorMessage = '请求权限失败: $e';
  174. });
  175. }
  176. }
  177. Future<void> _startClassificationWithStream() async {
  178. setState(() {
  179. _isClassifying = true;
  180. _errorMessage = '';
  181. _progress = null;
  182. _similarResult = [];
  183. _peopleResult = [];
  184. _screenshotResult = [];
  185. _blurryResult = [];
  186. });
  187. try {
  188. // 先配置分类器
  189. await _photoclassifier.configureClassifier(
  190. batchSize: 200,
  191. maxConcurrentProcessing: 4,
  192. similarityThreshold: 0.75,
  193. );
  194. // 设置流监听
  195. _subscription = _photoclassifier
  196. .startClassificationStream()
  197. .listen(
  198. (event) {
  199. if (event == null) return;
  200. setState(() {
  201. _progress = event.progress;
  202. print("批次: ${event.progress?.currentBatch}/${event.progress?.totalBatches}, 用时: ${event.progress?.batchDuration}");
  203. print("${event.result?.screenshotImages?.length}");
  204. var result = event.result;
  205. if (result != null) {
  206. _similarResult.addAll(result.similarGroups?.map((group) => group).toList() ?? []);
  207. _peopleResult.addAll(result.peopleImages?.map((image) => image).toList() ?? []);
  208. _screenshotResult.addAll(result.screenshotImages?.map((image) => image).toList() ?? []);
  209. _blurryResult.addAll(result.blurryImages?.map((image) => image).toList() ?? []);
  210. }
  211. if (event.progress?.isCompleted == true) {
  212. print("分类完成, 总耗时: ${event.progress?.totalDuration}");
  213. _isClassifying = false;
  214. _subscription?.cancel();
  215. // 取消订阅
  216. _subscription = null;
  217. }
  218. });
  219. },
  220. onError: (error) {
  221. setState(() {
  222. _errorMessage = '分类过程中出错: $error';
  223. _isClassifying = false;
  224. });
  225. },
  226. onDone: () {
  227. if (_progress?.isCompleted != true) {
  228. setState(() {
  229. _errorMessage = '分类过程意外结束';
  230. _isClassifying = false;
  231. });
  232. }
  233. },
  234. );
  235. } catch (e) {
  236. setState(() {
  237. _errorMessage = '启动分类失败: $e';
  238. _isClassifying = false;
  239. });
  240. }
  241. }
  242. void _cancelClassification() {
  243. _subscription?.cancel();
  244. _subscription = null;
  245. setState(() {
  246. _isClassifying = false;
  247. });
  248. _photoclassifier.resetClassifier();
  249. }
  250. void _resetAll() {
  251. setState(() {
  252. _progress = null;
  253. _similarResult = [];
  254. _peopleResult = [];
  255. _screenshotResult = [];
  256. _blurryResult = [];
  257. _errorMessage = '';
  258. _isClassifying = false;
  259. });
  260. // _photoclassifier.resetClassifier();
  261. }
  262. @override
  263. void dispose() {
  264. _subscription?.cancel();
  265. super.dispose();
  266. }
  267. }