123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128 |
- using System.Collections.Generic;
- using System;
- using System.IO;
- using System.IO.Compression;
- using System.Text;
- using System.Runtime.InteropServices;
- using AI.Common;
- using AI.Common.Log;
- using AI.Common.Tools;
- namespace AI.Reconstruction
- {
- /// <summary>
- /// 原始的体数据
- /// </summary>
- public class RawVolumeData
- {
- #region private
- private byte[] _dataBuffer = new byte[0];
- private readonly object _dataLocker = new object();
- private EnumColorType _colorType = EnumColorType.Gray8;
- private int _bytesPerPixel = 1;
- private int _width = 0;
- private int _depth = 0;
- private int _imageCount = 0;
- private RectF _logicalCoordRegion = RectF.Empty;
- private float _physicalWidth = 0;
- private float _physicalDepth = 0;
- private float _physicalLength = 0;
- private float _spacingXY = 0;
- private TransducerImgPlaneToWorkCoordMatrix[] _transducerPoses = null;
- private EnumVolumeDataScanType _scanType = EnumVolumeDataScanType.NotSpecified;
- private bool _isCropped = false;
- private bool _isParallel = false;
- private bool _isInterpolated = false;
- private bool _isUniformCube = false;
- private byte[] _extendedData = new byte[0];
- private const string SurfaceFileSuffix = "sixface.surface";
- private const string ModelFileSuffix = "volume.model";
- private const string ZipFileSuffix = "volume.zip";
- private const string MdlZipFileVersion = "V1";
- #endregion
- #region events
- /// <summary>
- /// 通知订阅者,重建过程中发生了错误
- /// </summary>
- public event EventHandler<ErrorEventArgs> NotifyError;
- /// <summary>
- /// 通知订阅者,有log要记
- /// </summary>
- public event EventHandler<LogEventArgs> NotifyLog;
- #endregion
- #region property
- /// <summary>
- /// 是否为一个均一的立方体
- /// </summary>
- public bool IsUniformCube
- {
- get => _isUniformCube;
- }
- /// <summary>
- /// 数据区
- /// </summary>
- public byte[] DataBuffer
- {
- get => _dataBuffer;
- }
- /// <summary>
- /// 图像是哪种类型的灰度或彩色
- /// </summary>
- public EnumColorType ColorType
- {
- get => _colorType;
- }
- /// <summary>
- /// 宽度方向的像素个数
- /// </summary>
- public int Width
- {
- get => _width;
- }
- /// <summary>
- /// 深度方向的像素个数
- /// </summary>
- public int Depth
- {
- get => _depth;
- }
- /// <summary>
- /// 图像数量
- /// </summary>
- public int ImageCount
- {
- get => _imageCount;
- }
- /// <summary>
- /// 物理宽度
- /// </summary>
- public float PhysicalWidth
- {
- get => _physicalWidth;
- }
- /// <summary>
- /// 物理深度
- /// </summary>
- public float PhysicalDepth
- {
- get => _physicalDepth;
- }
- /// <summary>
- /// 物理长度
- /// </summary>
- public float PhysicalLength
- {
- get => _physicalLength;
- }
- /// <summary>
- /// xy方向的像素间隔在实际物理坐标系下的长度
- /// </summary>
- public float SpacingXY
- {
- get => _spacingXY;
- }
- /// <summary>
- /// 获取探头成像平面到世界坐标系下的变换矩阵
- /// </summary>
- public TransducerImgPlaneToWorkCoordMatrix[] TransducerPoses
- {
- get => _transducerPoses;
- }
- /// <summary>
- /// 获取扫查的类型
- /// </summary>
- public EnumVolumeDataScanType ScanType
- {
- get => _scanType;
- }
- /// <summary>
- /// DataBuffer总共所占的有效内存空间
- /// </summary>
- public int DataBufferByteCounts
- {
- get => _bytesPerPixel * _width * _depth * _imageCount;
- }
- #endregion
- #region constructor
- /// <summary>
- /// 创建空的RawVolumeData
- /// </summary>
- /// <param name="scanType"></param>
- /// <param name="colorType"></param>
- public RawVolumeData()
- {
- }
- #endregion
- #region public
- /// <summary>
- /// 直接通过一系列VinnoImage的DataBuffer创建(直线扫查,只给physicalLength即可)
- /// 注意:VinnoImage里的DataBuffer是未解码的
- /// </summary>
- /// <param name="vinnoImageDataBuffers"></param>
- /// <param name="width"></param>
- /// <param name="depth"></param>
- /// <param name="logicalCoordRegion"></param>
- /// <param name="physicalWidth"></param>
- /// <param name="physicalDepth"></param>
- /// <param name="physicalLength"></param>
- /// <param name="scanType"></param>
- /// <param name="extendedData"></param>
- public bool ReadFromVinnoImageDatas(List<byte[]> vinnoImageDataBuffers, int width, int depth, RectF logicalCoordRegion,
- float physicalWidth, float physicalDepth, float physicalLength,
- EnumVolumeDataScanType scanType = EnumVolumeDataScanType.NotSpecified,byte[] extendedData = null)
- {
- try
- {
- _scanType = scanType;
- _width = width;
- _depth = depth;
- if (_width == 0 || _depth == 0)
- {
- throw new Exception("unexpected image size( " + _width + " * " + _depth + " ).");
- }
- if (vinnoImageDataBuffers != null)
- {
- _imageCount = vinnoImageDataBuffers.Count;
- }
- if (_imageCount < 3)
- {
- throw new Exception("too few ( " + _imageCount + " ) images for a volumeData.");
- }
- _logicalCoordRegion = logicalCoordRegion;
- _physicalWidth = physicalWidth;
- _physicalDepth = physicalDepth;
- _physicalLength = physicalLength;
- if (extendedData == null)
- {
- _extendedData = new byte[0];
- }
- else
- {
- _extendedData = extendedData;
- }
- // 生成扫查位置
- _transducerPoses = GenTransducerPosesForStraightVolumeDatas(logicalCoordRegion, physicalLength, _imageCount);
- _spacingXY = _logicalCoordRegion.Width / _width;
- // 判断是否为均一立方体
- _isCropped = IsCropped(logicalCoordRegion, physicalWidth, physicalDepth);
- _isParallel = IsParallelAndEquallySpaced(_transducerPoses);
- _isInterpolated = IsInterpolated(_transducerPoses, _spacingXY);
- _isUniformCube = _isCropped && _isParallel && _isInterpolated;
- // 将Vinno图像从Jpg压缩后的byte[]解析成不压缩的纯byte数组
- lock (_dataLocker)
- {
- DecodeVinnoImageDataBuffers(vinnoImageDataBuffers, width, depth, _bytesPerPixel, ref _dataBuffer);
- }
- // 由于在DecodeVinnoImageDataBuffers中会固定将图像读成Gray8的,因此这里的_colorType总是Gray8
- _colorType = EnumColorType.Gray8;
- _bytesPerPixel = RawImage.GetBytesPerPixel(_colorType);
- return true;
- }
- catch (Exception excep)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.ErrorLog, "Failed at ReadFromVinnoImageDatas:" + excep.Message));
- NotifyError?.Invoke(this, new ErrorEventArgs(excep));
- return false;
- }
- }
- /// <summary>
- /// 直接通过一系列VinnoImage的DataBuffer创建(任意扫查,需要给出每张图片采集时的探头坐标)
- /// 注意:VinnoImage里的DataBuffer是未解码的
- /// </summary>
- /// <param name="vinnoImageDataBuffers"></param>
- /// <param name="width"></param>
- /// <param name="depth"></param>
- /// <param name="logicalCoordRegion"></param>
- /// <param name="physicalWidth"></param>
- /// <param name="physicalDepth"></param>
- /// <param name="scanPositions"></param>
- /// <param name="scanType"></param>
- /// <param name="extendedData"></param>
- /// <param name="colorType"></param>
- public bool ReadFromVinnoImageDatas(List<byte[]> vinnoImageDataBuffers, int width, int depth, RectF logicalCoordRegion,
- float physicalWidth, float physicalDepth, Point3DF[] scanPositions, EnumVolumeDataScanType scanType = EnumVolumeDataScanType.NotSpecified,
- byte[] extendedData = null)
- {
- try
- {
- return true;
- }
- catch (Exception excep)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.ErrorLog, "Failed at ReadFromVinnoImageDatas:" + excep.Message));
- NotifyError?.Invoke(this, new ErrorEventArgs(excep));
- return false;
- }
- }
- /// <summary>
- /// 直接通过一系列RawImage图像创建(直线扫查,只给physicalLength即可)
- /// 注意:RawImage里的DataBuffer是解码后的,每个byte由一个一个像素顺序排列而来
- /// </summary>
- /// <param name="rawImages"></param>
- /// <param name="logicalCoordRegion"></param>
- /// <param name="physicalWidth"></param>
- /// <param name="physicalDepth"></param>
- /// <param name="physicalLength"></param>
- /// <param name="scanType"></param>
- /// <param name="extendedData"></param>
- public bool ReadFromRawImages(List<RawImage> rawImages, RectF logicalCoordRegion,
- float physicalWidth, float physicalDepth, float physicalLength,EnumVolumeDataScanType scanType = EnumVolumeDataScanType.NotSpecified,
- byte[] extendedData = null)
- {
- try
- {
- _scanType = scanType;
- if (rawImages != null)
- {
- _imageCount = rawImages.Count;
- }
- if (_imageCount < 3)
- {
- throw new Exception("too few ( " + _imageCount + " ) images for a volumeData.");
- }
- _width = rawImages[0].Width;
- _depth = rawImages[0].Height;
- if (_width == 0 || _depth == 0)
- {
- throw new Exception("unexpected image size( " + _width + " * " + _depth + " ).");
- }
- _logicalCoordRegion = logicalCoordRegion;
- _physicalWidth = physicalWidth;
- _physicalDepth = physicalDepth;
- _physicalLength = physicalLength;
- if (extendedData == null)
- {
- _extendedData = new byte[0];
- }
- else
- {
- _extendedData = extendedData;
- }
- // 生成扫查位置
- _transducerPoses = GenTransducerPosesForStraightVolumeDatas(logicalCoordRegion, physicalLength, _imageCount);
- _spacingXY = _logicalCoordRegion.Width / _width;
- // 判断是否为均一立方体
- _isCropped = IsCropped(logicalCoordRegion, physicalWidth, physicalDepth);
- _isParallel = IsParallelAndEquallySpaced(_transducerPoses);
- _isInterpolated = IsInterpolated(_transducerPoses, _spacingXY);
- _isUniformCube = _isCropped && _isParallel && _isInterpolated;
- // 将Vinno图像从Jpg压缩后的byte[]解析成不压缩的纯byte数组
- lock (_dataLocker)
- {
- int imgDataByteCounts = _width * _depth;
- Array.Resize(ref _dataBuffer, _imageCount * imgDataByteCounts);
- for (int ni = 0; ni < _imageCount; ni++)
- {
- var image = rawImages[ni].Clone(EnumColorType.Gray8);
- Array.Copy(image.DataBuffer, 0, _dataBuffer, ni * imgDataByteCounts, imgDataByteCounts);
- }
- }
- // 由于在DecodeVinnoImageDataBuffers中会固定将图像读成Gray8的,因此这里的_colorType总是Gray8
- _colorType = EnumColorType.Gray8;
- _bytesPerPixel = RawImage.GetBytesPerPixel(_colorType);
- return true;
- }
- catch (Exception excep)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.ErrorLog, "Failed at ReadFromRawImages:" + excep.Message));
- NotifyError?.Invoke(this, new ErrorEventArgs(excep));
- return false;
- }
- }
- /// <summary>
- /// 直接通过一系列RawImage图像创建(任意扫查,需要给出每张图片采集时的探头坐标)
- /// 注意:RawImage里的DataBuffer是解码后的,每个byte由一个一个像素顺序排列而来
- /// </summary>
- /// <param name="rawImages"></param>
- /// <param name="logicalCoordRegion"></param>
- /// <param name="physicalWidth"></param>
- /// <param name="physicalDepth"></param>
- /// <param name="scanPositions"></param>
- /// <param name="scanType"></param>
- /// <param name="extendedData"></param>
- public bool ReadFromRawImages(List<RawImage> rawImages, RectF logicalCoordRegion,
- float physicalWidth, float physicalDepth, Point3DF[] scanPositions,byte[] extendedData = null)
- {
- try
- {
- return true;
- }
- catch (Exception excep)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.ErrorLog, "Failed at ReadFromRawImages:" + excep.Message));
- NotifyError?.Invoke(this, new ErrorEventArgs(excep));
- return false;
- }
- }
- /// <summary>
- /// 从.model和.surface文件里重建出体数据
- /// </summary>
- /// <param name="modelFolder"></param>
- public bool ReadFromFile(string modelPath, string surfacePath)
- {
- try
- {
- // 读出surface里的关键信息
- if (!File.Exists(surfacePath))
- {
- throw new ArgumentException("Failed to load surface file:" + surfacePath);
- }
- using (var stream = new FileStream(surfacePath, FileMode.Open))
- {
- var reader = new AI.Common.Tools.AIStreamReader(stream);
- // 读入模型的长宽高
- _width = reader.ReadInt();
- _depth = reader.ReadInt();
- _imageCount = reader.ReadInt();
- // 读入extendedData
- var length = reader.ReadInt();
- _extendedData = reader.ReadBytes(length);
- // 读入图像张数
- var surfaceCount = reader.ReadInt();
- // 分别读入图像
- for (int ni = 0; ni < surfaceCount; ni++)
- {
- int dataSize = reader.ReadInt();
- var imageDataEncoded = reader.ReadBytes(dataSize);
- // ToDo 读进来的surface数据拿来干嘛?是否有必要单独存储?
- }
- // 读入bool值以便确认是否为RawVolumeData写入到surface的
- bool isRawVolumeVersion = reader.ReadBool();
- if (isRawVolumeVersion)
- {
- // 读入_logicalCoordRegion信息
- float rectLeft = reader.ReadFloat();
- float rectTop = reader.ReadFloat();
- float rectWidth = reader.ReadFloat();
- float rectHeight = reader.ReadFloat();
- _logicalCoordRegion = new RectF(rectLeft, rectTop, rectWidth, rectHeight);
- // 读入_physicalWidth、_physicalDepth、_physicalLength
- _physicalWidth = reader.ReadFloat();
- _physicalDepth = reader.ReadFloat();
- _physicalLength = reader.ReadFloat();
- // 读入_scanType
- var scanType = reader.ReadInt();
- _scanType = (EnumVolumeDataScanType)scanType;
- }
- else
- {
- // 按照旧版本颈动脉3D里定义的格式读入相关信息
- using (var sm = new MemoryStream(_extendedData))
- {
- sm.Position = 0;
- var rd = new AIStreamReader(sm);
- var carotidType = rd.ReadByte();
- if (carotidType == 0)
- {
- _scanType = EnumVolumeDataScanType.CarotidLeftStraightScan;
- }
- if (carotidType == 1)
- {
- _scanType = EnumVolumeDataScanType.CarotidRightStraightScan;
- }
- var probe = rd.ReadBytes();
- // 注:从probe中可以解析出探头名,探头类型,应用名,帧率
- // 因为貌似不需要用,所以未做进一步解析
- var visualCount = rd.ReadByte();
- for (int ni = 0; ni < visualCount; ni++)
- {
- var visual = rd.ReadBytes();
- // 从visual中解析出扫查的物理坐标信息
- using (var smVisual = new MemoryStream(visual))
- {
- smVisual.Position = 0;
- var rdVisual = new AIStreamReader(smVisual);
- var visualType = rdVisual.ReadByte();
- if (visualType == 0)
- {
- // visualType = 0 表示VinnoVisualType.V2D,为1表示VinnoVisualType.V3D
- var displayMode = rdVisual.ReadByte();
- var indicator = rdVisual.ReadByte();
- var activeModeType = rdVisual.ReadByte();
- var modeCount = rdVisual.ReadByte();
- for (int nj = 0; nj < modeCount; nj++)
- {
- var mode = rdVisual.ReadBytes();
- // 注:从mode中可以解析出显示的是B模式还是多普勒等信息
- // 因为貌似不需要用,所以未做进一步的解析
- }
- var physicalCoordCount = rdVisual.ReadByte();
- for (int nj = 0; nj < physicalCoordCount; nj++)
- {
- var visualAreaType = rdVisual.ReadByte();
- var physicalCoord = rdVisual.ReadBytes();
- if (visualAreaType == 0)
- {
- // visualAreaType=0 表示为Tissue
- using (var smPhysical = new MemoryStream(physicalCoord))
- {
- smPhysical.Position = 0;
- var rdPhysical = new AIStreamReader(smPhysical);
- var type = rdPhysical.ReadByte();
- if (type == 3)
- {
- // type=3 表示为LinearTissue
- var depthStart = rdPhysical.ReadDouble();
- var depthEnd = rdPhysical.ReadDouble();
- var width = rdPhysical.ReadDouble();
- var beamPosition = rdPhysical.ReadDouble();
- var steer = rdPhysical.ReadDouble();
- _physicalDepth = (float)(depthEnd - depthStart);
- _physicalWidth = (float)(width);
- }
- }
- }
- }
- var logicalCoordCount = rdVisual.ReadByte();
- for (int nj = 0; nj < logicalCoordCount; nj++)
- {
- var visualAreaType = rdVisual.ReadByte();
- var logicalCoord = rdVisual.ReadBytes();
- if (visualAreaType == 0)
- {
- // visualAreaType=0 表示为Tissue
- using (var smLogical = new MemoryStream(logicalCoord))
- {
- smLogical.Position = 0;
- var rdLogical = new AIStreamReader(smLogical);
- var isFlipHorizontal = rdLogical.ReadBool();
- var isFlipVertical = rdLogical.ReadBool();
- var xUnit = rdLogical.ReadByte();
- var yUnit = rdLogical.ReadByte();
- var left = rdLogical.ReadDouble();
- var top = rdLogical.ReadDouble();
- var right = rdLogical.ReadDouble();
- var bottom = rdLogical.ReadDouble();
- _logicalCoordRegion = new RectF((float)left, (float)top, (float)(right - left), (float)(bottom - top));
- }
- }
- }
- }
- }
- }
- }
- // 从_extendedData中无法读出扫查的距离,默认为7cm
- _physicalLength = 7;
- }
- }
- // 生成扫查信息
- _transducerPoses = GenTransducerPosesForStraightVolumeDatas(_logicalCoordRegion, _physicalLength, _imageCount);
- _spacingXY = _logicalCoordRegion.Width / _width;
- // 判断是否为均一立方体
- _isCropped = IsCropped(_logicalCoordRegion, _physicalWidth, _physicalDepth);
- _isParallel = IsParallelAndEquallySpaced(_transducerPoses);
- _isInterpolated = IsInterpolated(_transducerPoses, _spacingXY);
- _isUniformCube = _isCropped && _isParallel && _isInterpolated;
- // 读出model里的关键信息
- if (!File.Exists(modelPath))
- {
- throw new ArgumentException("Failed to load model file:" + modelPath);
- }
- using (var stream = new FileStream(modelPath, FileMode.Open))
- {
- var reader = new AI.Common.Tools.AIStreamReader(stream);
- int imageCount = reader.ReadInt();
- int imgDataByteCounts = _width * _depth * _bytesPerPixel;
- byte[] decodedDataBuffer = new byte[imgDataByteCounts];
- Array.Resize(ref _dataBuffer, imgDataByteCounts * ImageCount);
- for (int ni = 0; ni < imageCount; ni++)
- {
- int encodedDataSize = reader.ReadInt();
- var encodedDataBuffer = reader.ReadBytes(encodedDataSize);
- if (!AIReconstructor.ImDataDecode(encodedDataBuffer, encodedDataBuffer.Length,
- decodedDataBuffer, decodedDataBuffer.Length))
- {
- int errorMaxLen = 256;
- StringBuilder errorMsg = new StringBuilder(errorMaxLen);
- AIReconstructor.EnumCppCoreErrorCode errorCode = AIReconstructor.EnumCppCoreErrorCode.None;
- AIReconstructor.GetErrorCodeAndMsg(ref errorCode, errorMsg, errorMaxLen);
- throw new Exception("Failed at decoding model image datas, error code: "
- + errorCode.ToString() + " , details: " + errorMsg.ToString() + " .");
- }
- Array.Copy(decodedDataBuffer, 0, _dataBuffer, ni * imgDataByteCounts, imgDataByteCounts);
- }
- }
- return true;
- }
- catch (Exception excep)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.ErrorLog, "Failed at ReadFromFile:" + excep.Message));
- NotifyError?.Invoke(this, new ErrorEventArgs(excep));
- return false;
- }
- }
- /// <summary>
- /// 将体数据保存成.model 和 .surface
- /// </summary>
- /// <param name="modelFolder"></param>
- public bool SaveToFile(string modelFolder)
- {
- if (!_isUniformCube)
- {
- if (!TurnToDesiredUniformCube(_spacingXY))
- {
- return false;
- }
- }
- // 保存模型(内部会再存一份压缩后的模型)
- if (!SaveModel(modelFolder))
- {
- return false;
- }
- // 保存模型表面数据
- if (!SaveSurface(modelFolder))
- {
- return false;
- }
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.InfoLog, "RawVolumeData saved into " +modelFolder+"."));
- return true;
- }
- /// <summary>
- /// 将原始数据裁切,插值,resize成一个均一立方体
- /// </summary>
- /// <returns></returns>
- public bool TurnToDesiredUniformCube()
- {
- return TurnToDesiredUniformCube(_spacingXY);
- }
- /// <summary>
- /// 将原始数据裁切,插值,resize成一个均一立方体
- /// </summary>
- /// <param name="expectedSpacing"></param>
- public bool TurnToDesiredUniformCube(float expectedSpacing)
- {
- try
- {
- if (_isUniformCube)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.InfoLog,
- "the volume data is uniform cube already."));
- return true;
- }
- // 希望的spacing不能太小,太小网格太密集,计算量大
- if (MathHelper.AlmostEqual(expectedSpacing, 0))
- {
- throw new ArgumentException("expected spacing is too small.");
- }
- // 计算每幅图上的裁切区域(即,超声成像区域)
- Rect cropRect = new Rect(0, 0, _width, _depth);
- if (!_isCropped)
- {
- // 目前碰到的超声图像都是_physicalDepth等于_logicalCoordRegion.Height的
- // (因为一般情况下,认为探头靠紧皮肤,深度方向起始位置即是0)
- // 即:认为深度方向上不用裁切,如果不等于,需要抛出异常,具体去看这种图像应该怎么裁
- if (!MathHelper.AlmostEqual(_physicalDepth, _logicalCoordRegion.Height))
- {
- throw new ArgumentException("_physicalDepth and _logicalCoordRegion.Height unequal.");
- }
- // 计算需要裁切多大范围的图像
- int cropWidth = (int)(_physicalWidth / _spacingXY);
- int cropHeight = (int)(_physicalDepth / _spacingXY);
- // 要裁切的范围不能超过原图尺寸
- if (cropWidth > _width)
- {
- cropWidth = _width;
- }
- if (cropHeight > _depth)
- {
- cropHeight = _depth;
- }
- // 一般认为图像是左右居中,上下靠上摆放的
- int cropLeft = (_width - cropWidth) / 2;
- int cropTop = 0;
- cropRect = new Rect(cropLeft, cropTop, cropWidth, cropHeight);
- }
- // 根据_spacingXY 和 expectedSpacing 计算xy需要的尺寸
- float ratioXY = _spacingXY / expectedSpacing;
- int desiredWidth = (int)(cropRect.Width * ratioXY);
- int desiredHeight = (int)(cropRect.Height * ratioXY);
- if (_isParallel)
- {
- int desiredImgCount = (int)(_physicalLength / expectedSpacing);
- IntPtr dataPointer = Marshal.UnsafeAddrOfPinnedArrayElement(_dataBuffer, 0);
- AIReconstructor.StructVolumeDataPreProcessorInfo dataInfo = new AIReconstructor.StructVolumeDataPreProcessorInfo(_width, _depth, _imageCount,
- _colorType, cropRect,desiredWidth, desiredHeight, desiredImgCount, dataPointer);
- byte[] uniformCubeBuffer = new byte[desiredWidth * desiredHeight * desiredImgCount * _bytesPerPixel];
- if (!AIReconstructor.StraightScanDataToUniformCube(dataInfo,uniformCubeBuffer))
- {
- int errorMaxLen = 256;
- StringBuilder errorMsg = new StringBuilder(errorMaxLen);
- AIReconstructor.EnumCppCoreErrorCode errorCode = AIReconstructor.EnumCppCoreErrorCode.None;
- AIReconstructor.GetErrorCodeAndMsg(ref errorCode, errorMsg, errorMaxLen);
- throw new Exception("Failed at StraightScanDataToUniformCube, error code: "
- + errorCode.ToString() + " , details: " + errorMsg.ToString() + " .");
- }
- // 将得到的均一立方体替换当前裸数据的信息
- lock (_dataLocker)
- {
- _dataBuffer = uniformCubeBuffer;
- }
- _width = desiredWidth;
- _depth = desiredHeight;
- _imageCount = desiredImgCount;
- _logicalCoordRegion = new RectF(0, 0, _physicalWidth, _physicalDepth);
- _spacingXY = expectedSpacing;
- _transducerPoses = GenTransducerPosesForStraightVolumeDatas(_logicalCoordRegion, _physicalLength, _imageCount);
- _isCropped = true;
- _isParallel = true;
- _isInterpolated = true;
- _isUniformCube = true;
- }
- else
- {
- // ToDO 不平行的怎么计算,还没有实现
- }
- return true;
- }
- catch (Exception excep)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.ErrorLog, "Failed at TurnToDesiredUniformCube:"+excep.Message));
- NotifyError?.Invoke(this, new ErrorEventArgs(excep));
- return false;
- }
- }
- /// <summary>
- /// 根据融合结果更新相关信息
- /// </summary>
- /// <param name="fusedResult"></param>
- /// <returns></returns>
- internal bool ReadFromFusedResult(AIReconstructor.StructUniformVolumeDataInfo fusedResult)
- {
- _colorType = fusedResult.ColorType;
- _bytesPerPixel = RawImage.GetBytesPerPixel(_colorType);
- _width = fusedResult.X;
- _depth = fusedResult.Y;
- _imageCount = fusedResult.Z;
- _spacingXY = fusedResult.Spacing;
- _physicalWidth = _spacingXY * _width;
- _physicalDepth = _spacingXY * _depth;
- _physicalLength = _spacingXY * _imageCount;
- _logicalCoordRegion = new RectF(0, 0, _physicalWidth, _physicalDepth);
- _scanType = EnumVolumeDataScanType.ComputerReconstructed;
- _transducerPoses = GenTransducerPosesForStraightVolumeDatas(_logicalCoordRegion, _physicalLength, _imageCount);
- _isCropped = true;
- _isParallel = true;
- _isInterpolated = true;
- _isUniformCube = true;
- // ToDo 融合后得到的数据,暂未给extendedData赋值
- _extendedData = new byte[0];
- // 复制所需长度的数据
- int byteCounts =_width * _depth * _imageCount * _bytesPerPixel;
- Array.Resize(ref _dataBuffer, byteCounts);
- Marshal.Copy(fusedResult.DataPointer, _dataBuffer, 0, byteCounts);
- return true;
- }
- #endregion
- #region private
- /// <summary>
- /// 判断当前体数据是否已经裁切
- /// </summary>
- /// <param name="logicalCoordRegion"></param>
- /// <param name="physicalWidth"></param>
- /// <param name="physicalDepth"></param>
- /// <returns></returns>
- private bool IsCropped(RectF logicalCoordRegion, float physicalWidth, float physicalDepth)
- {
- // 判断是否已裁切
- return MathHelper.AlmostEqual(logicalCoordRegion.Width, physicalWidth) &&
- MathHelper.AlmostEqual(logicalCoordRegion.Height, physicalDepth);
- }
- /// <summary>
- /// 判断当前体数据的每一帧在扫查时是否平行且等间距
- /// </summary>
- /// <param name="transducerPoses"></param>
- /// <param name="spacing"></param>
- /// <returns></returns>
- private bool IsParallelAndEquallySpaced(TransducerImgPlaneToWorkCoordMatrix[] transducerPoses)
- {
- // 图像太少不能计算是否等间距
- int imgCount = transducerPoses.Length;
- if (imgCount < 2)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.WarnLog,
- "Need at least 2 images for the calculation of the spacing of transducer image planes."));
- return false;
- }
- var startPose = transducerPoses[0];
- var endPose = transducerPoses[imgCount - 1];
- var origin = new Point3DF(0, 0, 0);
- var startOrigin = startPose.Transform(origin);
- var endOrigin = endPose.Transform(origin);
- var spacingZ = Point3DF.Distance(startOrigin, endOrigin) / (imgCount - 1);
- // 一个个pose判断Z方向是否平行且等间距
- for (int ni = 1; ni < imgCount - 1; ni++)
- {
- var curPose = transducerPoses[ni];
- if (!curPose.ImagePlaneParallels(startPose, endPose))
- {
- return false;
- }
- var prePose = transducerPoses[ni - 1];
- var preOrigin = prePose.Transform(origin);
- var curOrigin = curPose.Transform(origin);
- var distance = Point3DF.Distance(curOrigin, preOrigin);
- if (!MathHelper.AlmostEqual(spacingZ, distance))
- {
- return false;
- }
- }
- return true;
- }
- /// <summary>
- /// 判断当前体数据是否已插值(xy方向的间距是否和z方向一致)
- /// </summary>
- /// <param name="transducerPoses"></param>
- /// <param name="spacingXY"></param>
- /// <returns></returns>
- private bool IsInterpolated(TransducerImgPlaneToWorkCoordMatrix[] transducerPoses,float spacingXY)
- {
- // 图像太少不能计算是否等间距
- int imgCount = transducerPoses.Length;
- if (imgCount < 2)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.WarnLog,
- "Need at least 2 images for the calculation of the spacing of transducer image planes."));
- return false;
- }
- // 判断Z方向的间距和XY方向是否相同
- var startPose = transducerPoses[0];
- var endPose = transducerPoses[imgCount - 1];
- var origin = new Point3DF(0, 0, 0);
- var startOrigin = startPose.Transform(origin);
- var endOrigin = endPose.Transform(origin);
- var spacingZ = Point3DF.Distance(startOrigin, endOrigin) / (imgCount -1);
- if (!MathHelper.AlmostEqual(spacingXY, spacingZ))
- {
- return false;
- }
- return true;
- }
- /// <summary>
- /// 体数据在一条直线上,认为没有旋转,只有平移,xy方向不动,z方向等间距生成平移量即可
- /// </summary>
- /// <param name="scanType"></param>
- /// <param name="logicalCoordRegion"></param>
- /// <param name="physicalLength"></param>
- /// <param name="imgCount"></param>
- /// <returns></returns>
- private TransducerImgPlaneToWorkCoordMatrix[] GenTransducerPosesForStraightVolumeDatas(RectF logicalCoordRegion, float physicalLength, int imgCount)
- {
- // 假定每张图扫查的时候没有旋转只有平移,且xy方向未发生移动,只有z方向平移
- // z可以根据physicalLength和imgcount等间距生成
- // x 取 logicalCoordinateRegion里的(Left+Right)/2 (如果是右甲状腺,则加上一个偏移量)
- // y 取 logicalCoordinateRegion里的Top
- float x = (logicalCoordRegion.Left + logicalCoordRegion.Right) / 2;
- float y = logicalCoordRegion.Top;
- var poses = new TransducerImgPlaneToWorkCoordMatrix[imgCount];
- if (imgCount > 0)
- {
- for (int ni = 0; ni < imgCount; ni++)
- {
- poses[ni] = new TransducerImgPlaneToWorkCoordMatrix(x, y, ni * physicalLength / (imgCount - 1));
- }
- }
- return poses;
- }
- /// <summary>
- /// 将vinnoImageDataBuffers里的JPG图像转存的byte数据解码成未压缩的byte数组
- /// </summary>
- /// <param name="vinnoImageDataBuffers"></param>
- /// <param name="width"></param>
- /// <param name="depth"></param>
- /// <param name="colorType"></param>
- /// <returns></returns>
- private void DecodeVinnoImageDataBuffers(List<byte[]> vinnoImageDataBuffers, int width, int depth, int bytesPerPixel, ref byte[] decodedDataBuffer)
- {
- var imgCount = vinnoImageDataBuffers.Count;
- var imgDataByteCounts = width * depth * bytesPerPixel;
- var decodeDataByteCounts = imgDataByteCounts * imgCount;
- if (decodedDataBuffer.Length < decodeDataByteCounts)
- {
- Array.Resize(ref decodedDataBuffer, decodeDataByteCounts);
- }
- byte[] oneDecodedDataBuffer = new byte[imgDataByteCounts];
- for (int ni = 0; ni < imgCount; ni++)
- {
- var encodedData = vinnoImageDataBuffers[ni];
- if (!AIReconstructor.ImDataDecode(encodedData, encodedData.Length, oneDecodedDataBuffer,
- imgDataByteCounts, AIReconstructor.EnumImreadMode.Grayscale))
- {
- int errorMaxLen = 256;
- StringBuilder errorMsg = new StringBuilder(errorMaxLen);
- AIReconstructor.EnumCppCoreErrorCode errorCode = AIReconstructor.EnumCppCoreErrorCode.None;
- AIReconstructor.GetErrorCodeAndMsg(ref errorCode, errorMsg, errorMaxLen);
- throw new Exception("Failed at decoding vinno image data buffers, error code: "
- + errorCode.ToString() + " , details: " + errorMsg.ToString() + " .");
- }
- Array.Copy(oneDecodedDataBuffer, 0, decodedDataBuffer, ni * imgDataByteCounts, imgDataByteCounts);
- }
- }
- /// <summary>
- /// 将模型存到指定路径下
- /// </summary>
- /// <param name="modelFolder"></param>
- /// <returns></returns>
- private bool SaveModel(string modelFolder)
- {
- try
- {
- if (!_isUniformCube)
- {
- throw new Exception("the volume data should be turned to uniform cube before save");
- }
- if (!Directory.Exists(modelFolder))
- {
- Directory.CreateDirectory(modelFolder);
- }
- // 保存模型(逐帧将图像数据压缩成jpg)
- string modelFilePath = modelFolder + "\\" + ModelFileSuffix;
- int imgDataByteCounts = _width * _depth * _bytesPerPixel;
- // 如果不需要保存压缩模型,不需要保存stackedEncodedDataBuffer
- byte[] stackedEncodedDataBuffer = new byte[imgDataByteCounts * _imageCount];
- int stackedDataSize = 0;
- using (var stream = File.OpenWrite(modelFilePath))
- {
- stream.Write(BitConverter.GetBytes(_imageCount), 0, sizeof(int));
- byte[] oneDecodedDataBuffer = new byte[imgDataByteCounts];
- byte[] oneEncodedDataBuffer = new byte[imgDataByteCounts];
- AIReconstructor.StructImwriteParam[] imwriteParams = new AIReconstructor.StructImwriteParam[1];
- imwriteParams[0] = new AIReconstructor.StructImwriteParam(AIReconstructor.EnumImwriteFlags.JpegQuality, 80);
- var decodedPointer = Marshal.UnsafeAddrOfPinnedArrayElement(oneDecodedDataBuffer, 0);
- AIReconstructor.StructImageInfo imageInfo = new AIReconstructor.StructImageInfo(_width, _depth, _colorType, decodedPointer);
- for (int ni = 0; ni < _imageCount; ni++)
- {
- int encodedDataSize = imgDataByteCounts;
- Array.Copy(_dataBuffer, ni * imgDataByteCounts, oneDecodedDataBuffer, 0, imgDataByteCounts);
- if (!AIReconstructor.ImDataEncode(imageInfo, AIReconstructor.EnumImwriteExtension.Jpg,
- imwriteParams, 1, oneEncodedDataBuffer, ref encodedDataSize))
- {
- int errorMaxLen = 256;
- StringBuilder errorMsg = new StringBuilder(errorMaxLen);
- AIReconstructor.EnumCppCoreErrorCode errorCode = AIReconstructor.EnumCppCoreErrorCode.None;
- AIReconstructor.GetErrorCodeAndMsg(ref errorCode, errorMsg, errorMaxLen);
- throw new Exception("Failed at encoding model image datas, error code: "
- + errorCode.ToString() + " , details: " + errorMsg.ToString() + " .");
- }
- stream.Write(BitConverter.GetBytes(encodedDataSize), 0, sizeof(int));
- stream.Write(oneEncodedDataBuffer, 0, encodedDataSize);
- Array.Copy(oneEncodedDataBuffer, 0, stackedEncodedDataBuffer, stackedDataSize, encodedDataSize);
- stackedDataSize += encodedDataSize;
- }
- }
- // 保存压缩后的数据
- string zipFilePath = modelFolder + "\\" + ZipFileSuffix;
- using (var stream = File.OpenWrite(zipFilePath))
- {
- // 写入压缩版本号
- var zipVersion = Encoding.Unicode.GetBytes(MdlZipFileVersion);
- stream.Write(BitConverter.GetBytes(zipVersion.Length), 0, sizeof(int));
- stream.Write(zipVersion, 0, zipVersion.Length);
- // 借用GZipStream完成压缩
- using (var memoryStream = new MemoryStream())
- {
- using (var compressionStream = new GZipStream(memoryStream, CompressionMode.Compress))
- {
- compressionStream.Write(stackedEncodedDataBuffer, 0, stackedDataSize);
- compressionStream.Close();
- }
- var compressedBytes = memoryStream.ToArray();
- // 写入压缩后的数据
- stream.Write(BitConverter.GetBytes(compressedBytes.Length), 0, sizeof(int));
- stream.Write(compressedBytes, 0, compressedBytes.Length);
- }
- }
- return true;
- }
- catch (Exception excep)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.ErrorLog, "Failed at SaveModel:" + excep.Message));
- NotifyError?.Invoke(this, new ErrorEventArgs(excep));
- return false;
- }
- }
- /// <summary>
- /// 将模型表面数据保存到指定路径下
- /// </summary>
- /// <param name="modelFolder"></param>
- /// <returns></returns>
- private bool SaveSurface(string modelFolder)
- {
- try
- {
- if (!_isUniformCube)
- {
- throw new Exception("the volume data should be turned to uniform cube before save");
- }
- if (!Directory.Exists(modelFolder))
- {
- Directory.CreateDirectory(modelFolder);
- }
- string surfaceFilePath = modelFolder + "\\" + SurfaceFileSuffix;
- using (var stream = File.OpenWrite(surfaceFilePath))
- {
- // 模型的宽高长
- stream.Write(BitConverter.GetBytes(_width), 0, sizeof(int));
- stream.Write(BitConverter.GetBytes(_depth), 0, sizeof(int));
- stream.Write(BitConverter.GetBytes(_imageCount), 0, sizeof(int));
- // 写入extendedData
- stream.Write(BitConverter.GetBytes(_extendedData.Length),0 ,sizeof(int));
- stream.Write(_extendedData, 0, _extendedData.Length);
- // 写入图像张数
- int surfaceNum = 6;
- stream.Write(BitConverter.GetBytes(surfaceNum), 0, sizeof(int));
- // 得到六个表面
- var volumeDataPointer = Marshal.UnsafeAddrOfPinnedArrayElement(_dataBuffer, 0);
- AIReconstructor.StructUniformVolumeDataInfo volumeDataInfo = new AIReconstructor.StructUniformVolumeDataInfo(_width, _depth,
- _imageCount, _spacingXY, _colorType, volumeDataPointer);
- AIReconstructor.StructSurfacePicInfo[] surfaceInfos = new AIReconstructor.StructSurfacePicInfo[6];
- // 写入时图像顺序参照之前颈动脉三维重建的代码中的写入顺序
- // 右
- var rightSurfaceBuffer = new byte[_depth * _imageCount * _bytesPerPixel];
- var rightSurfacePointer = Marshal.UnsafeAddrOfPinnedArrayElement(rightSurfaceBuffer, 0);
- surfaceInfos[0] = new AIReconstructor.StructSurfacePicInfo(AIReconstructor.EnumSurfacePicType.Right,
- new AIReconstructor.StructImageInfo(_depth, _imageCount, _colorType, rightSurfacePointer));
- // 左
- var leftSurfaceBuffer = new byte[_depth * _imageCount * _bytesPerPixel];
- var leftSurfacePointer = Marshal.UnsafeAddrOfPinnedArrayElement(leftSurfaceBuffer, 0);
- surfaceInfos[1] = new AIReconstructor.StructSurfacePicInfo(AIReconstructor.EnumSurfacePicType.Left,
- new AIReconstructor.StructImageInfo(_depth, _imageCount, _colorType,leftSurfacePointer));
- // 后
- var behindSurfaceBuffer = new byte[_width * _depth * _bytesPerPixel];
- var behindSurfacePointer = Marshal.UnsafeAddrOfPinnedArrayElement(behindSurfaceBuffer, 0);
- surfaceInfos[2] = new AIReconstructor.StructSurfacePicInfo(AIReconstructor.EnumSurfacePicType.Behind,
- new AIReconstructor.StructImageInfo(_width, _depth, _colorType, behindSurfacePointer));
- // 前
- var frontSurfaceBuffer = new byte[_width * _depth * _bytesPerPixel];
- var frontSurfacePointer = Marshal.UnsafeAddrOfPinnedArrayElement(frontSurfaceBuffer, 0);
- surfaceInfos[3] = new AIReconstructor.StructSurfacePicInfo(AIReconstructor.EnumSurfacePicType.Front,
- new AIReconstructor.StructImageInfo(_width, _depth, _colorType, frontSurfacePointer));
- // 上
- var topSurfaceBuffer = new byte[_width * _imageCount * _bytesPerPixel];
- var topSurfacePointer = Marshal.UnsafeAddrOfPinnedArrayElement(topSurfaceBuffer, 0);
- surfaceInfos[4] = new AIReconstructor.StructSurfacePicInfo(AIReconstructor.EnumSurfacePicType.Top,
- new AIReconstructor.StructImageInfo(_width, _imageCount, _colorType, topSurfacePointer));
- // 下
- var bottomSurfaceBuffer = new byte[_width * _imageCount * _bytesPerPixel];
- var bottomSurfacePointer = Marshal.UnsafeAddrOfPinnedArrayElement(bottomSurfaceBuffer, 0);
- surfaceInfos[5] = new AIReconstructor.StructSurfacePicInfo(AIReconstructor.EnumSurfacePicType.Bottom,
- new AIReconstructor.StructImageInfo(_width, _imageCount, _colorType, bottomSurfacePointer));
- if (!AIReconstructor.GetSurfacePicsFromUniformCube(volumeDataInfo, surfaceNum, surfaceInfos))
- {
- int errorMaxLen = 256;
- StringBuilder errorMsg = new StringBuilder(errorMaxLen);
- AIReconstructor.EnumCppCoreErrorCode errorCode = AIReconstructor.EnumCppCoreErrorCode.None;
- AIReconstructor.GetErrorCodeAndMsg(ref errorCode, errorMsg, errorMaxLen);
- throw new Exception("Failed at GetSurfacePicsFromUniformCube, error code: "
- + errorCode.ToString() + " , details: " + errorMsg.ToString() + " .");
- }
- // 分别写入这六张图(数据)
- int index = 0;
- AIReconstructor.StructImwriteParam[] imwriteParams = new AIReconstructor.StructImwriteParam[1];
- imwriteParams[0] = new AIReconstructor.StructImwriteParam(AIReconstructor.EnumImwriteFlags.JpegQuality, 80);
- for (int ni = 0; ni < surfaceNum; ni++)
- {
- var imageInfo = surfaceInfos[ni].ImageInfo;
- int imageByteCount = imageInfo.Width * imageInfo.Height * _bytesPerPixel;
- index += imageByteCount;
- byte[] imageDataEncoded = new byte[imageByteCount];
- int encodedDataSize = imageByteCount;
- if (!AIReconstructor.ImDataEncode(imageInfo, AIReconstructor.EnumImwriteExtension.Jpg,
- imwriteParams, 1, imageDataEncoded, ref encodedDataSize))
- {
- int errorMaxLen = 256;
- StringBuilder errorMsg = new StringBuilder(errorMaxLen);
- AIReconstructor.EnumCppCoreErrorCode errorCode = AIReconstructor.EnumCppCoreErrorCode.None;
- AIReconstructor.GetErrorCodeAndMsg(ref errorCode, errorMsg, errorMaxLen);
- throw new Exception("Failed at encoding surface image datas, error code: "
- + errorCode.ToString() + " , details: " + errorMsg.ToString() + " .");
- }
- stream.Write(BitConverter.GetBytes(encodedDataSize), 0, sizeof(int));
- stream.Write(imageDataEncoded, 0, encodedDataSize);
- }
- // 写入bool值以便确认是通过RawVolumeData写入到surface里的信息
- stream.Write(BitConverter.GetBytes(true), 0, sizeof(bool));
- // 写入_logicalCoordRegion的信息
- stream.Write(BitConverter.GetBytes(_logicalCoordRegion.Left), 0, sizeof(float));
- stream.Write(BitConverter.GetBytes(_logicalCoordRegion.Top), 0, sizeof(float));
- stream.Write(BitConverter.GetBytes(_logicalCoordRegion.Width), 0, sizeof(float));
- stream.Write(BitConverter.GetBytes(_logicalCoordRegion.Height), 0, sizeof(float));
- // 写入_physicalWidth、_physicalDepth、_physicalLength
- stream.Write(BitConverter.GetBytes(_physicalWidth), 0, sizeof(float));
- stream.Write(BitConverter.GetBytes(_physicalDepth), 0, sizeof(float));
- stream.Write(BitConverter.GetBytes(_physicalLength), 0, sizeof(float));
- // 写入_scanType
- stream.Write(BitConverter.GetBytes((int)_scanType), 0, sizeof(int));
- }
- return true;
- }
- catch (Exception excep)
- {
- NotifyLog?.Invoke(this, new LogEventArgs(EnumLogType.ErrorLog, "Failed at SaveSurface:" + excep.Message));
- NotifyError?.Invoke(this, new ErrorEventArgs(excep));
- return false;
- }
- }
- #endregion
- }
- }
|