Title: Split images into halves in C#
Sometimes I start a project that begins simply enough but that requires me to write a couple of other programs. In this case I wanted to write a simple flash card program to display images of Japanese Hiragana characters so I could learn their pronunciation. The program would display a character. When you clicked or pressed a button, it would show you a second image showing a mnemonic giving the character's pronunciation.
For example, the picture at the top of the post shows a Hiragana character on the left. The mnemonic image on the right has an A drawn on it to remind you that this character makes the "A" sound as in "car." (It's one of the better mnemonics. Some of the others are a bit of a stretch.
I found some images that I liked at ToFuGu, but that site places each character and its mnemonic on a single image. That lead to this program to split images.
Even that wasn't completely trivial because the two pieces of the image are not centered in their halves. If you split an image in the middle, then the two pieces are near the left and right edges of the halves. For example, if you look at the picture at the top of the post, you'll see that the two images are near the edges of the picture.
This program lets you split images into halves. It then processes the halves to remove whitespace around the edges so you can displays the results centered.
Using the Program
To use the program, enter the name of the directory that contains the images to split. Also enter an output directory. Use the radio buttons to indicate whether you want to split the images vertically or horizontally, and check the Remove Whitespace box if you like. When you click Split, the program splits the image files in the input directory and saves the results in the output directory with _a and _b appended to the name. For example, the file ku.png is split into files ku_a.png and ku_b.png.
Getting Started
When you click the Split button, the following code executes.
// Process the image files in the input directory,
// saving the results in the output directory.
private void btnSplit_Click(object sender, EventArgs e)
{
string from_dir, to_dir;
from_dir = txtFromDir.Text;
to_dir = txtToDir.Text;
bool split_horizontally = radSplitHorizontally.Checked;
bool remove_whitespace = chkRemoveWhitespace.Checked;
foreach (string filename in Directory.GetFiles(from_dir))
{
FileInfo file_info = new FileInfo(filename);
string extension = file_info.Extension.ToLower();
if ((extension == ".png") ||
(extension == ".jpg") ||
(extension == ".gif") ||
(extension == ".tiff") ||
(extension == ".jpeg"))
{
SplitFile(file_info, to_dir,
split_horizontally,
remove_whitespace);
}
}
picSample.Image = null;
}
This event handler gets the input and output directories, a value indicating which radio button is checked, and a value indicating whether the Remove Whitespace check box it checked. The code then uses Directory.GetFiles to loop over the files in the input directory.
For each file in the directory, the code creates a FileInfo object and examines its extension. If the extension is for a graphical file type, the code calls the SplitFile method described in the next section to split the file.
Split Images
The following SplitFile method splits a file.
// Split the image file into two pieces.
private void SplitFile(FileInfo file_info,
string to_dir, bool split_horizontally,
bool remove_whitespace)
{
Bitmap bm = LoadBitmapUnlocked(file_info.FullName);
picSample.Image = bm;
picSample.Refresh();
int wid = bm.Width / 2;
int hgt = bm.Height / 2;
if (split_horizontally)
hgt = bm.Height;
else
wid = bm.Width;
Rectangle src_rect_a = new Rectangle(0, 0, wid, hgt);
Bitmap bm_a = new Bitmap(wid, hgt);
using (Graphics gr = Graphics.FromImage(bm_a))
{
gr.DrawImage(bm, 0, 0, src_rect_a, GraphicsUnit.Pixel);
}
if (remove_whitespace) bm_a = RemoveWhitespace(bm_a);
string filename_a = to_dir + "\\" +
Path.GetFileNameWithoutExtension(file_info.Name) +
"_a" + file_info.Extension;
SaveImage(bm_a, filename_a);
Rectangle src_rect_b;
if (split_horizontally)
src_rect_b = new Rectangle(wid, 0, wid, hgt);
else
src_rect_b = new Rectangle(0, hgt, wid, hgt);
Bitmap bm_b = new Bitmap(wid, hgt);
using (Graphics gr = Graphics.FromImage(bm_b))
{
gr.DrawImage(bm, 0, 0, src_rect_b, GraphicsUnit.Pixel);
}
if (remove_whitespace) bm_b = RemoveWhitespace(bm_b);
string filename_b = to_dir + "\\" +
Path.GetFileNameWithoutExtension(file_info.Name) +
"_b" + file_info.Extension;
SaveImage(bm_b, filename_b);
}
The method first opens the file without locking it. To see how the LoadBitmapUnlocked method works, see the post Load images without locking their files in C#.
The code displays the image in the picSample PictureBox control. It then gets the image's dimensions and updates the width or height depending on whether the program should split the image vertically or horizontally.
Next the program creates a new bitmap that is big enough to hold one of the image's halves. It creates an associated Graphics object and draws the left/top half of the original image onto the new bitmap.
If the method's remove_whitespace parameter is true, then the code calls the RemoveWhitespace method described later to remove the whitespace around the image.
The code then gets the original file's name without its extension. It adds "_a" to the name, prepends the output directory, and appends the original extension. For example, this might convert the name C:\Input Directory\ku.png to the name C:\Output Directory\ku_a.png.
Now the code calls the SaveImage method to save the new image into a file with the appropriate format. For example, if the file's name ends with .jpg, then that method saves the image in JPG format. For information on that method, see the post Save images with an appropriate format depending on the file name's extension in C#.
The SplitFile method then repeats those steps to generate and save the image on the right/bottom of the original image.
RemoveWhitespace
The following RemoveWhitespace method returns a copy of an image with any whitespace around its edges removed.
// Return a copy of the parts of the bitmap that is not white.
private Bitmap RemoveWhitespace(Bitmap bm)
{
Rectangle src_rect = NonWhiteBounds(bm);
Rectangle dest_rect = new Rectangle(0, 0,
src_rect.Width, src_rect.Height);
Bitmap new_bm = new Bitmap(src_rect.Width, src_rect.Height);
using (Graphics gr = Graphics.FromImage(new_bm))
{
gr.DrawImage(bm, dest_rect, src_rect, GraphicsUnit.Pixel);
}
return new_bm;
}
The method first calls the NonWhiteBounds method described next to find the rectangle in the original image that contains all of its non-white pixels. It creates a bitmap that is the size of that rectangle, makes an associated Graphics object, and draws the corresponding area on the original image onto the new bitmap.
The method then returns the new bitmap.
NonWhiteBounds
The NonWhiteBounds method shown in the following code is somewhat lengthy by relatively straightforward.
// Find the bounds of the image's non-white pixels.
private Rectangle NonWhiteBounds(Bitmap bm)
{
Bitmap32 bm32 = new Bitmap32(bm);
bm32.LockBitmap();
int ymin = bm.Height;
for (int y = 0; y < bm.Height; y++)
{
for (int x = 0; x < bm.Width; x++)
{
byte r, g, b, a;
bm32.GetPixel(x, y, out r, out g, out b, out a);
if (r + g + b < 765)
{
ymin = y;
break;
}
}
if (ymin < bm.Height) break;
}
if (ymin == bm.Height) return new Rectangle(0, 0, -1, -1);
int ymax = -1;
for (int y = bm.Height - 1; y >= ymin; y--)
{
for (int x = 0; x < bm.Width; x++)
{
byte r, g, b, a;
bm32.GetPixel(x, y, out r, out g, out b, out a);
if (r + g + b < 765)
{
ymax = y;
break;
}
}
if (ymax > -1) break;
}
int xmin = bm.Width - 1;
for (int x = 0; x < bm.Width; x++)
{
for (int y = ymin; y <= ymax; y++)
{
byte r, g, b, a;
bm32.GetPixel(x, y, out r, out g, out b, out a);
if (r + g + b < 765)
{
xmin = x;
break;
}
}
if (xmin < bm.Width - 1) break;
}
int xmax = -1;
for (int x = bm.Width - 1; x >= xmin; x--)
{
for (int y = ymin; y <= ymax; y++)
{
byte r, g, b, a;
bm32.GetPixel(x, y, out r, out g, out b, out a);
if (r + g + b < 765)
{
xmax = x;
break;
}
}
if (xmax > -1) break;
}
bm32.UnlockBitmap();
return new Rectangle(xmin, ymin,
xmax - xmin + 1,
ymax - ymin + 1);
}
This method simply examines the image's pixels to see which are non-white. You can do that by using the Bitmap class's GetPixel method, but that method is slow. To make the operation faster, the program uses the Bitmap32 class described in the post Use the Bitmap32 class to manipulate image pixels very quickly in C#. See that post for details.
The NonWhiteBounds method creates a Bitmap32 object associated with the original bitmap. It then locks the Bitmap32 object so it can examine its pixels.
Next the code loops through the image's rows from top to bottom. For each row, it loops through the row's pixels from left to right.
When it examines a pixel, the code uses the Bitmap32 class's GetPixel method to get the pixel's red, green, blue, and alpha components. Each of those components is a byte with value between 0 and 255. The code adds the red, green, and blue components and compares the result to 765. The value 765 is three times 255, which is what you get if the pixel is completely white. You could modify the program here to ignore pixels that are almost white. For example, you could compare the sum to 755 to ignore pixels that are very close to white.
If the code finds a non-white pixel, the code sets ymin equal to the current row number and breaks out of the inner loop. When it finishes the inner loop, the code checks the ymin value to see if it has been set. If the value is set, the code breaks out of its outer loop. At this point ymin holds the Y coordinate of the first row that contains a non-white pixel.
If the loops do not find any non-white pixels, then ymin keeps its original value, which is the row one pixel beyond the limits of the bitmap. In that case, the method returns a rectangle with negative width and height to indicate that it did not find any non-white pixels.
The method then performs similar steps to find the largest Y coordinate that contains a non-white pixel. Note that ymax could be the same as ymin.
Next the method uses similar loops to find the smallest and largest X coordinates containing non-white pixels. Notice that in these inner loops the value y ranges from ymin to ymax and not over every Y value. We already know that Y values outside of that range contain no non-white pixels, so we don't need to check them.
After the method has found the minimum and maximum X and Y values, it uses those values to create a rectangle representing the area that contains non-white pixels and returns it.
Conclusion
That's the end of this part of the flash card project. Now we can split the flash card images into two halves containing a Hiragana character an a mnemonic, both with whitespace removed.
Unfortunately some of the mnemonics don't make much sense until you get used to them. For example, the picture on the right shows one of the mnemonics. From the picture alone, you might guess that this represents the M in Mountain or perhaps the F in Mt Fuji. (This is a Japanese character set, after all.) In fact this mnemonic represents the He in Mt Saint Helens.
To make the mnemonics usable until you get used to them, my next post will show how to add labels to the bottoms of images. For example, the picture on the right shows the annotated "he" image.
Download the example to experiment with it and to see additional details.
|