c# - how to merge images in vertical? - c#

I have this code but doesn't work.I'm trying to extract a image from a site that contains a captcha.
var width = Images.First().Image.Width; //all images in list have the same width so i take the first
var height = 0;
for (int i = 104; i < 140; i++) //the list has 300 images. I have to get 36 that contains the captcha separated into pieces
{
height += Images[i].Image.Height;
}
var bitmap2 = new Bitmap(width, height);
var g = Graphics.FromImage(bitmap2);
height = 0;
for (int i = 104; i < 140; i++)
{
Image image = Images[i].Image;
g.DrawImage(image, 0, height);
height += image.Height;
}
bitmap2.Save(#"C:\Users\user\Desktop\test\test.png", ImageFormat.Png);
With this code i get this result:
image
I don't know why it is of poor quality. I think it is repeating the images that are recorded in the result bitmap

I can see a few suboptimal things in the code, but, to be honest, not a single thing that can give that result. The only way you get problems like that is if you go messing with the raw back-end and perform operations that mess up how the data is interpreted as image.
The only two specific things that need fixing in the code seem to be:
Setting the resolution of all images to the same values. This affects how large they are drawn, and thus can mess up positioning
Closing the Graphics object after you're done with it, so all changes are confirmed to be finished before you attempt to save anything.
Note that in my adjusted code, images is just a List<Bitmap>, and the for-loop just goes over them all. You never specified what type your Images collection was, and this was much easier for me to test.
Int32 width = Images.First().Width;
Int32 height = 0;
for (Int32 i = 0; i < Images.Count; i++)
{
height += Images[i].Height;
}
Bitmap bitmap2 = new Bitmap(width, height);
bitmap2.SetResolution(72, 72); // <-- Set explicit resolution on bitmap2
// Always put Graphics objects in a 'using' block.
using (Graphics g = Graphics.FromImage(bitmap2))
{
height = 0;
for (Int32 i = 0; i < Images.Count; i++)
{
Bitmap image = Images[i];
image.SetResolution(72, 72); // <-- Set resolution equal to bitmap2
g.DrawImage(image, 0, height);
height += image.Height;
}
}
bitmap2.Save(#"C:\Users\user\Desktop\test\test.png", ImageFormat.Png);

Related

Custom Measure String in C# without Graphics.MeasureString

I am trying to create an Image with a Caption/Text on it for Youtube-Thumbnails.
Following Rules are defined:
The Text is the Title of the Video and always changes from Thumbnail to Thumbnail.
The Porgram uses a pre-defined Text-Width which must not be touched by the Text on the Image.
The Text should be as close to the pre-defined with as possible.
So my thoughts were that I would use Graphics.MeasureString to be able to track the Width of the String on the Image and increase the Font-Size and repeat this until the pre-defined Width is closely reached but not touched.
But I have tested it with MeasureString and found out that it isn't that accurate. And also found confirmation here: Graphics.MeasureCharacterRanges giving wrong size calculations
I have tried the things they have recommended but with no success as the final width of my string always overflooded the image borders. Even if my pre-defined Width was way smaller than the Image Width. (Image Width: 1920; Pre-Defined Width: 1600)
So I came up with the Idea to create a custom Measurement Method and to write the String as I want it on a new Bitmap and to count the maximum Pixels of the String in Height and Width. (The Height is just for future stuff)
My current Code is:
public static SizeF MeasuredStringSize(string text, Bitmap originBitmap, FontFamily fontFamily, StringFormat strformat)
{
int currentFontSize = 10;
SizeF measuredSize = new();
var highestWidth = 0;
var highestHeight = 0;
while (highestWidth < maximumTextWidth)
{
Bitmap bitmap = new(originBitmap);
Bitmap _bitmap = new(bitmap.Width, bitmap.Height);
using Graphics graphics = Graphics.FromImage(bitmap);
if (graphics != null)
{
graphics.TranslateTransform(bitmap.Width / 2, bitmap.Height / 2);
currentFontSize++;
graphics.Clear(Color.White);
using GraphicsPath path = new();
using SolidBrush brush = new(Color.Red);
using Pen pen = new(Color.Red, 6)
{
LineJoin = LineJoin.Round
};
path.AddString(text, fontFamily, (int)fontStyle, currentFontSize, new Point(0, 0), strformat);
graphics.DrawPath(pen, path);
graphics.FillPath(brush, path);
Dictionary<int, List<int>> redPixelMatrix = new();
for (int i = 0; i < bitmap.Width; i++)
{
for (int j = 0; j < bitmap.Height; j++)
{
var currentPixelColor = bitmap.GetPixel(i, j);
if (currentPixelColor.B != 255 && currentPixelColor.G != 255 && currentPixelColor.R == 255)
{
if (!redPixelMatrix.ContainsKey(i))
{
redPixelMatrix.Add(i, new());
}
redPixelMatrix[i].Add(j);
}
}
}
highestWidth = redPixelMatrix.Keys.Count;
highestHeight = redPixelMatrix.Aggregate((l, r) => l.Value.Count > r.Value.Count ? l : r).Value.Count;
Console.WriteLine($"X:{highestWidth};Y:{highestHeight}");
//Debugging the final Image with Text to see the Result
bitmap.Save(ResultPath);
}
}
measuredSize = new SizeF(highestWidth, highestHeight);
return measuredSize;
}
The Resulting Image from bitmap.Save(ResultPath); as the String reaches the Image borders looks like this:
But the exact String width is 1742 instead of the width of my Image 1920 which should be more or less the same at this moment.
So, why is the Text nearly as wide as the Image but doesn't have the same width?
highestWidth = redPixelMatrix.Keys.Count; This will just count the number of columns containing red pixels, excluding any spaces in the text. You presumably want the minimum and maximum indices.
I.e.
var minX = int.MaxValue;
var maxX = int.MinValue;
// Loops over rows & columns
// Check if pixel is red
if(i > maxX) maxX = i;
if(i < minX) minX = i;
If you only want the text width and not the bounds you can just do maxX - minX.

Convert 32bpp to 8bpp UWP

I'm trying to draw InkCanvas to an 8bpp image but when I try to do so the image convert itself to 32bpp, the lower I got was 24bpp but not 8bpp. Anyone can help me out? The image I am giving as input is an 8bpp BMP image created with paint.
Image imgToEdit;
InkCanvas inkCanvas;
file = await Windows.ApplicationModel.Package.Current.InstalledLocation.GetFileAsync(Ambiente.imgBlankFirma);
await file.CopyAsync(photoFolder, NomeFile, NameCollisionOption.ReplaceExisting);
file = await photoFolder.GetFileAsync(NomeFile);
imgToEdit = imgFirma;
inkCanvas = inkCanvasFirma;
if (inkCanvas.InkPresenter.StrokeContainer.GetStrokes().Count <= 0)
{
errore = true;
return;
}
var randomAccessStream = await file.OpenReadAsync();
CanvasDevice device = CanvasDevice.GetSharedDevice();
CanvasRenderTarget renderTarget = new CanvasRenderTarget(device, (int)inkCanvas.ActualWidth, (int)inkCanvas.ActualHeight, 96); //inkCanvas.ActualWidth inkCanvas.ActualHeight
using (var ds = renderTarget.CreateDrawingSession())
{
var image = await CanvasBitmap.LoadAsync(device, randomAccessStream);
// draw your image first
ds.DrawImage(image);
// then draw contents of your ink canvas over it
ds.DrawInk(inkCanvas.InkPresenter.StrokeContainer.GetStrokes());
}
randomAccessStream.Dispose();
// save results
using (var fileStream = await file.OpenAsync(FileAccessMode.ReadWrite))
{
await renderTarget.SaveAsync(fileStream, CanvasBitmapFileFormat.Tiff, 1f);
}
This shows how to do the conversion by hand. It requires direct pointer access to the image data. So just create a new 8bpp (i.e. Format8bppIndexed) bitmap with the correct size for the conversion target. So converting the data should look something like this:
public static unsafe void Bgr24ToMono8(byte* source, byte* target, int sourceStride, int targetStride, int width, int height)
{
for(var y = 0; y < height; y++)
{
var sourceRow = source + y * sourceStride;
var targetRow = y * targetStride;
for (int x = 0; x < width; x++)
{
var sourceIndex = (sourceRow + x * 3);
var value = (byte)(sourceIndex[0] * 0.11f + sourceIndex[1] * 0.59f + sourceIndex[2] * 0.3f);
target[targetRow + x] = value;
}
}
}
if you have 32bpp data it should just be a issue of changing x * 3 to x * 4. Note that there is some confusion regarding Bgr vs Rbg, different contexts uses different terms for the same thing.
Note that this converts the bitmap to 8bpp grayscale. If you need 8bpp color indexed this will be much more work since you would ideally need to find a optimal color-map, find the closest colors in said map, and apply dithering to avoid banding. For this I would recommend some image processing library. I do not think there is any built in functions for this, and it is way to much work to demonstrate here.

Merging multiple Images

I'm trying to merge multiple Images into one image. Problem is that most libraries with such functionality are not available in a Windows 8.1 App. I'd prefer to not have to use external libraries such as WriteableBitmapEx
This is my current code which unfortunately doesn't work:
int count = 4;
int size = 150;
WriteableBitmap destination = new WriteableBitmap(300, 300);
BitmapFrame frame = await (await BitmapDecoder.CreateAsync(randomAccessStream)).GetFrameAsync(0);
PixelDataProvider pixelData = await frame.GetPixelDataAsync();
byte[] test = pixelData.DetachPixelData();
MemoryStream mem = new MemoryStream();
for (int row = 0; row < frame.PixelHeight; row++) {
for (int i = 0; i < count; i++)
{
mem.Write(test, row * (int)frame.PixelWidth * 4, (int)frame.PixelWidth * 4);
}
}
mem.Seek(0, SeekOrigin.Begin);
BitmapImage bmp = new BitmapImage();
bmp.SetSourceAsync(mem.AsRandomAccessStream());
If I set the bmp as the source of an Image UIElement nothing happens.
My Idea was to get the Pixeldata as a byte array and to write it line by line (pixel row of each image, so they'd be next to each other) to a memory stream which is then used as the source of the BitmapImage.
Solved
Thanks to Aditya and Romasz I could solve this.
The problem was that I had to encode the pixel data back to an image.
If anyone has the same Problem the following class merges the pixel data of multiple images and returns a BitmapImage:
public class ImageMerger
{
public static async Task<BitmapImage> MergeImages(int singleWidth, int singleHeight, params byte[][] pixelData)
{
int perRow = (int) Math.Ceiling(Math.Sqrt(pixelData.Length));
byte[] mergedImageBytes = new byte[singleHeight * singleWidth * perRow * perRow * 4];
for (int i = 0; i < pixelData.Length; i++ )
{
LoadPixelBytesAt(ref mergedImageBytes, pixelData[i], (i % perRow) * singleWidth, (i / perRow) * singleHeight, perRow * singleWidth, singleWidth, singleHeight);
}
InMemoryRandomAccessStream mem = new InMemoryRandomAccessStream();
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, mem);
encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)(singleHeight * perRow), (uint)(singleWidth * perRow), 91, 91, mergedImageBytes);
await encoder.FlushAsync();
BitmapImage bmp = new BitmapImage();
bmp.SetSourceAsync(mem);
return bmp;
}
private static void LoadPixelBytesAt(ref byte[] dest, byte[] src, int destX, int destY, int destW, int srcW, int srcH)
{
for (int i = 0; i < srcH; i++)
{
for (int j = 0; j < srcW; j++)
{
if (src.Length < ((i * srcW + j + 1) * 4)) return;
for (int p = 0; p < 4; p++)
dest[((destY + i) * destW + destX + j) * 4 + p] = src[(i * srcW + j) * 4 + p];
}
}
}
}
This takes any number of images and puts them next to each other with around as many images from left to right as from top to bottom.
I.e. for 4 images it would return an image with them aligned like this:
1 2
3 4
Works for all of my images but one. There is one image that looks pretty weird after getting merged with others. Didn't figure out why yet.
This should do it :
byte[] PutOnCanvas(byte[] Canvas,byte[] Image,uint x,uint y,uint imageheight,uint imagewidth,uint CanvasWidth)
{
for (uint row = y; row < y+imageheight; row++)
for (uint col = x; col < x+imagewidth; col++)
for (int i = 0; i < 4; i++)
Canvas[(row * CanvasWidth + col) * 4 + i] = Image[((row-y) * imagewidth + (col - x)) * 4 + i];
return Canvas;
}
Now say I want to put two images (pixelbytes in Image1 and Image2) of 30x30 side by side and have a vertical margin of 10px in between them. I would call the function in the following way:
byte[] Canvas = new byte[30 * 70 * 4];
Canvas=PutOnCanvas(Canvas,Image1,0,0,30,30,70);
Canvas=PutOnCanvas(Canvas,Image2,40,0,30,30,70);
Then convert pixel bytes to BMP and you should be done!
Edit:
And this is the correct way to convert pixel bytes to image:
memStream.Size = 0;
var encoder = await BitmapEncoder.CreateAsync(Windows.Graphics.Imaging.BitmapEncoder.JpegEncoderId, memStream);
encoder.SetPixelData(
Windows.Graphics.Imaging.BitmapPixelFormat.Bgra8,
Windows.Graphics.Imaging.BitmapAlphaMode.Straight,
CanvasWidth, // pixel width
CanvasHeight, // pixel height
96, // horizontal DPI
96, // vertical DPI
PixelData);
try { await encoder.FlushAsync(); }
catch { }
memStream.Dispose();
Tried this method awhile ago and it worked for me.
http://www.codeproject.com/Articles/502249/Combineplusseveralplusimagesplustoplusformplusaplu
One option is to draw them in a Canvas like you normally would and then render that Canvas out. The only problem with this is that they must all be on the screen at the same time.
Unfortunately, that's about it as far as simple solutions without something like WriteableBitmapEx goes. Their BitmapContext class abstracts away a lot of the more complex math that goes on when changing an image's width. You can check out WinRTXamlToolkit's blit implementation here, but it has the limitation that the source and destination files must be the same width (due to the annoying math).
One option may be to try and up the size of the images without scaling, hopefully creating some whitespace in the proper spot, then layering them together using a facsimile of that blit implementation, but this seems like it will be a lot of trouble as well.
Your best bet, IMO, is to cut out the chunks of WriteableBitmapEx that you need, specifically their BitmapContext and the Blit Extensions that they provide, then create a blank image and overlay each image onto the destination image (as you are attempting to do now).
This is not legal advice.
WriteableBitmapEx is Microsoft License, which is very permissive, so you should be okay to do this.
Anyway, it'd likely be easier to just add the reference, but if it's necessary that you don't, you can still cut out the parts that you need (in this case) and use them 'a la carte'.

