main.dart 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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, isGroup: true),
  131. _buildResultItem('人物照片', _peopleResult.length),
  132. _buildResultItem('屏幕截图', _screenshotResult.length),
  133. _buildResultItem('模糊图片', _blurryResult.length),
  134. ],
  135. ),
  136. );
  137. }
  138. Widget _buildResultItem(String title, dynamic items, {bool isGroup = false}) {
  139. if (items == null) return const SizedBox.shrink();
  140. final count = isGroup ? (items as List).length : (items as List).length;
  141. final itemsText = isGroup ? '$count 组' : '$count 张';
  142. return Padding(
  143. padding: const EdgeInsets.symmetric(vertical: 8.0),
  144. child: Column(
  145. crossAxisAlignment: CrossAxisAlignment.start,
  146. children: [
  147. Text('$title: $itemsText',
  148. style: const TextStyle(fontWeight: FontWeight.bold)),
  149. // 这里可以添加图片预览
  150. ],
  151. ),
  152. );
  153. }
  154. Future<void> _requestPermission() async {
  155. try {
  156. // 使用permission_handler请求权限
  157. final status = await Permission.photos.request();
  158. final granted = status.isGranted;
  159. setState(() {
  160. _hasPermission = granted;
  161. if (!granted) {
  162. _errorMessage = '需要相册权限才能进行照片分类';
  163. } else {
  164. _errorMessage = '';
  165. }
  166. });
  167. // 如果被拒绝且不能再次请求,提示用户前往设置
  168. if (status.isPermanentlyDenied || status.isDenied) {
  169. setState(() {
  170. _errorMessage = '权限被拒绝,请点击下方按钮前往设置手动开启相册权限';
  171. });
  172. }
  173. } catch (e) {
  174. setState(() {
  175. _errorMessage = '请求权限失败: $e';
  176. });
  177. }
  178. }
  179. Future<void> _startClassificationWithStream() async {
  180. setState(() {
  181. _isClassifying = true;
  182. _errorMessage = '';
  183. _progress = null;
  184. _similarResult = [];
  185. _peopleResult = [];
  186. _screenshotResult = [];
  187. _blurryResult = [];
  188. });
  189. try {
  190. // 先配置分类器
  191. await _photoclassifier.configureClassifier(
  192. batchSize: 200,
  193. maxConcurrentProcessing: 4,
  194. similarityThreshold: 0.75,
  195. );
  196. // 设置流监听
  197. _subscription = _photoclassifier
  198. .startClassificationStream()
  199. .listen(
  200. (event) {
  201. if (event == null) return;
  202. setState(() {
  203. _progress = event.progress;
  204. print("批次: ${event.progress?.currentBatch}/${event.progress?.totalBatches}, 用时: ${event.progress?.batchDuration}");
  205. print("${event.result?.screenshotImages?.length}");
  206. var result = event.result;
  207. if (result != null) {
  208. _similarResult.addAll(result.similarGroups?.map((group) => group).toList() ?? []);
  209. _peopleResult.addAll(result.peopleImages?.map((image) => image).toList() ?? []);
  210. _screenshotResult.addAll(result.screenshotImages?.map((image) => image).toList() ?? []);
  211. _blurryResult.addAll(result.blurryImages?.map((image) => image).toList() ?? []);
  212. }
  213. if (event.progress?.isCompleted == true) {
  214. _isClassifying = false;
  215. _subscription?.cancel();
  216. // 取消订阅
  217. _subscription = null;
  218. }
  219. });
  220. },
  221. onError: (error) {
  222. setState(() {
  223. _errorMessage = '分类过程中出错: $error';
  224. _isClassifying = false;
  225. });
  226. },
  227. onDone: () {
  228. if (_progress?.isCompleted != true) {
  229. setState(() {
  230. _errorMessage = '分类过程意外结束';
  231. _isClassifying = false;
  232. });
  233. }
  234. },
  235. );
  236. } catch (e) {
  237. setState(() {
  238. _errorMessage = '启动分类失败: $e';
  239. _isClassifying = false;
  240. });
  241. }
  242. }
  243. void _cancelClassification() {
  244. _subscription?.cancel();
  245. _subscription = null;
  246. setState(() {
  247. _isClassifying = false;
  248. });
  249. _photoclassifier.resetClassifier();
  250. }
  251. void _resetAll() {
  252. setState(() {
  253. _progress = null;
  254. _similarResult = [];
  255. _peopleResult = [];
  256. _screenshotResult = [];
  257. _blurryResult = [];
  258. _errorMessage = '';
  259. _isClassifying = false;
  260. });
  261. // _photoclassifier.resetClassifier();
  262. }
  263. @override
  264. void dispose() {
  265. _subscription?.cancel();
  266. super.dispose();
  267. }
  268. }