For OpenCV - I'm not familiar with it, but I know, that there is a FindContours-Method in OpenCV. As far as I know this Method is only for binary images.
Here's my imagination of how you could do this in OpenCV:
- Extract the blue color channel from the image.
- Threshold this by a threshold of about 128, make sure you return a binary image.
- Pass this binary image to the FindContours method of OpenCV.
- Select the correct contour from the results.
- From this contour, display [and save] the enclosed portion of the *original image*.
Considerations:
- Add some logic to automatically get the correct threshold.
- Add some logic to make the selection of the correct chainCode/Contour more reliable for different backgrounds. E.G.: Test the "rectangularity" of the, say 10, largest area ChainCodes, or look at the Gradient, or the Entropy, to decide, if the processed ChainCode is the right one. Or, if the written part always contains some same words/letters, you could correlate the picture with a testimage to get the right contour... There's a lot of options to select the correct part of the orig img, depending on e.g.: performance, or "the parameters" of the problem itself.
Here's a screenshot of a quickly setup Winforms app using gdi-plus (not OpenCV) doing this
Edit: By your request from your comment in this question comment : ...
Here's .net8.0 c# WindowsForms code with a minimal implementation of a chaincode with a threshold for copying/pasting. ChainCode in this variant developed by V. Kovalevsky. It works as follows:
As the name crackcode says, we are moving on the (invisible) "cracks" in between the pixels to find the outlines of objects
in an image. The algorithm is as follows:
- Find a start pixel by moving left to right and top to bottom over the image which matches the conditions.
- Now we are at the leftTop location of the \*pixel\*, the crack left of the pixel.
- We now set our direction to the only reasonable value "down" and move along the crack, so we truly start the
algorithm at the lowest "point" of the left crack, ie. the leftBottom location of the starting pixel, and we turn our
- looking direction also down, so the pixel left in front of us is the pixel below of the start pixel.
- Now we:
- Look to the pixel thats left in front of us,
a) if it doesnt meet the conditions (search criteria is lower than the threshold),
we turn left (set the direction to left, turn ourselves left and move along that crack to the next junction,
being now at the BottomRight corner of the start pixel)
b) if it meets the conditions, we:
a) look to the pixels thats on the right in front of us,
aa) if it doesnt meet the conditions (search criteria is lower than the threshold),
we go straight on, keeping direction "down", being now at the BottomLeft corner
of the pixel below our start pixel
bb) if it meets the conditions, we turn right (setting the direction and ourselves "right"
and move along that crack to the next junction, being now at the BottomLeft corner
of the pixel on the left of our start pixel (when looking "normally" to the picture)
- Since we always turn our looking-direction, we can now repeat the above until we are back at the point we began.
- Since we move on the cracks, we always will come back to that point, if we only have one pixel matching the
conditions, we get a chain of 4 cracks, once around the pixel
//##########################################################################
What follows is a step by step walkthrough to setup the windows forms app:
- Open VisualStudio and create a new c# WinForms app in .net 8.0.
- Open the Forms1.Designer.cs file and replace the InitializeComponent - method (also the surrounding comment and pre-processor directive ("region ...")) with the complete content of the following code-block.
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
btnOpen = new Button();
btnGetChains = new Button();
splitContainer1 = new SplitContainer();
numTh = new NumericUpDown();
label1 = new Label();
btnSave = new Button();
splitContainer2 = new SplitContainer();
pictureBox1 = new PictureBox();
splitContainer3 = new SplitContainer();
pictureBox2 = new PictureBox();
label7 = new Label();
label6 = new Label();
cbSelSingleClick = new CheckBox();
btnSelNone = new Button();
btnSelAll = new Button();
cbRestrict = new CheckBox();
numRestrict = new NumericUpDown();
checkedListBox1 = new CheckedListBox();
saveFileDialog1 = new SaveFileDialog();
OpenFileDialog1 = new OpenFileDialog();
((System.ComponentModel.ISupportInitialize)splitContainer1).BeginInit();
splitContainer1.Panel1.SuspendLayout();
splitContainer1.Panel2.SuspendLayout();
splitContainer1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)numTh).BeginInit();
((System.ComponentModel.ISupportInitialize)splitContainer2).BeginInit();
splitContainer2.Panel1.SuspendLayout();
splitContainer2.Panel2.SuspendLayout();
splitContainer2.SuspendLayout();
((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit();
((System.ComponentModel.ISupportInitialize)splitContainer3).BeginInit();
splitContainer3.Panel1.SuspendLayout();
splitContainer3.Panel2.SuspendLayout();
splitContainer3.SuspendLayout();
((System.ComponentModel.ISupportInitialize)pictureBox2).BeginInit();
((System.ComponentModel.ISupportInitialize)numRestrict).BeginInit();
SuspendLayout();
//
// btnOpen
//
btnOpen.Location = new Point(12, 15);
btnOpen.Name = "btnOpen";
btnOpen.Size = new Size(75, 23);
btnOpen.TabIndex = 0;
btnOpen.Text = "open";
btnOpen.UseVisualStyleBackColor = true;
btnOpen.Click += btnOpen_Click;
//
// btnGetChains
//
btnGetChains.Location = new Point(348, 15);
btnGetChains.Name = "btnGetChains";
btnGetChains.Size = new Size(75, 23);
btnGetChains.TabIndex = 1;
btnGetChains.Text = "GetChains";
btnGetChains.UseVisualStyleBackColor = true;
btnGetChains.Click += btnGetChains_Click;
//
// splitContainer1
//
splitContainer1.Dock = DockStyle.Fill;
splitContainer1.Location = new Point(0, 0);
splitContainer1.Name = "splitContainer1";
splitContainer1.Orientation = Orientation.Horizontal;
//
// splitContainer1.Panel1
//
splitContainer1.Panel1.Controls.Add(numTh);
splitContainer1.Panel1.Controls.Add(label1);
splitContainer1.Panel1.Controls.Add(btnSave);
splitContainer1.Panel1.Controls.Add(btnOpen);
splitContainer1.Panel1.Controls.Add(btnGetChains);
//
// splitContainer1.Panel2
//
splitContainer1.Panel2.Controls.Add(splitContainer2);
splitContainer1.Size = new Size(1008, 750);
splitContainer1.SplitterDistance = 46;
splitContainer1.TabIndex = 2;
//
// numTh
//
numTh.Location = new Point(492, 17);
numTh.Maximum = new decimal(new int[] { 255, 0, 0, 0 });
numTh.Name = "numTh";
numTh.Size = new Size(65, 23);
numTh.TabIndex = 4;
numTh.Value = new decimal(new int[] { 128, 0, 0, 0 });
//
// label1
//
label1.AutoSize = true;
label1.Location = new Point(429, 19);
label1.Name = "label1";
label1.Size = new Size(57, 15);
label1.TabIndex = 3;
label1.Text = "threshold";
//
// btnSave
//
btnSave.Location = new Point(622, 15);
btnSave.Name = "btnSave";
btnSave.Size = new Size(75, 23);
btnSave.TabIndex = 2;
btnSave.Text = "save";
btnSave.UseVisualStyleBackColor = true;
btnSave.Click += btnSave_Click;
//
// splitContainer2
//
splitContainer2.Dock = DockStyle.Fill;
splitContainer2.Location = new Point(0, 0);
splitContainer2.Name = "splitContainer2";
//
// splitContainer2.Panel1
//
splitContainer2.Panel1.Controls.Add(pictureBox1);
//
// splitContainer2.Panel2
//
splitContainer2.Panel2.Controls.Add(splitContainer3);
splitContainer2.Size = new Size(1008, 700);
splitContainer2.SplitterDistance = 336;
splitContainer2.TabIndex = 0;
//
// pictureBox1
//
pictureBox1.Dock = DockStyle.Fill;
pictureBox1.Location = new Point(0, 0);
pictureBox1.Name = "pictureBox1";
pictureBox1.Size = new Size(336, 700);
pictureBox1.SizeMode = PictureBoxSizeMode.Zoom;
pictureBox1.TabIndex = 0;
pictureBox1.TabStop = false;
//
// splitContainer3
//
splitContainer3.Dock = DockStyle.Fill;
splitContainer3.Location = new Point(0, 0);
splitContainer3.Name = "splitContainer3";
//
// splitContainer3.Panel1
//
splitContainer3.Panel1.Controls.Add(pictureBox2);
//
// splitContainer3.Panel2
//
splitContainer3.Panel2.Controls.Add(label7);
splitContainer3.Panel2.Controls.Add(label6);
splitContainer3.Panel2.Controls.Add(cbSelSingleClick);
splitContainer3.Panel2.Controls.Add(btnSelNone);
splitContainer3.Panel2.Controls.Add(btnSelAll);
splitContainer3.Panel2.Controls.Add(cbRestrict);
splitContainer3.Panel2.Controls.Add(numRestrict);
splitContainer3.Panel2.Controls.Add(checkedListBox1);
splitContainer3.Size = new Size(668, 700);
splitContainer3.SplitterDistance = 368;
splitContainer3.TabIndex = 0;
//
// pictureBox2
//
pictureBox2.Dock = DockStyle.Fill;
pictureBox2.Location = new Point(0, 0);
pictureBox2.Name = "pictureBox2";
pictureBox2.Size = new Size(368, 700);
pictureBox2.SizeMode = PictureBoxSizeMode.Zoom;
pictureBox2.TabIndex = 0;
pictureBox2.TabStop = false;
//
// label7
//
label7.AutoSize = true;
label7.Location = new Point(143, 254);
label7.Name = "label7";
label7.Size = new Size(16, 15);
label7.TabIndex = 658;
label7.Text = "...";
//
// label6
//
label6.AutoSize = true;
label6.Location = new Point(13, 254);
label6.Name = "label6";
label6.Size = new Size(16, 15);
label6.TabIndex = 659;
label6.Text = "...";
//
// cbSelSingleClick
//
cbSelSingleClick.AutoSize = true;
cbSelSingleClick.Location = new Point(12, 315);
cbSelSingleClick.Name = "cbSelSingleClick";
cbSelSingleClick.Size = new Size(131, 19);
cbSelSingleClick.TabIndex = 652;
cbSelSingleClick.Text = "SelectOnSingleClick";
cbSelSingleClick.UseVisualStyleBackColor = true;
cbSelSingleClick.CheckedChanged += cbSelSingleClick_CheckedChanged;
//
// btnSelNone
//
btnSelNone.ForeColor = SystemColors.ControlText;
btnSelNone.Location = new Point(199, 340);
btnSelNone.Margin = new Padding(4, 3, 4, 3);
btnSelNone.Name = "btnSelNone";
btnSelNone.Size = new Size(88, 27);
btnSelNone.TabIndex = 654;
btnSelNone.Text = "UnselectAll";
btnSelNone.UseVisualStyleBackColor = true;
btnSelNone.Click += btnSelNone_Click;
//
// btnSelAll
//
btnSelAll.ForeColor = SystemColors.ControlText;
btnSelAll.Location = new Point(12, 340);
btnSelAll.Margin = new Padding(4, 3, 4, 3);
btnSelAll.Name = "btnSelAll";
btnSelAll.Size = new Size(88, 27);
btnSelAll.TabIndex = 656;
btnSelAll.Text = "SelectAll";
btnSelAll.UseVisualStyleBackColor = true;
btnSelAll.Click += btnSelAll_Click;
//
// cbRestrict
//
cbRestrict.AutoSize = true;
cbRestrict.Checked = true;
cbRestrict.CheckState = CheckState.Checked;
cbRestrict.Location = new Point(12, 290);
cbRestrict.Name = "cbRestrict";
cbRestrict.Size = new Size(158, 19);
cbRestrict.TabIndex = 653;
cbRestrict.Text = "restrict amount chains to";
cbRestrict.UseVisualStyleBackColor = true;
//
// numRestrict
//
numRestrict.Location = new Point(198, 289);
numRestrict.Maximum = new decimal(new int[] { 32767, 0, 0, 0 });
numRestrict.Minimum = new decimal(new int[] { 1, 0, 0, 0 });
numRestrict.Name = "numRestrict";
numRestrict.Size = new Size(88, 23);
numRestrict.TabIndex = 657;
numRestrict.Value = new decimal(new int[] { 100, 0, 0, 0 });
//
// checkedListBox1
//
checkedListBox1.FormattingEnabled = true;
checkedListBox1.Location = new Point(13, 15);
checkedListBox1.Name = "checkedListBox1";
checkedListBox1.Size = new Size(271, 238);
checkedListBox1.TabIndex = 0;
checkedListBox1.SelectedIndexChanged += checkedListBox1_SelectedIndexChanged;
//
// saveFileDialog1
//
saveFileDialog1.FileName = "Bild1.png";
saveFileDialog1.Filter = "Png-Images (*.png)|*.png";
//
// OpenFileDialog1
//
OpenFileDialog1.FileName = "OpenFileDialog1";
OpenFileDialog1.Filter = "Bilder (*.jpg;*.bmp;*.png)|*.jpg;*.bmp;*.png|Alle Dateien (*.*)|*.*";
//
// Form1
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(1008, 750);
Controls.Add(splitContainer1);
Name = "Form1";
Text = "Form1";
FormClosing += Form1_FormClosing;
splitContainer1.Panel1.ResumeLayout(false);
splitContainer1.Panel1.PerformLayout();
splitContainer1.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer1).EndInit();
splitContainer1.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)numTh).EndInit();
splitContainer2.Panel1.ResumeLayout(false);
splitContainer2.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)splitContainer2).EndInit();
splitContainer2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit();
splitContainer3.Panel1.ResumeLayout(false);
splitContainer3.Panel2.ResumeLayout(false);
splitContainer3.Panel2.PerformLayout();
((System.ComponentModel.ISupportInitialize)splitContainer3).EndInit();
splitContainer3.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)pictureBox2).EndInit();
((System.ComponentModel.ISupportInitialize)numRestrict).EndInit();
ResumeLayout(false);
}
#endregion
private Button btnOpen;
private Button btnGetChains;
private SplitContainer splitContainer1;
private SplitContainer splitContainer2;
private PictureBox pictureBox1;
private SplitContainer splitContainer3;
private PictureBox pictureBox2;
private CheckedListBox checkedListBox1;
private SaveFileDialog saveFileDialog1;
internal OpenFileDialog OpenFileDialog1;
private Button btnSave;
private Label label7;
private Label label6;
private CheckBox cbSelSingleClick;
private Button btnSelNone;
private Button btnSelAll;
private CheckBox cbRestrict;
private NumericUpDown numRestrict;
private NumericUpDown numTh;
private Label label1;
- Open the Form1.cs code-view and replace the form1-block (public partial class Form1 : Form { ... }) with the complete content of the following code-block. Make sure, the last closing curly brace (closing the namespace-block) doesn't get lost.
[The classes for the ChainCode will follow in the next answer, due to character-amount-restrictions per answer]
public partial class Form1 : Form
{
private bool _picChanged;
public Form1()
{
InitializeComponent();
}
private void btnOpen_Click(object sender, EventArgs e)
{
if (this._picChanged)
{
DialogResult dlg = MessageBox.Show("Save image?", "Unsaved data", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation);
if (dlg == DialogResult.Yes)
btnSave.PerformClick();
else if (dlg == DialogResult.Cancel)
return;
}
Bitmap? bmp = null;
if (this.OpenFileDialog1.ShowDialog() == DialogResult.OK)
{
using (Image img = Image.FromFile(this.OpenFileDialog1.FileName))
bmp = new Bitmap(img);
Image iOld = this.pictureBox1.Image;
this.pictureBox1.Image = bmp;
if (iOld != null)
iOld.Dispose();
this.Text = this.OpenFileDialog1.FileName;
}
}
private void btnSave_Click(object sender, EventArgs e)
{
if (this.pictureBox1.Image != null)
{
this.saveFileDialog1.Filter = "Png-Images (*.png)|*.png";
this.saveFileDialog1.FileName = "Bild1.png";
try
{
if (this.saveFileDialog1.ShowDialog() == DialogResult.OK)
{
this.pictureBox2.Image.Save(this.saveFileDialog1.FileName, System.Drawing.Imaging.ImageFormat.Png);
_picChanged = false;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
}
private void btnGetChains_Click(object sender, EventArgs e)
{
this.checkedListBox1.Items.Clear();
int th = (int)this.numTh.Value;
using Bitmap bmp = new Bitmap(this.pictureBox1.Image);
ChainFinder cf = new ChainFinder();
//please note that we pass a color-image to the function with the grayscale switch on.
//This means that only the blue color channel will be processed in the start_crach_search function,
//since this function assumes that the image is grayscaled before processing, and so just checks one
//of the three color channels.
List<ChainCode>? c = cf.GetOutline(bmp, th, true, 0, false, 0, false);
if (c != null)
{
c = c.OrderByDescending(a => a.Area).ToList();
this.checkedListBox1.Items.Clear();
this.checkedListBox1.SuspendLayout();
this.checkedListBox1.BeginUpdate();
int cnt = c.Count;
if (this.cbRestrict.Checked)
cnt = Math.Min(c.Count, (int)this.numRestrict.Value);
for (int i = 0; i < cnt; i++)
this.checkedListBox1.Items.Add(c[i], false);
this.checkedListBox1.EndUpdate();
this.checkedListBox1.ResumeLayout();
if (this.checkedListBox1.Items.Count > 0)
{
this.checkedListBox1.SetItemChecked(0, true);
this.checkedListBox1.SelectedIndex = 0;
}
}
}
private void cbSelSingleClick_CheckedChanged(object sender, EventArgs e)
{
this.checkedListBox1.CheckOnClick = this.cbSelSingleClick.Checked;
}
private void btnSelAll_Click(object sender, EventArgs e)
{
for (int i = 0; i < this.checkedListBox1.Items.Count; i++)
this.checkedListBox1.SetItemChecked(i, true);
checkedListBox1_SelectedIndexChanged(this.checkedListBox1, new EventArgs());
}
private void btnSelNone_Click(object sender, EventArgs e)
{
for (int i = 0; i < this.checkedListBox1.Items.Count; i++)
this.checkedListBox1.SetItemChecked(i, false);
checkedListBox1_SelectedIndexChanged(this.checkedListBox1, new EventArgs());
}
private void checkedListBox1_SelectedIndexChanged(object sender, EventArgs e)
{
List<ChainCode> fList = new List<ChainCode>();
for (int i = 0; i < this.checkedListBox1.CheckedItems.Count; i++)
if (this.checkedListBox1.CheckedItems[i] != null)
{
ChainCode? f = this.checkedListBox1.CheckedItems[i] as ChainCode;
if (f != null)
fList.Add(f);
}
if (this.pictureBox1.Image != null)
{
using Bitmap b = new Bitmap(this.pictureBox1.Image);
Bitmap bmp = new Bitmap(b.Width, b.Height);
ChainFinder cf = new ChainFinder();
FillOutlineInBmp(bmp, b, fList);
Image? iOld = this.pictureBox2.Image;
this.pictureBox2.Image = bmp;
if (iOld != null)
iOld.Dispose();
iOld = null;
this.pictureBox2.Refresh();
this._picChanged = true;
}
}
private void FillOutlineInBmp(Bitmap bmp, Bitmap bOrig, List<ChainCode> fList)
{
using Graphics gx = Graphics.FromImage(bmp);
using TextureBrush tb = new TextureBrush(bOrig);
for (int i = 0; i < fList.Count; i++)
{
using GraphicsPath gP = new GraphicsPath();
gP.AddLines(fList[i].Coord.Select(a => new PointF(a.X, a.Y)).ToArray());
gx.FillPath(tb, gP);
}
}
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (this.pictureBox1.Image != null)
this.pictureBox1.Image.Dispose();
if (this.pictureBox2.Image != null)
this.pictureBox2.Image.Dispose();
}
}
3.2. Add the code for the ChainCode (in follow-up answer)
- Build the app.
##########################################################################
Usage:
- Start the app and click onto the "open" button to load a picture.
- [Adjust the threshold - in the NumericUpDown Control] and click on the "GetChains" button.
- Repeat the last step until you like the result.//##########################################################################
Regards,
Thorsten