Getting colour values from various positions in a bitmap c#

I have a bitmap and I am wanting to get the colour values from the pixels but only in certain areas of the image. I am wanting to the get the pixels of a image for the full width and only a bit of the height (say height =1) and then I want to move the position to one down and get the same values.
I am using
for (int i = 0; i < 302; i++)
{
Rectangle cloneRect = new Rectangle(0, i, 514, 1);
System.Drawing.Imaging.PixelFormat format = bm.PixelFormat;
Bitmap cloneBitmap = bm.Clone(cloneRect, format);
bitMapList.Add(cloneBitmap);
}
foreach (Bitmap bmp in bitMapList)
{
c = bmp.GetPixel(514, 1);
r = Convert.ToInt16(c.R);
lumi.Add(r);
}
The for statement to create the areas I want on the bitmap and then the foreach to loop through these bitmaps and then get the values. Only problem is I am getting the error message "Parameter must be positive and < Width."
On the line
c = bmp.GetPixel(514, 1);
anyone know why?
Thanks
You need to make sure that the pixel you are getting is inside of the image (which must not be the case). You could wrap this in a call to run a check first something like:
public static Color GetPixelSafe(Bitmap image, int x, int y) {
if (x >= image.Width) x = image.Width - 1;
else if (x < 0) x = 0;
if (y >= image.Height) y = image.Height - 1;
else if (y < 0) y = 0;
return image.GetPixel(x, y);
}
Now, this is not going to fix your processing algorithm itself, but it should at least fix the exception. One other pointer is that if you are going to be processing lots of color values and performance is a concern you should really consider using image.LockBits instead of GetPixel. For more information on that see here: http://msdn.microsoft.com/en-us/library/5ey6h79d(v=vs.110).aspx.
It seems that 514 is bigger then your image actual Width. How did you come up with that number?

