(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 override
public override void Reset()
{
base.Reset();
if(Image != null)
{
DispSize = new Size2d(Image.Width, Image.Height);
}
else
DispSize = 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);
else
DispSize = 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;
}
}
#endregion
public 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
}
结尾:目前完成图片的查看,代码比较糙(后续代码可能会重构过),后续会添加 画点、线、圆、旋转矩形等操作,最后会结合人机交互绘制以上几何形状
几何图形绘制及调整操作已完成,最近工作较忙,这方面的学习暂停了,源代码分享地址,需要的可自行下载: