12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061 |
- import 'dart:convert';
- import 'dart:math';
- import 'dart:typed_data';
- import 'dart:ui' as ui;
- import 'package:fis_common/index.dart';
- import 'package:fis_common/logger/logger.dart';
- import 'package:fis_i18n/i18n.dart';
- import 'package:fis_jsonrpc/rpc.dart';
- import 'package:fis_measure/interfaces/process/workspace/application.dart';
- import 'package:fis_measure/process/language/measure_language.dart';
- import 'package:fis_measure/process/visual/visual.dart';
- import 'package:fis_measure/process/workspace/measure_data_controller.dart';
- import 'package:fis_measure/process/workspace/rpc_helper.dart';
- import 'package:fis_measure/process/workspace/visual_loader.dart';
- import 'package:fis_measure/utils/prompt_box.dart';
- import 'package:fis_measure/values/unit_desc.dart';
- import 'package:fis_measure/view/ai_result_modifier/state.dart';
- import 'package:fis_measure/view/mobile_view/widgets/throttle.dart' as utils;
- import 'package:fis_ui/utils/sizer/sizer.dart';
- import 'package:flutter/gestures.dart';
- import 'package:flutter/material.dart';
- import 'package:get/get.dart';
- import 'package:path_drawing/path_drawing.dart';
- import 'package:vid/us/vid_us_image.dart';
- import 'package:vid/us/vid_us_unit.dart';
- import 'package:http/http.dart' as http;
- class AiResultModifierController extends GetxController {
- final rpcHelper = Get.find<RPCHelper>();
- MeasureDataController get measureData => Get.find<MeasureDataController>();
- /// 后台语言包控制器
- // final languageService = Get.find<LanguageService>();
- final state = AiResultModifierState();
- /// 传入参数 [图像code,图像帧下标,图像元数据, 图像编辑过的code]
- final String remedicalCode;
- final int currFrameIndex;
- final VidUsImage currFrame;
- /// 初次查询到的完整数据
- AIDiagnosisPerImageDTO resultDTO = AIDiagnosisPerImageDTO();
- /// 编辑后的完整数据【用于发给后端】
- AIDiagnosisPerImageDTO modifiedDataDTO = AIDiagnosisPerImageDTO();
- // 用于画布绘制的轮廓点集
- List<Offset> _canvasContoursPoints = [];
- // 用于画布绘制的关键点集【拖拽模式】
- List<Offset> _canvasKeyPoints = [];
- // 用于画布绘制的高亮关键点集【拖拽模式】
- final List<Offset> _canvasAffectedKeyPoints = [];
- // 用于画布绘制的病灶大小横纵比例线段【四个坐标下标】
- List<int> _canvasLesionSizePointsIndexes = [];
- // 用于画布绘制的轮廓关键点下标集合【画轮廓模式】
- final List<int> _canvasPenModeKeyPointIndexes = [];
- // 用于画布绘制的轮廓关键点下标集合【画轮廓模式】
- final List<Offset> _canvasNewContoursPoints = [];
- // 播放器组件的key
- final List<Offset> _aiPoints = [];
- // 病灶结论列表
- List<EnumItemDTO> _diagnosisEnumItems = [];
- // 当前横线像素长度
- int _horizontalLengthInPixel = 0;
- // 当前横线像素长度
- int _verticalLengthInPixel = 0;
- // 当前AI病灶下标
- int currentAiDetectedObjectIndex = 0;
- // 播放器区域的key
- GlobalKey framePlayerKey = GlobalKey();
- // 截图区域的key
- GlobalKey captureAreaKey = GlobalKey();
- // 画布组件的大小
- Size aiCanvasSize = Size.zero;
- // 图像的实际大小
- Size frameSize = Size.zero;
- // 图像的缩放比例
- double _scale = 1.0;
- // 图像的物理单位像素长度
- double _unitsPhysicalPixels = 0.0;
- // 图像的物理单位
- String _xUnit = '';
- // 当前的轮廓点集
- List<AIDiagnosisPoint2D> contours = [];
- // 当前的病灶大小
- AIDiagnosisLesionSize? lesionSize;
- // 当前的关键点集
- List<DiagnosisKeyPointDTO> keyPoints = [];
- // 当前受影响的高亮的关键点下标集合
- List<int> affectedKeyPointIndexes = [];
- // 当前操作模式
- AiResultModifierMode _mode = AiResultModifierMode.drag;
- // 当前是否正在绘制新轮廓
- bool _isDrawingNewContours = false;
- // 拖拽起点
- Offset _dragStartPoint = Offset.zero;
- // 拖拽开始时的轮廓点集【仅用于发请求】
- List<AIDiagnosisPoint2D> contoursOnDragStart = [];
- // 拖拽开始时的关键点集【仅用于发请求】
- List<DiagnosisKeyPointDTO> keyPointsOnDragStart = [];
- /// 测量语言包
- final measureLanguage = MeasureLanguage();
- AiResultModifierController(
- {required this.remedicalCode,
- required this.currFrameIndex,
- required this.currFrame});
- late final application = Get.find<IApplication>();
- /// 多个ai病灶
- List<AIDetectedObject> get aiDetectedObjectList =>
- modifiedDataDTO.diagResultsForEachOrgan?.first.detectedObjects ?? [];
- /// 当前病灶
- AIDetectedObject? get aiDetectedObject => modifiedDataDTO
- .diagResultsForEachOrgan
- ?.first
- .detectedObjects?[currentAiDetectedObjectIndex];
- List<Offset> get aiPoints => _aiPoints;
- List<Offset> get canvasAffectedKeyPoints => _canvasAffectedKeyPoints;
- List<Offset> get canvasContoursPoints => _canvasContoursPoints;
- List<Offset> get canvasKeyPoints => _canvasKeyPoints;
- List<int> get canvasLesionSizePointsIndexes => _canvasLesionSizePointsIndexes;
- List<Offset> get canvasNewContoursPoints => _canvasNewContoursPoints;
- List<int> get canvasPenModeKeyPointIndexes => _canvasPenModeKeyPointIndexes;
- AiResultModifierMode get currMode => _mode;
- List<EnumItemDTO> get diagnosisEnumItems => _diagnosisEnumItems;
- /// 当前器官
- DiagnosisOrganEnum get diagnosisOrgan =>
- modifiedDataDTO.diagResultsForEachOrgan?.first.organ ??
- DiagnosisOrganEnum.Null;
- /// 获取病灶的水平长度
- String get horizontalLength =>
- _countLesionLengthWithUnit(_horizontalLengthInPixel);
- /// 获取病灶的垂直长度
- String get verticalLength =>
- _countLesionLengthWithUnit(_verticalLengthInPixel);
- /// 病灶横纵比
- String get lesionRatio =>
- _verticalLengthInPixel / _horizontalLengthInPixel > 1 ||
- _verticalLengthInPixel / _horizontalLengthInPixel == 1
- ? '> 1'
- : '< 1';
- /// 切换操作模式
- void changeModifierMode(AiResultModifierMode newMode) {
- if (_mode == newMode) return;
- _mode = newMode;
- _canvasAffectedKeyPoints.clear();
- update(['ai_result_modifier', 'ai_mode_change_buttons']);
- }
- /// 切换ai病灶
- Future<void> changeAiDetectedObjectIndex(int index) async {
- _setNewCurrContoursToModifiedDataDTO(
- oldIndex: currentAiDetectedObjectIndex);
- currentAiDetectedObjectIndex = index;
- await _updateContoursByIndex(index);
- update(['ai_result_canvas', 'ai_conclusion_result', 'ai_index_tag']);
- }
- /// 切换病灶轮廓
- Future<void> _updateContoursByIndex(int index) async {
- contours = modifiedDataDTO
- .diagResultsForEachOrgan![0].detectedObjects![index].contours ??
- [];
- List<AIDiagnosisDescription>? descriptions = modifiedDataDTO
- .diagResultsForEachOrgan![0].detectedObjects![index].descriptions;
- //遍历 descriptions 取出病灶大小
- for (AIDiagnosisDescription description in descriptions!) {
- if (description.type == DiagnosisDescriptionEnum.LesionSize) {
- lesionSize =
- AIDiagnosisLesionSize.fromJson(jsonDecode(description.value ?? ""));
- }
- }
- keyPoints = await _queryAllKeyPoints();
- _canvasAffectedKeyPoints.clear();
- _updateCurrContoursPoints();
- _updateCurrKeyPoints();
- }
- /// 更新当前轮廓点集【要在 currentAiDetectedObjectIndex 更新前触发】
- void _setNewCurrContoursToModifiedDataDTO({required int oldIndex}) {
- List<AIDiagnosisPoint2D> newContours =
- _convertCanvasPoints(_canvasContoursPoints);
- modifiedDataDTO.diagResultsForEachOrgan![0].detectedObjects![oldIndex]
- .contours = newContours;
- List<AIDiagnosisDescription>? descriptions = modifiedDataDTO
- .diagResultsForEachOrgan![0].detectedObjects![oldIndex].descriptions;
- //遍历 descriptions 更新病灶大小
- for (var i = 0; i < descriptions!.length; i++) {
- if (descriptions[i].type == DiagnosisDescriptionEnum.LesionSize) {
- descriptions[i].value = jsonEncode(lesionSize);
- }
- }
- modifiedDataDTO.diagResultsForEachOrgan![0].detectedObjects![oldIndex]
- .descriptions = descriptions;
- }
- /// 获取当前的新病灶大小
- AIDiagnosisLesionSize _getNewLesionSize(List<AIDiagnosisPoint2D> p1234,
- int newHorizontalLengthInPixel, int newVerticalLengthInPixel) {
- return AIDiagnosisLesionSize(
- horizontalPoint1: p1234[0],
- horizontalPoint2: p1234[1],
- verticalPoint1: p1234[2],
- verticalPoint2: p1234[3],
- horizontalLengthInPixel: newHorizontalLengthInPixel,
- verticalLengthInPixel: newVerticalLengthInPixel);
- }
- /// 上传当前封面图以及压缩后的缩略图
- Future<ImageUrls> _getCurrImageUrls() async {
- try {
- final Uint8List vidImageBytes = currFrame.imageData;
- /// 生成缩略图
- final Rect offscreenCanvasRect =
- Rect.fromLTWH(0, 0, frameSize.width, frameSize.height);
- final ui.PictureRecorder recorder = ui.PictureRecorder();
- final Canvas offscreenCanvas = Canvas(recorder, offscreenCanvasRect);
- offscreenCanvas.drawImage(
- await decodeImageFromList(vidImageBytes), Offset.zero, Paint());
- _paintAllContours(offscreenCanvas);
- final ui.Image orginalFileImage = await recorder
- .endRecording()
- .toImage(currFrame.width.toInt(), currFrame.height.toInt());
- final orginalFileByteData =
- await orginalFileImage.toByteData(format: ui.ImageByteFormat.png);
- final orginalFileByteDataBuffer =
- orginalFileByteData!.buffer.asUint8List();
- final String aiFileToken = await rpcHelper.rpc.storage.uploadUint8List(
- orginalFileByteDataBuffer,
- "ai_modified_orginal_${remedicalCode}_$currFrameIndex.png",
- rpcHelper.userToken) ??
- '';
- print('coverUrl: $aiFileToken');
- /// 生成缩略图
- final double scale = _calcScale(
- srcWidth: currFrame.width.toDouble(),
- srcHeight: currFrame.height.toDouble(),
- minWidth: 200,
- minHeight: 200,
- );
- final int scaledWidth = currFrame.width ~/ scale;
- final int scaledHeight = currFrame.height ~/ scale;
- final Rect previewOffscreenCanvasRect =
- Rect.fromLTWH(0, 0, scaledWidth.toDouble(), scaledHeight.toDouble());
- final ui.PictureRecorder lowLevelRecorder = ui.PictureRecorder();
- final Canvas previewOffscreenCanvas =
- Canvas(lowLevelRecorder, previewOffscreenCanvasRect);
- previewOffscreenCanvas.drawImageRect(orginalFileImage,
- offscreenCanvasRect, previewOffscreenCanvasRect, Paint());
- final ui.Image previewFileImage = await lowLevelRecorder
- .endRecording()
- .toImage(scaledWidth, scaledHeight);
- final previewFileByteData =
- await previewFileImage.toByteData(format: ui.ImageByteFormat.png);
- final previewFileByteDataBuffer =
- previewFileByteData!.buffer.asUint8List();
- final String previewFileUrl = await rpcHelper.rpc.storage.uploadUint8List(
- previewFileByteDataBuffer,
- "ai_modified_preview_${remedicalCode}_$currFrameIndex.png",
- rpcHelper.userToken) ??
- '';
- print('previewFileUrl: $previewFileUrl');
- return ImageUrls(
- aiFileToken: aiFileToken, previewFileUrl: previewFileUrl);
- } catch (e) {
- logger.e('get screenshot failed', e);
- return ImageUrls(aiFileToken: '', previewFileUrl: '', isUploaded: false);
- }
- }
- /// 计算压缩倍率
- double _calcScale({
- required double srcWidth,
- required double srcHeight,
- required double minWidth,
- required double minHeight,
- }) {
- var scaleW = srcWidth / minWidth;
- var scaleH = srcHeight / minHeight;
- var scale = max(1.0, min(scaleW, scaleH));
- return scale;
- }
- /// 在图像上绘制所有轮廓
- void _paintAllContours(Canvas canvas) {
- for (var i = 0;
- i < modifiedDataDTO.diagResultsForEachOrgan![0].detectedObjects!.length;
- i++) {
- contours = modifiedDataDTO
- .diagResultsForEachOrgan![0].detectedObjects![i].contours ??
- [];
- // 设置虚线圆点画笔
- final contoursPaint = Paint()
- ..color = Colors.green
- ..strokeCap = StrokeCap.round
- ..strokeWidth = 3.0
- ..style = PaintingStyle.stroke;
- // 遍历 contoursPoints 绘制轮廓
- if (contours.isNotEmpty) {
- Path path = Path();
- path.moveTo(contours[0].x.toDouble(), contours[0].y.toDouble());
- for (int i = 1; i < contours.length; i++) {
- path.lineTo(contours[i].x.toDouble(), contours[i].y.toDouble());
- }
- path.close();
- canvas.drawPath(
- dashPath(
- path,
- dashArray: CircularIntervalList<double>([1, 10]),
- ),
- contoursPaint);
- }
- AIDiagnosisLesionSize currLesionSize = AIDiagnosisLesionSize();
- List<AIDiagnosisDescription>? descriptions = modifiedDataDTO
- .diagResultsForEachOrgan![0].detectedObjects![i].descriptions;
- //遍历 descriptions 取出病灶大小
- //Descriptions 为空属于非正常数据
- if (descriptions!.isEmpty) {
- continue;
- }
- for (AIDiagnosisDescription description in descriptions) {
- if (description.type == DiagnosisDescriptionEnum.LesionSize) {
- currLesionSize = AIDiagnosisLesionSize.fromJson(
- jsonDecode(description.value ?? ""));
- }
- }
- final AIDiagnosisPoint2D p1 = currLesionSize.horizontalPoint1!;
- final AIDiagnosisPoint2D p2 = currLesionSize.horizontalPoint2!;
- final AIDiagnosisPoint2D p3 = currLesionSize.verticalPoint1!;
- final AIDiagnosisPoint2D p4 = currLesionSize.verticalPoint2!;
- Path path = Path();
- path.moveTo(p1.x.toDouble(), p1.y.toDouble());
- path.lineTo(p2.x.toDouble(), p2.y.toDouble());
- canvas.drawPath(
- dashPath(
- path,
- dashArray: CircularIntervalList<double>([1, 5]),
- ),
- contoursPaint);
- Path path2 = Path();
- path2.moveTo(p3.x.toDouble(), p3.y.toDouble());
- path2.lineTo(p4.x.toDouble(), p4.y.toDouble());
- canvas.drawPath(
- dashPath(
- path2,
- dashArray: CircularIntervalList<double>([1, 5]),
- ),
- contoursPaint);
- paintX(
- canvas,
- Offset(p1.x.toDouble(), p1.y.toDouble()),
- 6.0,
- 3,
- Colors.green,
- );
- paintX(
- canvas,
- Offset(p2.x.toDouble(), p2.y.toDouble()),
- 6.0,
- 3,
- Colors.green,
- );
- paintX(
- canvas,
- Offset(p3.x.toDouble(), p3.y.toDouble()),
- 6.0,
- 3,
- Colors.green,
- );
- paintX(
- canvas,
- Offset(p4.x.toDouble(), p4.y.toDouble()),
- 6.0,
- 3,
- Colors.green,
- );
- }
- }
- /// 绘制叉叉
- void paintX(
- Canvas canvas, Offset center, double radius, double width, Color color) {
- final paint = Paint()
- ..color = color
- ..strokeCap = StrokeCap.round
- ..strokeWidth = width
- ..style = PaintingStyle.stroke;
- Path path = Path();
- path.moveTo(center.dx - radius, center.dy - radius);
- path.lineTo(center.dx + radius, center.dy + radius);
- path.moveTo(center.dx + radius, center.dy - radius);
- path.lineTo(center.dx - radius, center.dy + radius);
- canvas.drawPath(path, paint);
- }
- /// 获取AI模块的翻译值
- String getValuesFromAiLanguage(String code) {
- final value = measureLanguage.t('ai', code);
- return value;
- }
- /// 重置AI结果
- void resetAIResult() async {
- await _initAIResult();
- update(['ai_conclusion_result']);
- }
- @override
- void onClose() {
- super.onClose();
- Sizer.ins.removeListener(_onWindowResize);
- }
- @override
- void onInit() async {
- super.onInit();
- await _getDiagnosisEnumItemsAsync();
- _updateModifierInteractiveLayerSize();
- _updateImagePhysicalSize();
- _initAIResult();
- Sizer.ins.addListener(_onWindowResize);
- }
- /// 窗口大小改变
- void _onWindowResize(_) {
- update(['ai_result_modifier']);
- frameSize = Size(currFrame.width.toDouble(), currFrame.height.toDouble());
- WidgetsBinding.instance.addPostFrameCallback((_) {
- final RenderBox box =
- framePlayerKey.currentContext!.findRenderObject() as RenderBox;
- final framePlayerSize = Size(box.size.width, box.size.height);
- _scale = min(framePlayerSize.width / frameSize.width,
- framePlayerSize.height / frameSize.height);
- aiCanvasSize = Size(frameSize.width * _scale, frameSize.height * _scale);
- _updateModifierInteractiveLayerSize();
- _updateCurrKeyPoints();
- _updateCurrContoursPoints();
- _updateCurrAffectedKeyPoints();
- // 更新交互层尺寸
- update(["ai_result_modifier_interactive_layer", "ai_result_canvas"]);
- });
- }
- /// 鼠标拖拽
- void onMouseDrag(DragUpdateDetails details) {
- switch (_mode) {
- case AiResultModifierMode.drag:
- utils.throttle(() {
- _onDragModeCallDragFunction(details.localPosition);
- }, 'onMouseDrag', 100);
- break;
- case AiResultModifierMode.pen:
- _onPenModeCallDragFunction(details.localPosition);
- break;
- default:
- }
- }
- /// 鼠标拖拽结束
- void onMouseDragEnd(DragEndDetails details) async {
- switch (_mode) {
- case AiResultModifierMode.drag:
- break;
- case AiResultModifierMode.pen:
- if (_isDrawingNewContours) {
- _isDrawingNewContours = false;
- await _callContourMergeAsync();
- _updateCurrContoursPoints();
- _updateCurrKeyPoints();
- }
- _canvasNewContoursPoints.clear();
- update(['ai_result_canvas']);
- break;
- default:
- }
- }
- /// 鼠标拖拽开始【记录起点】
- void onMouseDragStart(DragDownDetails details) {
- switch (_mode) {
- case AiResultModifierMode.drag:
- _dragStartPoint = details.localPosition;
- contoursOnDragStart = contours;
- keyPointsOnDragStart = keyPoints;
- break;
- case AiResultModifierMode.pen:
- if (_canvasPenModeKeyPointIndexes.isNotEmpty) {
- _isDrawingNewContours = true;
- _dragStartPoint = details.localPosition;
- _canvasNewContoursPoints.clear();
- _canvasNewContoursPoints
- .add(_canvasContoursPoints[_canvasPenModeKeyPointIndexes[0]]);
- _canvasNewContoursPoints.add(_dragStartPoint);
- }
- break;
- default:
- }
- }
- /// 鼠标离开区域
- void onMouseExit(PointerExitEvent e) async {
- // 延迟200ms (因为鼠标位置更新高亮关键点有100ms延迟)
- await Future.delayed(const Duration(milliseconds: 200));
- _canvasAffectedKeyPoints.clear();
- update(['ai_result_canvas']);
- }
- /// 鼠标悬浮移动
- void onMouseHover(PointerHoverEvent e) async {
- if (keyPoints.isEmpty) return;
- switch (_mode) {
- case AiResultModifierMode.drag:
- utils.throttle(() {
- _onDragModeCallHoverFunction(e.localPosition);
- }, 'onMouseHover', 100);
- break;
- case AiResultModifierMode.pen:
- utils.throttle(() {
- _onPenModeCallHoverFunction(e.localPosition);
- }, 'onMouseHover', 10);
- // Offset point = e.localPosition;
- break;
- default:
- }
- }
- @override
- void onReady() {
- super.onReady();
- _initData();
- }
- /// 保存AI修改结果
- Future<void> saveAIResult({
- String? code,
- }) async {
- PromptBox.toast(i18nBook.realTimeConsultation.uploading.t);
- _setNewCurrContoursToModifiedDataDTO(
- oldIndex: currentAiDetectedObjectIndex);
- try {
- final ImageUrls imageUrls = await _getCurrImageUrls();
- if (!imageUrls.isUploaded) {
- logger.e("Url not uploaded");
- PromptBox.toast(i18nBook.user.saveFailed.t);
- return;
- }
- //该逻辑是为了判断当前是否是编辑状态
- //如果AIEditCode为空,说明当前模式为从源Vid里新建单帧Vid进行AI信息的update操作
- //否则,则是在一个编辑过的Vid上面进行AI信息的update操作
- bool isAIEditMode = measureData
- .measureImageData.remedicalAISelectedInfoCode.isNotNullOrEmpty;
- final result =
- await rpcHelper.rpc.remedical.saveRemedicalAISelectedInfoAsync(
- SaveRemedicalAISelectedInfoRequest(
- token: rpcHelper.userToken,
- remedicalCode: remedicalCode,
- code: isAIEditMode
- ? measureData.measureImageData.remedicalAISelectedInfoCode
- : null,
- frameIndex: currFrameIndex,
- // diagnosisConclusion: diagnosisOrgan,
- previewFileToken: imageUrls.previewFileUrl,
- aIFileToken: imageUrls.aiFileToken,
- diagnosisData: jsonEncode(modifiedDataDTO),
- ),
- );
- if (result) {
- PromptBox.toast(
- "${i18nBook.user.saveSuccess.t} \r\n ${i18nBook.measure.saveLocation.t + ' > ' + i18nBook.measure.aiImage.t}");
- Get.back();
- } else {
- logger.e("Server result is false");
- PromptBox.toast(i18nBook.user.saveFailed.t);
- }
- } catch (e) {
- logger.e("Operation failed with exception ${e} ");
- PromptBox.toast(i18nBook.user.saveFailed.t);
- }
- }
- /// 加载AI结果并调用绘制
- Future<void> _initAIResult() async {
- try {
- var existAIResult = jsonDecode(measureData.aiResults);
- //当aiResult==1时,为具有单帧AI结果图像,无论是编辑过的还是原vid的单帧图像
- //均可用此从measureData里读取,不受影响,因此该处无需区分图像源是AI编辑还是普通VID
- if (measureData
- .measureImageData.remedicalAISelectedInfoCode.isNotNullOrEmpty &&
- existAIResult.length == 1) {
- resultDTO = AIDiagnosisPerImageDTO.fromJson(existAIResult[0]);
- } else {
- final result =
- await rpcHelper.rpc.remedical.getRemedicalDiagnosisDataAsync(
- GetRemedicalDiagnosisDataRequest(
- token: rpcHelper.userToken,
- remedicalCode: remedicalCode,
- frameIndex: currFrameIndex,
- ),
- );
- resultDTO = AIDiagnosisPerImageDTO.fromJson(jsonDecode(result));
- }
- List<AIDetectedObject> legalObjs = [];
- var tempResultDto = resultDTO;
- var rawObjs = tempResultDto.diagResultsForEachOrgan![0].detectedObjects!;
- for (var detectedObject in rawObjs) {
- var isLegalObject = detectedObject.descriptions?.isNotEmpty ?? false;
- if (isLegalObject) {
- legalObjs.add(detectedObject);
- }
- }
- rawObjs.clear();
- rawObjs.addAll(legalObjs);
- modifiedDataDTO = tempResultDto;
- contours = resultDTO.diagResultsForEachOrgan![0]
- .detectedObjects![currentAiDetectedObjectIndex].contours ??
- [];
- List<AIDiagnosisDescription>? descriptions = resultDTO
- .diagResultsForEachOrgan![0]
- .detectedObjects![currentAiDetectedObjectIndex]
- .descriptions;
- //遍历 descriptions 取出病灶大小
- for (AIDiagnosisDescription description in descriptions!) {
- if (description.type == DiagnosisDescriptionEnum.LesionSize) {
- lesionSize = AIDiagnosisLesionSize.fromJson(
- jsonDecode(description.value ?? ""));
- }
- }
- keyPoints = await _queryAllKeyPoints();
- _canvasAffectedKeyPoints.clear();
- _updateCurrContoursPoints();
- _updateCurrKeyPoints();
- update(['ai_result_canvas', 'ai_result_panel', 'ai_index_tag']);
- } catch (e) {
- logger.e('load ai result failed', e);
- }
- }
- /// 更新交互层尺寸
- void _updateModifierInteractiveLayerSize() {
- frameSize = Size(currFrame.width.toDouble(), currFrame.height.toDouble());
- WidgetsBinding.instance.addPostFrameCallback((_) {
- final RenderBox box =
- framePlayerKey.currentContext!.findRenderObject() as RenderBox;
- final framePlayerSize = Size(box.size.width, box.size.height);
- _scale = min(framePlayerSize.width / frameSize.width,
- framePlayerSize.height / frameSize.height);
- aiCanvasSize = Size(frameSize.width * _scale, frameSize.height * _scale);
- /// 更新交互层尺寸
- update(["ai_result_modifier_interactive_layer"]);
- });
- }
- /// 计算带单位的病灶长度
- String _countLesionLengthWithUnit(int length) {
- String lengthStr =
- (length * _unitsPhysicalPixels).toStringAsFixed(2).toString();
- return "$lengthStr $_xUnit";
- }
- /// 更新图像物理尺度信息
- void _updateImagePhysicalSize() {
- _unitsPhysicalPixels =
- (application.visuals[0].visualAreas[0].viewport?.region.width)! /
- (application.frameData!.width).toDouble();
- VidUsUnit targetUnit =
- application.visuals[0].visualAreas[0].viewport?.xUnit ?? VidUsUnit.cm;
- _xUnit = UnitDescriptionMap.getDesc(targetUnit);
- }
- /// 自动吸附闭合判断
- void _autoCloseContours() async {
- if (_canvasNewContoursPoints.length < 6) return;
- double minDistance = double.infinity;
- int nearestKeyPointIndex = -1;
- final lastPoint = _canvasNewContoursPoints.last;
- for (int i = 0; i < canvasContoursPoints.length; i++) {
- final point = canvasContoursPoints[i];
- final double distance = (point - lastPoint).distance;
- if (distance < minDistance) {
- minDistance = distance;
- nearestKeyPointIndex = i;
- }
- }
- if (minDistance < 6) {
- _canvasPenModeKeyPointIndexes.add(nearestKeyPointIndex);
- _canvasNewContoursPoints.add(canvasContoursPoints[nearestKeyPointIndex]);
- _isDrawingNewContours = false;
- await _callContourMergeAsync();
- _updateCurrContoursPoints();
- _updateCurrKeyPoints();
- }
- }
- /// 发送请求通知后端合并轮廓
- Future<bool> _callContourMergeAsync() async {
- final ContourMergeResult result =
- await rpcHelper.rpc.aIDiagnosis.contourMergeAsync(
- ContourMergeRequest(
- token: rpcHelper.userToken,
- contourPoints: contours,
- lesionSize: lesionSize,
- drawingNewContourPoints: _convertCanvasPoints(_canvasNewContoursPoints),
- ),
- );
- contours = result.dstContours ?? [];
- lesionSize = result.dstLesionSize;
- keyPoints = await _queryAllKeyPoints();
- return true;
- }
- /// 画布坐标系转换【画布坐标系 -> 接口坐标系】
- List<AIDiagnosisPoint2D> _convertCanvasPoints(List<Offset> points) {
- List<AIDiagnosisPoint2D> result = [];
- for (Offset point in points) {
- result.add(
- AIDiagnosisPoint2D(x: point.dx ~/ _scale, y: point.dy ~/ _scale));
- }
- return result;
- }
- /// 关键点坐标转换【接口坐标系 -> 画布坐标系】同时更新横纵比例线段下标
- List<Offset> _convertKeyPoints(List<DiagnosisKeyPointDTO> points) {
- List<Offset> result = [];
- List<int> pointIndexes = List.generate(4, (_) => 0);
- for (int i = 0; i < points.length; i++) {
- final point = points[i];
- if (point.point == null) continue;
- result.add(Offset(point.point!.x.toDouble() * _scale,
- point.point!.y.toDouble() * _scale));
- if (point.type != DiagnosisKeyPointType.OtherKeyPoints) {
- switch (point.type) {
- case DiagnosisKeyPointType.HorizontalPointLeft:
- pointIndexes[0] = i;
- break;
- case DiagnosisKeyPointType.HorizontalPointRight:
- pointIndexes[1] = i;
- break;
- case DiagnosisKeyPointType.VerticalPointUp:
- pointIndexes[2] = i;
- break;
- case DiagnosisKeyPointType.VerticalPointDown:
- pointIndexes[3] = i;
- break;
- default:
- }
- }
- }
- _canvasLesionSizePointsIndexes = pointIndexes;
- _updateLesionSizeAndRatio();
- return result;
- }
- /// 坐标转换【接口坐标系 -> 画布坐标系】
- List<Offset> _convertPoints(List<AIDiagnosisPoint2D> points) {
- List<Offset> result = [];
- for (AIDiagnosisPoint2D point in points) {
- result.add(
- Offset(point.x.toDouble() * _scale, point.y.toDouble() * _scale));
- }
- return result;
- }
- /// 获取ai结果相关枚举集合
- Future<void> _getDiagnosisEnumItemsAsync() async {
- final getDiagnosisEnumItems =
- await rpcHelper.rpc.aIDiagnosis.getDiagnosisEnumItemsAsync(
- GetDiagnosisEnumItemsRequest(
- token: rpcHelper.userToken,
- ),
- );
- _diagnosisEnumItems = getDiagnosisEnumItems.source ?? [];
- }
- void _initData() {
- update(["ai_result_modifier"]);
- }
- /// 在拖拽模式下触发拖拽事件【每隔100ms触发一次】
- void _onDragModeCallDragFunction(Offset pos) async {
- AIDiagnosisPoint2D startPoint = AIDiagnosisPoint2D(
- x: _dragStartPoint.dx ~/ _scale, y: _dragStartPoint.dy ~/ _scale);
- AIDiagnosisPoint2D endPoint =
- AIDiagnosisPoint2D(x: pos.dx ~/ _scale, y: pos.dy ~/ _scale);
- final bool success = await _queryDragResult(startPoint, endPoint);
- if (success) {
- _updateCurrKeyPoints();
- _updateCurrContoursPoints();
- _updateCurrAffectedKeyPoints();
- update(["ai_result_canvas"]);
- }
- }
- /// 在拖拽模式下,通过鼠标位置更新高亮的关键点下标【每隔100ms触发一次】
- void _onDragModeCallHoverFunction(Offset localPosition) async {
- final mousePos = AIDiagnosisPoint2D(
- x: localPosition.dx ~/ _scale, y: localPosition.dy ~/ _scale);
- affectedKeyPointIndexes = await _queryAffectedKeyPoints(mousePos);
- _updateCurrAffectedKeyPoints();
- update(["ai_result_canvas"]);
- }
- /// 在画轮廓模式下触发拖拽事件
- void _onPenModeCallDragFunction(Offset pos) async {
- if (!_isDrawingNewContours) return;
- // 点间距【疏密程度】
- const double pointDistance = 8;
- final double distance = (pos - _canvasNewContoursPoints.last).distance;
- if (distance >= pointDistance) {
- int numPointsToInsert = (distance / pointDistance).ceil() - 1; // 需要插入的点数
- for (int i = 0; i < numPointsToInsert; i++) {
- double t = (i + 1) / (numPointsToInsert + 1);
- Offset interpolatedPoint = Offset(
- _canvasNewContoursPoints.last.dx +
- t * (pos.dx - _canvasNewContoursPoints.last.dx),
- _canvasNewContoursPoints.last.dy +
- t * (pos.dy - _canvasNewContoursPoints.last.dy),
- );
- _canvasNewContoursPoints.add(interpolatedPoint);
- }
- _canvasNewContoursPoints.add(pos);
- update(["ai_result_canvas"]);
- }
- _autoCloseContours();
- }
- /// 在画轮廓模式下,通过鼠标位置更新最近的关键点【每隔10ms触发一次】
- void _onPenModeCallHoverFunction(Offset localPosition) async {
- double minDistance = double.infinity;
- int nearestKeyPointIndex = -1;
- for (int i = 0; i < canvasContoursPoints.length; i++) {
- final point = canvasContoursPoints[i];
- final double distance = (point - localPosition).distance;
- if (distance < minDistance) {
- minDistance = distance;
- nearestKeyPointIndex = i;
- }
- }
- _canvasPenModeKeyPointIndexes.clear();
- if (minDistance < 10) {
- _canvasPenModeKeyPointIndexes.add(nearestKeyPointIndex);
- }
- update(["ai_result_canvas"]);
- }
- /// 根据鼠标位置查询受影响的关键点
- Future<List<int>> _queryAffectedKeyPoints(AIDiagnosisPoint2D mousePos) async {
- try {
- final List<int> result =
- await rpcHelper.rpc.aIDiagnosis.affectedKeyPointsByDragActionAsync(
- AffectedKeyPointsByDragActionRequest(
- token: rpcHelper.userToken,
- keyPoints: keyPoints,
- mousePoint: mousePos,
- ),
- );
- // print(result);
- return result;
- } catch (e) {
- return [];
- }
- }
- /// 查询所有关键点【需要先存好contours和lesionSize】
- Future<List<DiagnosisKeyPointDTO>> _queryAllKeyPoints() async {
- try {
- final List<DiagnosisKeyPointDTO> result =
- await rpcHelper.rpc.aIDiagnosis.getKeyPointsOfContourAsync(
- GetKeyPointsOfContourRequest(
- token: rpcHelper.userToken,
- contours: contours,
- lesionSize: lesionSize,
- ),
- );
- return result;
- } catch (e) {
- return [];
- }
- }
- /// 查询拖拽结果集合【需要先存好 contoursOnDragStart 和 keyPointsOnDragStart】
- Future<bool> _queryDragResult(
- AIDiagnosisPoint2D startPoint, AIDiagnosisPoint2D endPoint) async {
- try {
- final ContourAndKeyPointsAfterDragResult result =
- await rpcHelper.rpc.aIDiagnosis.contourAndKeyPointsAfterDragAsync(
- ContourAndKeyPointsAfterDragRequest(
- token: rpcHelper.userToken,
- contours: contoursOnDragStart,
- keyPoints: keyPointsOnDragStart,
- startPoint: startPoint,
- endPoint: endPoint,
- ),
- );
- keyPoints = result.dstKeyPoints ?? [];
- contours = result.dstContours ?? [];
- affectedKeyPointIndexes = result.affectedKeyPointIndexes!;
- return true;
- } catch (e) {
- return false;
- }
- }
- // 根据病灶四个点位置横纵比参数
- void _updateLesionSizeAndRatio() {
- if (_canvasLesionSizePointsIndexes.length != 4) return;
- if (keyPoints.length < 4) return;
- final pIndexs = _canvasLesionSizePointsIndexes;
- final p1 = keyPoints[pIndexs[0]].point!;
- final p2 = keyPoints[pIndexs[1]].point!;
- final p3 = keyPoints[pIndexs[2]].point!;
- final p4 = keyPoints[pIndexs[3]].point!;
- /// 计算 p1 到 p2 的像素距离 更新到 _horizontalLengthInPixel
- /// 计算 p3 到 p4 的像素距离 更新到 _verticalLengthInPixel
- double _horizontalLength =
- (Offset(p1.x.toInt().toDouble(), p1.y.toInt().toDouble()) -
- Offset(p2.x.toInt().toDouble(), p2.y.toInt().toDouble()))
- .distance;
- _horizontalLengthInPixel = _horizontalLength.ceil();
- double _verticalLength =
- (Offset(p3.x.toInt().toDouble(), p3.y.toInt().toDouble()) -
- Offset(p4.x.toInt().toDouble(), p4.y.toInt().toDouble()))
- .distance;
- _verticalLengthInPixel = _verticalLength.ceil();
- lesionSize = _getNewLesionSize(
- [p1, p2, p3, p4], _horizontalLengthInPixel, _verticalLengthInPixel);
- update(['ai_result_lesion_size', 'ai_result_lesion_ratio']);
- }
- /// [⭐ _canvasAffectedKeyPoints ] 根据当前的受影响关键点下标更新受影响关键点集
- void _updateCurrAffectedKeyPoints() {
- _canvasAffectedKeyPoints.clear();
- if (keyPoints.isEmpty) return;
- for (int i = 0; i < keyPoints.length; i++) {
- if (affectedKeyPointIndexes.contains(i)) {
- _canvasAffectedKeyPoints.add(Offset(
- keyPoints[i].point!.x.toDouble() * _scale,
- keyPoints[i].point!.y.toDouble() * _scale));
- }
- }
- }
- /// [⭐ _canvasContoursPoints ] 更新当前轮廓点
- void _updateCurrContoursPoints() {
- _canvasContoursPoints = _convertPoints(contours);
- }
- /// [⭐ _canvasKeyPoints ] 更新当前关键点
- void _updateCurrKeyPoints() async {
- _canvasKeyPoints = _convertKeyPoints(keyPoints);
- }
- }
- enum AiResultModifierMode {
- /// 拖拽
- drag,
- /// 画笔
- pen,
- /// 截图
- screenshot,
- }
- ///存储服务扩展类
- extension StorageServiceExt on StorageService {
- ///鉴权 fileName 为空则接口报错,所以此处设置一个默认值
- Future<StorageServiceSettingDTO> getAuth({
- String? fileName,
- bool? isRechristen,
- List<DataItemDTO>? urlParams,
- List<DataItemDTO>? headerParams,
- String? requestMethod,
- required String userToken,
- }) async {
- try {
- final result = await getAuthorizationAsync(FileServiceRequest(
- token: userToken,
- fileName: fileName ?? "dat",
- isRechristen: isRechristen ?? true,
- urlParams: urlParams,
- headerParams: headerParams,
- requestMethod: requestMethod,
- ));
- return result;
- } catch (e) {
- return StorageServiceSettingDTO();
- }
- }
- ///文件上传(UInt8List)
- Future<String?> uploadUint8List(Uint8List buffer, String name, String token,
- [bool? isRechristen]) async {
- try {
- var nameInfos = name.split('.');
- final auth = await getAuth(
- fileName: nameInfos.last,
- isRechristen: isRechristen,
- userToken: token,
- );
- Map<String, String> params = {};
- params['Authorization'] = auth.authorization!;
- params['ContentType'] = auth.contentType!;
- final response = await http
- .put(
- Uri.parse(auth.storageUrl!),
- body: buffer,
- headers: params,
- )
- .timeout(
- const Duration(seconds: 30),
- );
- if (response.statusCode == 200) {
- return auth.storageUrl;
- }
- } catch (e) {
- logger.e('StorageServiceExt uploadUint8List ex:$e');
- }
- return null;
- }
- }
- class ImageUrls {
- /// 原始图像地址
- String aiFileToken;
- /// 缩略图地址
- String previewFileUrl;
- /// 是否已经上传
- bool isUploaded = true;
- ImageUrls({
- required this.aiFileToken,
- required this.previewFileUrl,
- this.isUploaded = true,
- });
- }
|