(12条消息) C#+OpenCvSharp实现图片显示控件(可缩放显示像元)
之前实现过随意缩放的图片查看控件,利用picturebox,通过改变picturebox的Size和Location进行缩放和移动,效果不好,图片放大后没有显示像元(缩放的算法不同),而且放大倍数过大会导致绘图错误且很卡,因此,从而改变思路,重新做一个图片查看器。
最近正在学习OpenCvSharp,于是就利用OpenCvSharp实现一个图片查看器(支持图片随意缩放不卡顿且能显示图片像元、鼠标集中图片某点缩放),看网上关于这一块的资源蛮少的,有的都是跟我之前做的那个差不多,因此把思路和过程放上来,大家一起交流。
以下是效果图:

思路如下:
以控件的原点建立坐标系,根据横纵像元尺寸(PixcelSize)计算实际图片需要显示的大小,MatDisplayRect.Size =(Image.Width*PixelSize.Width,Image.Height*PixelSize.Height),MatDisplayRect.Location控制图片显示的位置。
每次重绘的时候根据MatDisplayRect.Location跟坐标轴原点(0,0)的距离和横纵像元尺寸(PixcelSize),计算出实际需要显示在屏幕中图片区域,截取该区域,根据PixcelSize计算该区域显示的屏幕尺寸并进行缩放,计算绘制起点,最后重绘在控件上。