Bitmap.Lockbits confusion

MSDN reference: [1] http://msdn.microsoft.com/en-us/library/5ey6h79d.aspx#Y1178
From the link it says that the first argument will "specifies the portion of the Bitmap to lock" which I set to be a smaller part of the Bitmap (Bitmap is 500x500, my rectangle is (0,0,50,50)) however the returned BitmapData has stride of 1500 (=500*3) so basically every scan will still scan through the whole picture horizontally. However, what I want is only the top left 50x50 part of the bitmap.
How does this work out?
The stride will always be of the full bitmap, but the Scan0 property will be different according to the start point of the lock rectangle, as well as the Height and Width of the BitmapData.
The reason for that is that you will still need to know the real bit-width of the bitmap, in order to iterate over the rows (add stride to address).
A simple way to go about it would be:
var bitmap = new Bitmap(100, 100);
var data = bitmap.LockBits(new Rectangle(0, 0, 10, 10),
ImageLockMode.ReadWrite,
bitmap.PixelFormat);
var pt = (byte*)data.Scan0;
var bpp = data.Stride / bitmap.Width;
for (var y = 0; y < data.Height; y++)
{
// This is why real scan-width is important to have!
var row = pt + (y * data.Stride);
for (var x = 0; x < data.Width; x++)
{
var pixel = row + x * bpp;
for (var bit = 0; bit < bpp; bit++)
{
var pixelComponent = pixel[bit];
}
}
}
bitmap.UnlockBits(data);
So it is basically really just locking the whole bitmap, but giving you a pointer to the top-left pixel of the rectangle in the bitmap, and setting the scan's width and height appropriately.

Categories