locked
Convert Image To Bitonal While Maintaining Maximum Readability RRS feed

  • Question

  • I am currently converting and merging multiple images together into tif format using CCITT4 compression.  Also, I have a bitonal method that runs converting each image into 1bpp so that it can use this compression encoding.  However, I am losing the readability of my documents (Most of which are receipts and hand-written forms) after the bitonal conversion.  Is there a better bitonal algorithm to use or a better way to achieve this compression whilst maintaining my readability.  Thank you for your help!

    Below are the two methods for merging the documents and also converting them to bitonal:

    Merge:

        // This will take all of the image files the user has uploaded that have been converted into .tif format and merge them together into a multi-page document
        public int Create_Multi_Frame_Tiff(string str_TempPath, string str_DestinationPath, List<string> list_Files)
        {
          try
          {
            using (System.Drawing.Bitmap oBitmap = ConvertToBitonal(new System.Drawing.Bitmap(list_Files[0])))
            {
              oBitmap.SetResolution(300, 300);
    
              oBitmap.Save(str_TempPath, obj_CodecInfo, obj_EncoderParameters);
    
              obj_EncoderParameters.Param[0] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.FrameDimensionPage);
    
    
              System.Drawing.Bitmap img;
              for (int i = 1; i < list_Files.Count; i++)
              {
                using (img = ConvertToBitonal(new System.Drawing.Bitmap(list_Files[i])))
                {
                  img.SetResolution(300, 300);
                  oBitmap.SaveAdd(img, obj_EncoderParameters);
                }
              }
    
              obj_EncoderParameters.Param[0] = new EncoderParameter(Encoder.SaveFlag, (long)EncoderValue.Flush);
              oBitmap.SaveAdd(obj_EncoderParameters);
    
              //We check to see if we are in debug mode here as the server recognizes the U:\ differently from my own machine. As this is where the my documents area is stored.
              if (WebConfigurationManager.AppSettings["DEBUG"] != "TRUE")
              {
                using (Impersonator imp = new Impersonator())
                {
                  imp.Impersonate();
    
                  File.Copy(str_TempPath, str_DestinationPath, true);
                }
              }
              else
              {
                File.Copy(str_TempPath, str_DestinationPath, true);
              }
    
              obj_EncoderParameters.Dispose();
            }
            
            return 0;        
          }
          catch (Exception ex)
          {
            obj_Email.Send(obj_Email.Error_Email, "Error in Claims_Upload -> Create_Multi_Frame_Tiff()", ex.Message + "<br/><br/>" + ex.StackTrace + "<br/><br/>" + str_TempPath + "<br/><br/>" + str_DestinationPath);
            return -1;
          }
        }

     

    Bitonal Conversion:

        //This will convert a color image into black and white
        public Bitmap ConvertToBitonal(Bitmap original)
        {
          Bitmap source = null;
    
          // If original bitmap is not already in 32 BPP, ARGB format, then convert
          if (original.PixelFormat != System.Drawing.Imaging.PixelFormat.Format32bppArgb)
          {
            source = new Bitmap(original.Width, original.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
            source.SetResolution(original.HorizontalResolution, original.VerticalResolution);
            using (Graphics g = Graphics.FromImage(source))
            {
              g.DrawImageUnscaled(original, 0, 0);
            }
          }
          else
          {
            source = original;
          }
    
          // Lock source bitmap in memory
          BitmapData sourceData = source.LockBits(new System.Drawing.Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
    
          // Copy image data to binary array
          int imageSize = sourceData.Stride * sourceData.Height;
          byte[] sourceBuffer = new byte[imageSize];
          Marshal.Copy(sourceData.Scan0, sourceBuffer, 0, imageSize);
    
          // Unlock source bitmap
          source.UnlockBits(sourceData);
    
          // Create destination bitmap
          Bitmap destination = new Bitmap(source.Width, source.Height, System.Drawing.Imaging.PixelFormat.Format1bppIndexed);
    
          // Lock destination bitmap in memory
          BitmapData destinationData = destination.LockBits(new System.Drawing.Rectangle(0, 0, destination.Width, destination.Height), ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format1bppIndexed);
    
          // Create destination buffer
          imageSize = destinationData.Stride * destinationData.Height;
          byte[] destinationBuffer = new byte[imageSize];
    
          int sourceIndex = 0;
          int destinationIndex = 0;
          int pixelTotal = 0;
          byte destinationValue = 0;
          int pixelValue = 128;
          int height = source.Height;
          int width = source.Width;
          int threshold = 500;
    
          // Iterate lines
          for (int y = 0; y < height; y++)
          {
            sourceIndex = y * sourceData.Stride;
            destinationIndex = y * destinationData.Stride;
            destinationValue = 0;
            pixelValue = 128;
    
            // Iterate pixels
            for (int x = 0; x < width; x++)
            {
              // Compute pixel brightness (i.e. total of Red, Green, and Blue values)
              pixelTotal = sourceBuffer[sourceIndex + 1] + sourceBuffer[sourceIndex + 2] + sourceBuffer[sourceIndex + 3];
              if (pixelTotal > threshold)
              {
                destinationValue += (byte)pixelValue;
              }
              if (pixelValue == 1)
              {
                destinationBuffer[destinationIndex] = destinationValue;
                destinationIndex++;
                destinationValue = 0;
                pixelValue = 128;
              }
              else
              {
                pixelValue >>= 1;
              }
              sourceIndex += 4;
            }
            if (pixelValue != 128)
            {
              destinationBuffer[destinationIndex] = destinationValue;
            }
          }
    
          // Copy binary image data to destination bitmap
          Marshal.Copy(destinationBuffer, 0, destinationData.Scan0, imageSize);
    
          // Unlock destination bitmap
          destination.UnlockBits(destinationData);
    
          original.Dispose();
          source.Dispose();
    
          // Return
          return destination;
        }
    Tuesday, June 1, 2010 9:16 PM

Answers

  • Try this:

        public int Create_Multi_Frame_Tiff(string str_TempPath, string str_DestinationPath, List<string> list_Files)
        {
          
    using (FileStream fs = new FileStream(str_DestinationPath, FileMode.Create))
          {
            
    TiffBitmapEncoder tifEnc = new TiffBitmapEncoder();
            tifEnc.Compression = 
    TiffCompressOption.Ccitt4;
            
    foreach (string fileName in list_Files)
            {
              
    BitmapImage bmpImg = new BitmapImage(new Uri(fileName));
              
    FormatConvertedBitmap fcb = new FormatConvertedBitmap(bmpImg,
                                                                    
    PixelFormats.BlackWhite,
                                                                    
    BitmapPalettes.BlackAndWhite,
                                                                    1.0);
              tifEnc.Frames.Add(
    BitmapFrame.Create(fcb));
            }
            tifEnc.Save(fs);
          }
          
    return 0;
        }

    • Marked as answer by NuNn DaDdY Thursday, June 10, 2010 12:41 PM
    Wednesday, June 2, 2010 10:13 AM

All replies

  • I can't suggest a better algorithm but I can suggest 2 corrections to this one:

       // Compute pixel brightness (i.e. total of Red, Green, and Blue values)
         pixelTotal = sourceBuffer[sourceIndex + 1] + sourceBuffer[sourceIndex + 2] + sourceBuffer[sourceIndex + 3];
         if (pixelTotal > threshold)
    

     The sourceIndex offset are wrong. The order of bytes in memory is BGRA so what you need here is

    sourceBuffer[sourceIndex + 0] + sourceBuffer[sourceIndex + 1] + sourceBuffer[sourceIndex + 2]
    

     Also, simply adding the colors to get the brightness is not exactly right. The human eye has a different sensitivity to each color and you need to adjust for that. The usual proportions are 0.3 red, 0.59 green and 0.11 blue. So:

    (sourceBuffer[sourceIndex + 0] * 11 + sourceBuffer[sourceIndex + 1] * 59 + sourceBuffer[sourceIndex + 2] * 30) / 100

    Not sure if all this would help much. If ,for example, you're input files are grayscale then the above won't matter a bit.

    Tuesday, June 1, 2010 11:16 PM
  • I suggest you attempt to change the colorspace of your RGB bitmap to YCrCb and use the  luminance component (Y). Luminence is a measure of intensity and I think the results of thresholding this component will produce a higher quality bitonal. Below are the equations from CCIR 601, but all you need is the Y component, don't worry about the color one YCrCb.

    Y   =    0.299 · R +  0.587 · G +   0.114 · B 

    Cb  =   -0.1687 · R - 0.3313 · G +   0.114 · B + 128

    Cr  =       0.5 · R - 0.4148 · G  - 0.0813 · B + 128

    For more information I recommend the excellent colorspace FAQ by Charles A. Poynton at http://www.poynton.com/ColorFAQ.html.

    -joe-

     

    Wednesday, June 2, 2010 5:51 AM
  • Try this:

        public int Create_Multi_Frame_Tiff(string str_TempPath, string str_DestinationPath, List<string> list_Files)
        {
          
    using (FileStream fs = new FileStream(str_DestinationPath, FileMode.Create))
          {
            
    TiffBitmapEncoder tifEnc = new TiffBitmapEncoder();
            tifEnc.Compression = 
    TiffCompressOption.Ccitt4;
            
    foreach (string fileName in list_Files)
            {
              
    BitmapImage bmpImg = new BitmapImage(new Uri(fileName));
              
    FormatConvertedBitmap fcb = new FormatConvertedBitmap(bmpImg,
                                                                    
    PixelFormats.BlackWhite,
                                                                    
    BitmapPalettes.BlackAndWhite,
                                                                    1.0);
              tifEnc.Frames.Add(
    BitmapFrame.Create(fcb));
            }
            tifEnc.Save(fs);
          }
          
    return 0;
        }

    • Marked as answer by NuNn DaDdY Thursday, June 10, 2010 12:41 PM
    Wednesday, June 2, 2010 10:13 AM
  • Hello again,

    Thank you for your replies, so far I have only had a chance to implement John's suggestion but I will be trying the others shortly. 

     

    John:  I was able to successfully implement your method and the result image appeared to be correct.  However, the program we use to view these files is unable to render the images, does your revisions account for 1bpp?

     

    Thanks again!

    Wednesday, June 2, 2010 2:50 PM
  • John:  I was able to successfully implement your method and the result image appeared to be correct.  However, the program we use to view these files is unable to render the images, does your revisions account for 1bpp?

    By 1bpp, I assume you mean different colors for the black and white.  To change the colors, you'll have to change the PixelFormat to Indexed1 and supply your palette.
    Wednesday, June 2, 2010 3:11 PM
  • John:  I was able to successfully implement your method and the result image appeared to be correct.  However, the program we use to view these files is unable to render the images, does your revisions account for 1bpp?

    By 1bpp, I assume you mean different colors for the black and white.  To change the colors, you'll have to change the PixelFormat to Indexed1 and supply your palette.


    Hello again,

    Sorry for being vague, with the bpp I meant 1 bit per pixel

    CORRECTION:  After looking at the parameters more in depth for the FormatConvertedBitmap I see that the PixelFormats.BlackWhite achieves this...Now to find out why the darn thing isn't showing up properly in the program that opens the image file.

    Wednesday, June 2, 2010 4:32 PM
  • John:  After working with the code you have provided I have noticed that I often receive IO errors in my Clean_Up method when I attempting to remove my temporary image files that I send to your method.  It appears that it thinks that one or more of the files are still in use; is there anything to add to your code to ensure that the file locks are removed (although I cannot find a way to displose the BitmapImage as it doesn't use this method).  Would I want to use bmpImg.BeginInit() and EndInit()?  Thank you for your help.

    Tuesday, June 8, 2010 1:49 PM
  •   Would I want to use bmpImg.BeginInit() and EndInit()? 
    I'll see what I can do about releasing any locks on the files.  I don't understand why you would want to initialize the BitmapImage in a BeginInit ... EndInit block rather than in the constructor.  Are you having other problems?
    Tuesday, June 8, 2010 2:48 PM
  • Hello again,  nevermind the BeginInit and EndInit as I had thought they were something else but after looking at the documentation I was wrong.  As for other problems, the only other is with jpeg files after they are converted but I believe this is with the 3rd party software I am using to house the images as newer versions of the software appear to be working correctly.  I think that the locking files issue is the only item I need resolved.  Thank you so much for you help.
    Tuesday, June 8, 2010 3:22 PM
  • You were right.  You have to use BeginInit and EndInit in order to set the BitmapCacheOption to OnLoad instead of the default OnDemand.  OnDemand locks the file.

              BitmapImage bmpImg = new BitmapImage();
              bmpImg.BeginInit();
              bmpImg.CacheOption = 
    BitmapCacheOption.OnLoad;
              bmpImg.UriSource = 
    new Uri(fileName);
              bmpImg.EndInit();

    Tuesday, June 8, 2010 5:31 PM
  • Thank you for your help John.  Everything appears to be working great now :)
    Thursday, June 10, 2010 12:41 PM