CvDisplayGraphicsMat 类中包含了绘制Mat图片的操作
/// <summary>/// 需要绘制的Mat对象/// </summary>public class CvDisplayGraphicsMat : CvDisplayGraphicsObject{protected Mat _Image = null;public Mat Image{get{return _Image;}set{if (_Image != null){_Image.Dispose();}if (value != null)_Image = new Mat(value,new Rect(0,0,value.Width,value.Height));Reset();}}public Rect2d DispRect{get{return new Rect2d(DispOrigin, DispSize);}}public Size2d DispSize;public CvDisplayGraphicsMat(){DispSize = new Size2d(0,0);}#region overridepublic override void Reset(){base.Reset();if(Image != null){DispSize = new Size2d(Image.Width, Image.Height);}elseDispSize = new Size2d(0, 0);}public override void Dispose(){if (_Image != null){_Image.Dispose();}base.Dispose();}public override void OnPaint(PaintEventArgs e, Size2d pixelSize){Rect showMatRect = new Rect(); //需要裁减的图片范围System.Drawing.PointF drawImageStartPos = new System.Drawing.PointF(); //绘制showMatRect的起始点if (DispRect.X < 0){//显示区域的起始点X不在屏幕内showMatRect.X = (int)(Math.Abs(DispRect.X) / pixelSize.Width);drawImageStartPos.X = (float)(showMatRect.X * pixelSize.Width + DispRect.X);}else{showMatRect.X = 0;drawImageStartPos.X = (float)DispRect.X;}showMatRect.Width = (int)((e.ClipRectangle.Width - drawImageStartPos.X) / pixelSize.Width) + 1;if (DispRect.Y < 0){//显示区域的起始点Y不在屏幕内showMatRect.Y = (int)(Math.Abs(DispRect.Y) / pixelSize.Height);drawImageStartPos.Y = (float)(showMatRect.Y * pixelSize.Height + DispRect.Y);}else{showMatRect.Y = 0;drawImageStartPos.Y = (float)DispRect.Y;}showMatRect.Height = (int)((e.ClipRectangle.Height - drawImageStartPos.Y) / pixelSize.Height) + 1;AdjustMatRect(Image, ref showMatRect);//调整需要显示Mat区域,以免截取的区域超出图片范围using (Mat displayMat = new Mat(Image, showMatRect)){//计算截取区域需要显示在屏幕中的大小CvSize drawSize = new CvSize((int)(displayMat.Width * pixelSize.Width),(int)(displayMat.Height * pixelSize.Height));if (drawSize.Width < 1) drawSize.Width = 1;if (drawSize.Height < 1) drawSize.Height = 1;Mat resizeMat = new Mat();//以Nearest的方式缩放图片尺寸Cv2.Resize(displayMat, resizeMat, drawSize, 0, 0, InterpolationFlags.Nearest);//缩放完的图片直接画在控件上System.Drawing.Image drawImage = OpenCvSharp.Extensions.BitmapConverter.ToBitmap(resizeMat);e.Graphics.DrawImage(drawImage, drawImageStartPos);}}public override bool IsFocus(PointF pos){return DispRect.Contains(pos.X, pos.Y);}#endregion#region public method/// <summary>/// 根据需要显示的像素大小,重新计算图像显示的尺寸/// </summary>/// <param name="pixelSize"></param>public void ResizeDispRectWithPixcelSize(Size2d pixelSize){if (Image == null)DispSize = new Size2d(0, 0);elseDispSize = new Size2d(Image.Width * pixelSize.Width, Image.Height * pixelSize.Height);}/// <summary>/// 转换屏幕坐标为图片中的像素坐标/// </summary>/// <param name="pos">屏幕坐标</param>/// <param name="pixclSize">单像元尺寸</param>/// <returns></returns>public CvPoint TransformPixelPostion(SdPoint pos,Size2d pixclSize){CvPoint res = new CvPoint(-1, -1);if (IsFocus(pos)){res.X = (int)((pos.X - DispRect.X) / pixclSize.Width);res.Y = (int)((pos.Y - DispRect.Y) / pixclSize.Height);}return res;}#endregion#region protected method/// <summary>/// 调整显示的图片区域,以免截取的mat越界/// </summary>/// <param name="mt"></param>/// <param name="rect"></param>protected void AdjustMatRect(Mat mt, ref Rect rect){//调整XY坐标if (rect.X < 0)rect.X = 0;if (rect.X >= mt.Width)rect.X = mt.Width - 1;if (rect.Y < 0)rect.Y = 0;if (rect.Y >= mt.Height)rect.Y = mt.Height - 1;//调整长宽if (rect.Width + rect.X > mt.Width)rect.Width = mt.Width - rect.X;if (rect.Height + rect.Y > mt.Height)rect.Height = mt.Height - rect.Y;}#endregion}
CvDisplay 类用于绘制所有需要绘图的元素,以及一些缩放、移动等操作
class CvDisplay : PictureBox{#region 内部操作数据protected CvDisplayGraphicsMat _cdgMat; //Mat绘制类protected Size2d _pixcelSize; //一个图片像素需要在绘图中绘制的大小protected bool _isMouseMoving = false; //鼠标是否允许移动protected Point _mouseDownLocation; //鼠标点下的坐标protected System.Drawing.Point _mouseLocation; //鼠标实时位置protected Point _mousePixcelLocation; //鼠标放置位置的像素实际坐标#endregion#region 事件/// <summary>/// 当前像元位置变化/// </summary>public event EventHandler<PosChangedEventArgs> PositionChanged;#endregion#region 公开属性public enum AutoDisplayMode{Original,Fit,Full}/// <summary>/// 绘图元素集合/// </summary>[EditorBrowsable(EditorBrowsableState.Never)]public CvDisplayGraphicsObjectCollection GraphicsObjects{get;protected set;}[EditorBrowsable(EditorBrowsableState.Always)][CategoryAttribute("CvDisplay"), DescriptionAttribute("自动显示图片模式")]public AutoDisplayMode AutoDisplay{get;set;}[EditorBrowsable(EditorBrowsableState.Always)][CategoryAttribute("CvDisplay"), DescriptionAttribute("OpenCv2 Mat图片数据类")]public new Mat Image{get{return _cdgMat.Image;}set{_cdgMat.Image = value;ImageResize();}}[EditorBrowsable(EditorBrowsableState.Never)]public override Image BackgroundImage{get{return base.BackgroundImage;}set{base.BackgroundImage = null;}}#endregionpublic CvDisplay(){_cdgMat = new CvDisplayGraphicsMat();DoubleBuffered = true;AutoDisplay = AutoDisplayMode.Original;this.ContextMenuStrip = new ContextMenuStrip();ContextMenuStrip.Items.Add("Fit image", null, OnFitImageClick);ContextMenuStrip.Items.Add("Original image", null, OnOriginalImageClick);ContextMenuStrip.Items.Add("Full image", null, OnFullImageClick);ContextMenuStrip.Items.Add("Save as", null, OnSaveAsClick);GraphicsObjects = new CvDisplayGraphicsObjectCollection();}#region 事件处理protected virtual void OnFitImageClick(object sender, EventArgs e){Fit();}protected virtual void OnOriginalImageClick(object sender, EventArgs e){OriginalSize();}protected virtual void OnFullImageClick(object sender, EventArgs e){Full();}protected virtual void OnSaveAsClick(object sender, EventArgs e){if (Image == null) return;using (SaveFileDialog ofd = new SaveFileDialog()){ofd.Filter = "Bitmap|*.bmp";if (ofd.ShowDialog() == DialogResult.OK){SaveAs(ofd.FileName);}}}#endregion#region 父类重载protected override void OnMouseDown(MouseEventArgs e){if (e.Button == MouseButtons.Left){this.Cursor = Cursors.SizeAll;_isMouseMoving = true;_mouseDownLocation = new Point(e.Location.X, e.Location.Y);}base.OnMouseDown(e);}protected virtual void ImageResize(){switch (AutoDisplay){case AutoDisplayMode.Original:OriginalSize();break;case AutoDisplayMode.Fit:Fit();break;case AutoDisplayMode.Full:Full();break;}}protected override void OnResize(EventArgs e){if (this.Width != 0 && this.Height != 0){ImageResize();}base.OnResize(e);}protected override void OnMouseUp(MouseEventArgs e){this.Cursor = Cursors.Default;_isMouseMoving = false;base.OnMouseUp(e);}protected override void OnMouseWheel(MouseEventArgs e){if (e.Delta > 0){Zoom(2, 2, new PointF(e.X, e.Y));}else{Zoom(0.5, 0.5, new PointF(e.X, e.Y));}base.OnMouseWheel(e);}protected override void OnMouseMove(MouseEventArgs e){_mouseLocation = e.Location;if (_isMouseMoving && Image != null){//移动图片Point nowLocation = new Point(e.X, e.Y);Point move = (nowLocation - _mouseDownLocation);SyncUpdateOrigin( _cdgMat.DispOrigin + move);Refresh();_mouseDownLocation = nowLocation;}else if (_cdgMat.IsFocus(e.Location)){//坐标在绘图区域内//记录实际像素点和颜色 ,提示在tooltip上this.Cursor = Cursors.Cross;Point p = _cdgMat.TransformPixelPostion(e.Location,_pixcelSize);if (!p.Equals(_mouseLocation) && !p.Equals(_mousePixcelLocation)){string tip = string.Format("({0},{1})", p.X, p.Y);object[] res = null;MatHelper.GetMatChannelValues(Image, p.X, p.Y, out res);tip += " [";foreach (object obj in res){tip += obj + ",";}tip = tip.Substring(0, tip.Length - 1) + ']';Console.WriteLine(tip);if (PositionChanged != null)PositionChanged(this, new PosChangedEventArgs(p, res));}_mousePixcelLocation = p;}else{//坐标不在绘图区域内_mousePixcelLocation = new Point(-1, -1);}base.OnMouseMove(e);}protected override void OnPaint(PaintEventArgs e){base.OnPaint(e);Graphics gh = e.Graphics;gh.Clear(this.BackColor);if (Image != null){_cdgMat.OnPaint(e, _pixcelSize);}foreach(CvDisplayGraphicsObject obj in GraphicsObjects){obj.OnPaint(e, _pixcelSize);}}#endregion#region 内部使用函数/// <summary>/// 同步更新所有绘图的原点/// </summary>/// <param name="p"></param>protected void SyncUpdateOrigin(Point2d p){_cdgMat.DispOrigin = p;foreach(CvDisplayGraphicsObject obj in this.GraphicsObjects){obj.DispOrigin = p;}}static System.Drawing.Point ConvertCvPoint2DrawingPoint(Point p){return new System.Drawing.Point(p.X, p.Y);}static Point ConvertDrawingPoint2CvPoint(System.Drawing.Point p){return new Point(p.X, p.Y);}#endregion#region 对外接口/// <summary>/// 图片缩放/// </summary>/// <param name="scale">x,y等比例缩放参数</param>public void Zoom(double scale){Zoom(scale, scale);}/// <summary>/// 另存为/// </summary>/// <param name="filepath"></param>public void SaveAs(string filepath){if (Image == null) return;Cv2.ImWrite(filepath, Image);}/// <summary>/// 图片缩放/// </summary>/// <param name="xScale">x缩放参数</param>/// <param name="yScale">y缩放参数</param>public void Zoom(double xScale, double yScale){Zoom(xScale, yScale, new PointF(0, 0));}/// <summary>/// 根据某个原点进行缩放/// </summary>/// <param name="xScale">x缩放参数</param>/// <param name="yScale">y缩放参数</param>/// <param name="zoomOrign">缩放参考点</param>public void Zoom(double xScale, double yScale, PointF zoomOrign){if (Image == null) return;double newXPixelSize = Math.Abs(xScale) * _pixcelSize.Width;double newYPixelSize = Math.Abs(yScale) * _pixcelSize.Height;if (newXPixelSize > 0 && newYPixelSize > 0){int dispPixelX = (int)(this.Width / newXPixelSize),dispPixelY = (int)(this.Height / newYPixelSize);if (dispPixelX < 1 || dispPixelY < 1) //最少显示一个像素点return;_pixcelSize = new Size2d(newXPixelSize, newYPixelSize);if (_cdgMat.IsFocus(zoomOrign)) //如果在聚焦在图片某点放大{//变换前 图片绘制坐标原点距离 当前鼠标鼠标的距离double disX = zoomOrign.X - _cdgMat.DispOrigin.X,disY = zoomOrign.Y - _cdgMat.DispOrigin.Y;//缩放后的距离disX *= xScale;disY *= yScale;//同步更新所有需要绘图的元素的原点SyncUpdateOrigin( new Point2d(zoomOrign.X - disX, zoomOrign.Y - disY));}_cdgMat.ResizeDispRectWithPixcelSize(_pixcelSize);Refresh();}}/// <summary>/// 整个图片充满控件/// </summary>public virtual void Full(){if (Image == null) return;//换算单个像素尺寸_pixcelSize.Width = this.Width / (double)Image.Width;_pixcelSize.Height = this.Height / (double)Image.Height;_cdgMat.DispOrigin = new Point2d(0, 0);_cdgMat.ResizeDispRectWithPixcelSize(_pixcelSize);Refresh();}/// <summary>/// 自适应图片的横纵比最大化/// </summary>public virtual void Fit(){if (Image == null) return;Size2d newsize = new Size2d();double hvScale1 = this.Width / (double)this.Height,//控件横纵比hvScale2 = Image.Width / (double)Image.Height;//图片横纵比//根据横纵比算出实际上画图的大小if (hvScale1 > hvScale2){newsize.Height = this.Height;newsize.Width = (Image.Width * ((double)newsize.Height / Image.Height));}else{newsize.Width = this.Width;newsize.Height = (Image.Height * ((double)newsize.Width / Image.Width));}//计算单像素尺寸_pixcelSize.Width = newsize.Width / (double)Image.Width;_pixcelSize.Height = newsize.Height / (double)Image.Height;_cdgMat.ResizeDispRectWithPixcelSize(_pixcelSize);SyncUpdateOrigin(new Point2d((this.Width - _cdgMat.DispRect.Width) / 2,(this.Height - _cdgMat.DispRect.Height) / 2));Refresh();}/// <summary>/// 恢复图片原始比例/// </summary>public virtual void OriginalSize(){if (Image == null) return;_pixcelSize.Width = 1.0;_pixcelSize.Height = 1.0;SyncUpdateOrigin( new Point2d(0, 0));_cdgMat.ResizeDispRectWithPixcelSize(_pixcelSize);Refresh();}#endregion}
结尾:目前完成图片的查看,代码比较糙(后续代码可能会重构过),后续会添加 画点、线、圆、旋转矩形等操作,最后会结合人机交互绘制以上几何形状
几何图形绘制及调整操作已完成,最近工作较忙,这方面的学习暂停了,源代码分享地址,需要的可自行下载:
