マウスによる拡大処理と自由曲線描画 |
過去2回でpictureBoxへの①自由曲線描画と②描画画像拡大を行いました。今回はそれらを同時に実行するツールを作成しました。
私にとっての課題は、拡大し異なる倍率で表示されている画像に対して、同じ太さを指定した時に同じ太さの線を描画することです。当り前の様ですが、同じ太さとは、表示倍率に対応した太さの曲線を描くことです。どの様に太さを指定するのでしょうか。
今回は、元画像(写真)と同サイズの描画CANVASを用意し、表示倍率に関係なくCANVAS側に描画後、倍率・表示位置を指定し、pictureBoxにCANVASの全体もしくは一部を描画することにしました。
描画CANVASとpictureBox構成 |
描画用CANVASと表示用pictureBoxの構成を次の図に示します。以前投稿した自由曲線は直接pictureBoxに描画していました。この方法ではpictureBoxサイズに対応する線の太さとなる為、元画像表示倍率を変更した時も同じ太さの線になってしまいました。
この問題を解決する為に、元画像と同じサイズのBitmapキャンバスを用意します。pictureBoxマウスイベント発生時に取得出来る座標を、このBitmapキャンバスに対応する座標に変換・描画後、元画像と同範囲・同縮尺で対応するpictureBoxに描画します。
pictureBoxはレイヤー構成となっており、元画像以外のレイヤーは背景が無色透明になっているので、元画像上に描画している様に見えます。
直接pictureBoxに描画する方法に対し、一度Bitmapキャンバスに書き込む方法は処理時間が長くなります。マウス操作時に発生するマウスムーブイベント毎に再描画処理を行うと遅延が大きくなるので、予め画像を縮小化したり、再描画処理の頻度を減らす様にしています。
評価プログラム |
評価プログラムを掲載します。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace ListViewTest { public partial class Frm_05 : Form { private Bitmap srcImg; private Bitmap drwCvs; private Bitmap tmpCvs; private bool draw_flag = false; private Point[] mus_dn = new Point[2]; private double zRatio = 1d ; private Rectangle drwRct ; private Rectangle srcRct ; private Size minSiz; //private int color_trans; //private int line_wdth; private List<Point> points = new List<Point>(); public Frm_05() { InitializeComponent(); } private void Frm_05_Load(object sender, EventArgs e) { pict_init(); minSiz = this.Size; // 起動時サイズを Minimumサイズとして登録する btnSizeLock(1); string src_pth = @""; string tgt_pth = @""; SaveImage(src_pth, tgt_pth, 80); // 元ファイルパス、新規ファイルパス、品質(1~100)を指定し圧縮する System.Drawing.Bitmap orgImg = new Bitmap(tgt_pth); srcImg = trans_orgImg_to_crtImg(orgImg); // 最大幅1600として画像を縮小する tmpCvs = new Bitmap(srcImg.Width, srcImg.Height); drwCvs = new Bitmap(srcImg.Width, srcImg.Height); draw_picturebox(0); } // pictureBox初期設定 private void pict_init() { pictureBox1.Location = new Point(8, 8); pictureBox2.Location = new Point(8, 8); pictureBox3.Location = new Point(8, 8); pictureBox4.Location = new Point(8, 8); // 親pictureBox対し、透明化する処理 pictureBox2.Parent = pictureBox1; pictureBox2.BackColor = Color.Transparent; // 元のParent(Form)に対する座標を補正する pictureBox2.Location = new Point(pictureBox2.Location.X - pictureBox1.Location.X, pictureBox2.Location.Y - pictureBox1.Location.Y); pictureBox2.Size = pictureBox1.Size; // 親pictureBox対し、透明化する処理 pictureBox3.Parent = pictureBox2; pictureBox3.BackColor = Color.Transparent; // 元のParent(Form)に対する座標を補正する pictureBox3.Location = new Point(pictureBox3.Location.X - pictureBox1.Location.X, pictureBox3.Location.Y - pictureBox1.Location.Y); pictureBox3.Size = pictureBox1.Size; // 親pictureBox対し、透明化する処理 pictureBox4.Parent = pictureBox3; pictureBox4.BackColor = Color.Transparent; // 元のParent(Form)に対する座標を補正する pictureBox4.Location = new Point(pictureBox4.Location.X - pictureBox1.Location.X, pictureBox4.Location.Y - pictureBox1.Location.Y); pictureBox4.Size = pictureBox1.Size; } // 拡大・描画切り替えボタン private void button1_Click(object sender, EventArgs e) { if (button1.Text == "描画") { btnSizeLock(0); } else { btnSizeLock(1); } } // 描画・拡大切り替え private void btnSizeLock(int dsp) { if (dsp == 1) { this.MinimumSize = this.Size; this.MaximumSize = this.Size; button1.BackColor = Color.Red; button1.ForeColor = Color.Yellow; button1.Text = "描画"; } else { this.MinimumSize = minSiz; this.MaximumSize = new Size(0, 0); button1.BackColor = Color.Blue; button1.ForeColor = Color.White; button1.Text = "拡大"; } } // ◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆◆ // 画像幅が一定値より大きい場合、再描画(画像サイズ縮小) private Bitmap trans_orgImg_to_crtImg(Bitmap orgImg) { Bitmap tmpBitMap; int org_wdt = orgImg.Width; int org_hgt = orgImg.Height; int trs_wdt = 1600; int trs_hgt = (int)(trs_wdt * org_hgt / org_wdt); if (org_wdt > trs_wdt) { Bitmap cvs = new Bitmap(trs_wdt, trs_hgt); Graphics g = Graphics.FromImage(cvs); g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.DrawImage(orgImg, 0, 0, trs_wdt, trs_hgt); g.Dispose(); tmpBitMap = cvs; } else { tmpBitMap = orgImg; } return tmpBitMap; } /// <summary> /// 指定された画像ファイルを、品質を指定してJPEGで保存する /// </summary> /// <param name="fileName">変換する画像ファイル名</param> /// <param name="quality">品質</param> public static void SaveImage(string fl_Name_Src, string fl_Name_Tgt, int quality) { System.Drawing.Bitmap bmp = new System.Drawing.Bitmap(fl_Name_Src); // 画像ファイル読込 // EncoderParameterオブジェクト1つ格納するEncoderParametersクラスの新インスタンスを初期化(品質のみ指定するため1つ) System.Drawing.Imaging.EncoderParameters eps = new System.Drawing.Imaging.EncoderParameters(1); // 品質指定 System.Drawing.Imaging.EncoderParameter ep = new System.Drawing.Imaging.EncoderParameter(System.Drawing.Imaging.Encoder.Quality, (long)quality); eps.Param[0] = ep; // EncoderParametersにセット System.Drawing.Imaging.ImageCodecInfo ici = GetEncoderInfo("image/jpeg"); // イメージエンコーダに関する情報取得 string ext = ici.FilenameExtension.Split(';')[0]; // 新ファイル拡張子取得 ext = System.IO.Path.GetExtension(ext).ToLower(); string saveName = System.IO.Path.ChangeExtension(fl_Name_Tgt, ext); // 保存ファイル名を決定(拡張子を変える) bmp.Save(saveName, ici, eps); // 保存 bmp.Dispose(); eps.Dispose(); } //MimeTypeで指定されたImageCodecInfoを探して返す private static System.Drawing.Imaging.ImageCodecInfo GetEncoderInfo(string mineType) { //GDI+ に組み込まれたイメージ エンコーダに関する情報をすべて取得 System.Drawing.Imaging.ImageCodecInfo[] encs = System.Drawing.Imaging.ImageCodecInfo.GetImageEncoders(); //指定されたMimeTypeを探して見つかれば返す foreach (System.Drawing.Imaging.ImageCodecInfo enc in encs) { if (enc.MimeType == mineType) { return enc; } } return null; } // フォームリサイズ private void Frm_05_Resize(object sender, EventArgs e) { draw_picturebox(0); } // ◆◆◆ ↓↓↓ マウス操作 ↓↓↓ ◆◆◆ private void pictureBox4_MouseDown(object sender, MouseEventArgs e) { draw_flag = true; if (button1.Text.Trim() == "拡大") { // 拡大処理 mus_dn[0].X = e.X; mus_dn[0].Y = e.Y; } else { // 描画 Point tmp_pnt = new Point(e.X, e.Y); points.Add(tmp_pnt); } } private void pictureBox4_MouseMove(object sender, MouseEventArgs e) { if (!draw_flag) { return; }; if (button1.Text.Trim() == "拡大") { // 拡大処理 Bitmap tmp_cvs = new Bitmap(pictureBox4.Width, pictureBox4.Height); Graphics tmp_g = Graphics.FromImage(tmp_cvs); Point tmp_mv = new Point(); Pen tmp_p = new Pen(Color.FromArgb(255, 0, 0, 0), 1); tmp_p.DashStyle = DashStyle.Dot; if (e.X - mus_dn[0].X >= 0) { tmp_mv.X = mus_dn[0].X; } else { tmp_mv.X = e.X; } // 始点修正(X) if (e.Y - mus_dn[0].Y >= 0) { tmp_mv.Y = mus_dn[0].Y; } else { tmp_mv.Y = e.Y; } // 始点修正(Y) int tmp_wdt = Math.Abs(e.X - mus_dn[0].X); // 幅(X) int tmp_hgt = Math.Abs(e.Y - mus_dn[0].Y); // 幅(Y) tmp_g.DrawRectangle(tmp_p, tmp_mv.X, tmp_mv.Y, tmp_wdt, tmp_hgt); // 指定領域枠描画 pictureBox4.Image = tmp_cvs; } else { // 描画 points.Add(new Point(e.X, e.Y)); Point[] pa = points.ToArray(); if (pa.Length < 3) { return; } if (pa.Length % 6 == 0) { draw_tmpCvs( pa , 0 ); } // ★ 処理頻度を下げ、遅延抑制 } } private void pictureBox4_MouseUp(object sender, MouseEventArgs e) { if (!draw_flag) { return; }; if (button1.Text.Trim() == "拡大") { // 拡大処理 if (e.X > pictureBox4.Width) { mus_dn[1].X = pictureBox4.Width ; } else if (e.X < 0) { mus_dn[1].X = 0 ; } else { mus_dn[1].X = e.X ; } if (e.Y > pictureBox4.Height) { mus_dn[1].Y = pictureBox4.Height; } else if (e.Y < 0) { mus_dn[1].Y = 0; } else { mus_dn[1].Y = e.Y; } pictureBox4.Image.Dispose(); pictureBox4.Image = null; draw_picturebox(1); // ★★★ 再表示 ★★★ draw_flag = false; } else { // 描画 points.Add(new Point(e.X, e.Y)); Point[] pa = points.ToArray(); if (pa.Length < 3) { return; } draw_tmpCvs(pa, 1); // ★ 描画内容確定 points.Clear(); } draw_flag = false; } // ◆◆◆ ↑↑↑ マウス操作 ↑↑↑ ◆◆◆ // パスをpictureBoxに直接描画(kbn → 0:MouseMove時(暫定)、1:MouseUp時(確定)) private void draw_tmpCvs(Point[] pa , int kbn) { // 座標変換 GraphicsPath tmp_pth = new GraphicsPath(); for (int i = 0; i < pa.Length; i++) { int tmp_X = srcRct.X + (int)Math.Round((double)srcRct.Width * pa[i].X / drwRct.Width); int tmp_Y = srcRct.Y + (int)Math.Round((double)srcRct.Height * pa[i].Y / drwRct.Height); pa[i].X = tmp_X; pa[i].Y = tmp_Y; if (i == 0){ tmp_pth.StartFigure(); } else if (i > 0) { tmp_pth.AddLine(pa[i - 1].X, pa[i - 1].Y, pa[i].X, pa[i].Y); } } // ペンの定義 Pen tmp_p = new Pen(Color.FromArgb(50, 255, 0, 0), 30); tmp_p.EndCap = System.Drawing.Drawing2D.LineCap.Round; tmp_p.StartCap = System.Drawing.Drawing2D.LineCap.Round; tmp_p.LineJoin = System.Drawing.Drawing2D.LineJoin.Round; // キャンバスに描画 Graphics tmp_g; if (kbn == 0) { tmpCvs = new Bitmap(srcImg.Width, srcImg.Height); tmp_g = Graphics.FromImage(tmpCvs); } else { pictureBox3.Image = null; tmp_g = Graphics.FromImage(drwCvs); } tmp_g.DrawPath(tmp_p, tmp_pth); tmp_p.Dispose(); tmp_g.Dispose(); // ピクチャーボックスに描画コメント表示 Bitmap cvs = new Bitmap(pictureBox1.Width, pictureBox1.Height); Graphics g = Graphics.FromImage(cvs); if (kbn == 0) { g.DrawImage(tmpCvs, drwRct, srcRct, GraphicsUnit.Pixel); g.Dispose(); pictureBox3.Image = cvs; } else { g.DrawImage(drwCvs, drwRct, srcRct, GraphicsUnit.Pixel); g.Dispose(); pictureBox2.Image = cvs; } } // ①起動時、フォームリサイズ時、リセットボタン押下時 // ②マウス領域指定による画像拡大(マウスアップ時呼出) private void draw_picturebox(int kbn) { Bitmap cvs; Graphics g; Size PicMaxSiz = new Size(this.ClientSize.Width - 16, this.ClientSize.Height - 70); //Size SrcPicSiz = new Size(srcImg.Width, srcImg.Height); if (PicMaxSiz.Width < 1 || PicMaxSiz.Height < 1) { return; } if (kbn == 0) { // フォームサイズ変更に対応し、全画像を表示 double ratio_w = (double)PicMaxSiz.Width / srcImg.Width; // 幅方向倍率 double ratio_h = (double)PicMaxSiz.Height / srcImg.Height; // 高さ方向倍率 if (ratio_w > ratio_h) { zRatio = ratio_h; } else { zRatio = ratio_w; } // 全対象領域が収まる倍率確定 srcRct = new Rectangle(0, 0, srcImg.Width, srcImg.Height); drwRct = new Rectangle(0, 0, (int)Math.Round(srcImg.Width * zRatio), (int)Math.Round(srcImg.Height * zRatio)); } else if (kbn == 1) { // 切り抜き部分を表示 Point tmp_mv = new Point(); if (mus_dn[1].X - mus_dn[0].X >= 0) { tmp_mv.X = mus_dn[0].X; } else { tmp_mv.X = mus_dn[1].X; } // 始点修正(X) if (mus_dn[1].Y - mus_dn[0].Y >= 0) { tmp_mv.Y = mus_dn[0].Y; } else { tmp_mv.Y = mus_dn[1].Y; } // 始点修正(Y) int tmp_wdt = Math.Abs(mus_dn[1].X - mus_dn[0].X); // 幅(X) int tmp_hgt = Math.Abs(mus_dn[1].Y - mus_dn[0].Y); // 幅(Y) double ratio_w = (double)PicMaxSiz.Width / tmp_wdt; // 幅方向倍率 double ratio_h = (double)PicMaxSiz.Height / tmp_hgt; // 高さ方向倍率 if (ratio_w > ratio_h) { zRatio = ratio_h; } else { zRatio = ratio_w; } // 全対象領域が収まる倍率確定 Rectangle tmpRct = srcRct; Rectangle tp_Rct = drwRct; // 指定領域を元画像に対する領域に変換 srcRct.X = tmpRct.X + (int)Math.Round((double)tmpRct.Width * tmp_mv.X / tp_Rct.Width); // 元画像始点(X) srcRct.Y = tmpRct.Y + (int)Math.Round((double)tmpRct.Height * tmp_mv.Y / tp_Rct.Height); // 元画像始点(Y) srcRct.Width = (int)Math.Round((double)tmpRct.Width * tmp_wdt / tp_Rct.Width); // 元画像幅(W) srcRct.Height = (int)Math.Round((double)tmpRct.Height * tmp_hgt / tp_Rct.Height); // 元画像高さ(H) drwRct = new Rectangle(0, 0, (int)Math.Round(tmp_wdt * zRatio), (int)Math.Round(tmp_hgt * zRatio)); } pictureBox1.Size = new Size( drwRct.Width , drwRct.Height); pictureBox2.Size = pictureBox1.Size; pictureBox3.Size = pictureBox1.Size; pictureBox4.Size = pictureBox1.Size; textBox1.Text = drwRct.Width.ToString(); textBox2.Text = drwRct.Height.ToString(); // 元画像(写真)表示 cvs = new Bitmap(pictureBox1.Width, pictureBox1.Height); g = Graphics.FromImage(cvs); g.DrawImage(srcImg, drwRct, srcRct, GraphicsUnit.Pixel); g.Dispose(); pictureBox1.Image = cvs; // 描画コメント表示 cvs = new Bitmap(pictureBox2.Width, pictureBox2.Height); g = Graphics.FromImage(cvs); g.DrawImage(drwCvs, drwRct, srcRct, GraphicsUnit.Pixel); g.Dispose(); pictureBox2.Image = cvs; } // 画面初期化(表示されている全画像) private void Button2_Click(object sender, EventArgs e) { draw_picturebox(0); } // 初期化(コメント消去) private void Button3_Click(object sender, EventArgs e) { tmpCvs = new Bitmap(srcImg.Width, srcImg.Height); drwCvs = new Bitmap(srcImg.Width, srcImg.Height); draw_picturebox(0); } } }
結果 |
次の2つのフォームアプリケーション内のpictureBoxに表示されている写真は同じ様に見えますが、実際の表示サイズ(拡大率)は異なります。
写真下の幅・高さ値やフォームに対するボタン類の大きさ比率からも確認できると思います。
結果としては、表示倍率の異なる画像に対し、比率が同じ自由曲線を書けることを確認出来たので、とりあえず成功しました。