【问题标题】:Automatic enhancement of scanned images扫描图像的自动增强
【发布时间】:2025-12-05 03:35:01
【问题描述】:

我正在开发一个用于自动增强扫描的 35 毫米幻灯片的例程。我正在寻找一种很好的算法来增加对比度和消除偏色。该算法必须是完全自动化的,因为需要处理数千张图像。这些是直接来自扫描仪的几个示例图像,仅针对网络进行了裁剪和缩小:

我正在使用 AForge.NET 库并尝试了 HistogramEqualizationContrastStretch 过滤器。 HistogramEqualization 有利于最大化局部对比度,但总体上不会产生令人满意的结果。 ContrastStretch 更好,但由于它单独拉伸每个色带的直方图,因此有时会产生强烈的色偏:

为了减少颜色偏移,我使用ImageStatisticsLevelsLinear 类自己创建了一个UniformContrastStretch 过滤器。这对所有色带使用相同的范围,以降低对比度为代价保留颜色。

    ImageStatistics stats = new ImageStatistics(image);
    int min = Math.Min(Math.Min(stats.Red.Min, stats.Green.Min), stats.Blue.Min);
    int max = Math.Max(Math.Max(stats.Red.Max, stats.Green.Max), stats.Blue.Max);
    LevelsLinear levelsLinear = new LevelsLinear();
    levelsLinear.Input = new IntRange(min, max);
    Bitmap stretched = levelsLinear.Apply(image);

虽然图像仍然很蓝,所以我创建了一个ColorCorrection 过滤器,它首先计算图像的平均亮度。然后为每个颜色通道计算伽马校正值,以便每个颜色通道的平均值等于平均亮度。均匀对比度拉伸图像的平均值为R=70 G=64 B=93,平均亮度为(70 + 64 + 93) / 3 = 76。伽马值计算为R=1.09 G=1.18 B=0.80,结果非常中性,图像的平均值为R=76 G=76 B=76,正如预期的那样:

现在,解决真正的问题...我想将图像的平均颜色校正为灰色有点过于激烈,并且会使某些图像看起来很暗淡,例如第二个示例(第一张图片是均匀拉伸的,下一张是相同的图片颜色校正):

在照片编辑程序中手动执行颜色校正的一种方法是对已知中性色(白色/灰色/黑色)的颜色进行采样,然后将图像的其余部分调整为该颜色。但由于这个程序必须是完全自动的,所以这不是一种选择。

我想我可以为我的ColorCorrection 滤镜添加一个强度设置,这样强度为 0.5 会将平均值移动到平均亮度的一半距离。但另一方面,有些图像可能在没有任何颜色校正的情况下表现最佳。

对于更好的算法有什么想法吗?或者一些方法来检测图像是否有偏色或只是有很多颜色,比如第二个样本?

【问题讨论】:

  • 如果图像最亮的部分(没有剪裁)是中性色,您可能不需要应用颜色校正。这会捕捉到你的船形象。
  • @MarkRansom,这是个好主意。我肯定会排除在所有通道(255、255、255)中被剪裁的颜色,但您认为排除仅在一个通道中被剪裁的颜色(如(244、249、255)是个好主意吗?
  • 如果其他通道在适当的范围内,则仅在一个通道中被裁剪的颜色将是色偏的良好证据。想象一张玫瑰的照片,它可以很容易地夹在红色通道上,但相比之下其他通道仍然会很暗。其他通道需要足够高才能被视为接近白色,但要足够低以表明有色偏。
  • 是的,当然,这是有道理的。也使编码更容易。 :-)
  • 我将从 Gasparini 和 Schettini 的“使用简单图像统计的数码照片色彩平衡”开始(如果您有访问权限,请访问 sciencedirect.com/science/article/pii/S0031320304000068,否则可以找到质量较低的版本使用搜索引擎),不一定是新的,但更强大。与任何论文一样,它还快速描述了相关研究和与该问题相关的问题。

标签: c# image algorithm image-processing aforge


【解决方案1】:

为避免在拉伸对比度时改变图像的颜色,请先将其转换为 HSV/HSL 颜色空间。然后,在 L 或 V 通道中应用常规的对比拉伸,但不要改变 H 或 S 通道。

【讨论】:

    【解决方案2】:
    • 翻译成hsv
    • 通过将值从 (min,max) 范围缩放到 (0,255) 范围来校正 V 层
    • 组装回 rgb
    • 通过与第二步的 V 层相同的想法来校正 R、G、B 层的结果

    没有aforge.net 代码,因为它是由php 原型代码处理的,但是afaik 用aforge.net 这样做没有任何问题。 结果是:

    【讨论】:

      【解决方案3】:

      使用以下方法将您的 RGB 转换为 HSL:

          System.Drawing.Color color = System.Drawing.Color.FromArgb(red, green, blue);
          float hue = color.GetHue();
          float saturation = color.GetSaturation();
          float lightness = color.GetBrightness();
      

      相应地调整饱和度和亮度

      通过以下方式将 HSL 转换回 RGB:

      /// <summary>
      /// Convert HSV to RGB
      /// h is from 0-360
      /// s,v values are 0-1
      /// r,g,b values are 0-255
      /// Based upon http://ilab.usc.edu/wiki/index.php/HSV_And_H2SV_Color_Space#HSV_Transformation_C_.2F_C.2B.2B_Code_2
      /// </summary>
      void HsvToRgb(double h, double S, double V, out int r, out int g, out int b)
      {
        // ######################################################################
        // T. Nathan Mundhenk
        // mundhenk@usc.edu
        // C/C++ Macro HSV to RGB
      
        double H = h;
        while (H < 0) { H += 360; };
        while (H >= 360) { H -= 360; };
        double R, G, B;
        if (V <= 0)
          { R = G = B = 0; }
        else if (S <= 0)
        {
          R = G = B = V;
        }
        else
        {
          double hf = H / 60.0;
          int i = (int)Math.Floor(hf);
          double f = hf - i;
          double pv = V * (1 - S);
          double qv = V * (1 - S * f);
          double tv = V * (1 - S * (1 - f));
          switch (i)
          {
      
            // Red is the dominant color
      
            case 0:
              R = V;
              G = tv;
              B = pv;
              break;
      
            // Green is the dominant color
      
            case 1:
              R = qv;
              G = V;
              B = pv;
              break;
            case 2:
              R = pv;
              G = V;
              B = tv;
              break;
      
            // Blue is the dominant color
      
            case 3:
              R = pv;
              G = qv;
              B = V;
              break;
            case 4:
              R = tv;
              G = pv;
              B = V;
              break;
      
            // Red is the dominant color
      
            case 5:
              R = V;
              G = pv;
              B = qv;
              break;
      
            // Just in case we overshoot on our math by a little, we put these here. Since its a switch it won't slow us down at all to put these here.
      
            case 6:
              R = V;
              G = tv;
              B = pv;
              break;
            case -1:
              R = V;
              G = pv;
              B = qv;
              break;
      
            // The color is not defined, we should throw an error.
      
            default:
              //LFATAL("i Value error in Pixel conversion, Value is %d", i);
              R = G = B = V; // Just pretend its black/white
              break;
          }
        }
        r = Clamp((int)(R * 255.0));
        g = Clamp((int)(G * 255.0));
        b = Clamp((int)(B * 255.0));
      }
      
      /// <summary>
      /// Clamp a value to 0-255
      /// </summary>
      int Clamp(int i)
      {
        if (i < 0) return 0;
        if (i > 255) return 255;
        return i;
      }
      

      原代码:

      【讨论】:

        【解决方案4】:

        我需要对一个庞大的视频缩略图库做同样的事情。我想要一个保守的解决方案,这样我就不必抽查缩略图是否完全被破坏了。这是我使用的杂乱无章的解决方案。

        我首先使用这个类来计算图像中的颜色分布。我首先在 HSV 色彩空间中做了一个,但发现基于灰度的一个更快,而且几乎一样好:

        class GrayHistogram
          def initialize(filename)
            @hist = hist(filename)
            @percentile = {}
          end
        
          def percentile(x)
            return @percentile[x] if @percentile[x]
            bin = @hist.find{ |h| h[:count] > x }
            c = bin[:color]
            return @percentile[x] ||= c/256.0
          end
        
          def midpoint
            (percentile(0.25) + percentile(0.75)) / 2.0
          end
        
          def spread
            percentile(0.75) - percentile(0.25)
          end
        
        private
          def hist(imgFilename)
            histFilename = "/tmp/gray_hist.txt"
        
            safesystem("convert #{imgFilename} -depth 8 -resize 50% -colorspace GRAY /tmp/out.png")
            safesystem("convert /tmp/out.png -define histogram:unique-colors=true " +
                       "        -format \"%c\" histogram:info:- > #{histFilename}")
        
            f = File.open(histFilename)
            lines = f.readlines[0..-2] # the last line is always blank
            hist = lines.map { |line| { :count => /([0-9]*):/.match(line)[1].to_i, :color => /,([0-9]*),/.match(line)[1].to_i } }
            f.close
        
            tot = 0
            cumhist = hist.map do |h|
              tot += h[:count]
              {:count=>tot, :color=>h[:color]}
            end
            tot = tot.to_f
            cumhist.each { |h| h[:count] = h[:count] / tot }
        
            safesystem("rm /tmp/out.png #{histFilename}")
        
            return cumhist
          end
        end
        

        然后我创建了这个类来使用直方图来确定如何校正图像:

        def safesystem(str)
          out = `#{str}`
          if $? != 0
            puts "shell command failed:"
            puts "\tcmd: #{str}"
            puts "\treturn code: #{$?}"
            puts "\toutput: #{out}"
            raise
          end
        end
        
        def generateHist(thumb, hist)
          safesystem("convert #{thumb} histogram:hist.jpg && mv hist.jpg #{hist}")
        end
        
        class ImgCorrector
          def initialize(filename)
            @filename = filename
            @grayHist = GrayHistogram.new(filename)
          end
        
          def flawClass
            if !@flawClass
              gapLeft  = (@grayHist.percentile(0.10) > 0.13) || (@grayHist.percentile(0.25) > 0.30)
              gapRight = (@grayHist.percentile(0.75) < 0.60) || (@grayHist.percentile(0.90) < 0.80)
        
              return (@flawClass="low"   ) if (!gapLeft &&  gapRight)
              return (@flawClass="high"  ) if ( gapLeft && !gapRight)
              return (@flawClass="narrow") if ( gapLeft &&  gapRight)
              return (@flawClass="fine"  )
            end
            return @flawClass
          end
        
          def percentileSummary
            [ @grayHist.percentile(0.10),
              @grayHist.percentile(0.25),
              @grayHist.percentile(0.75),
              @grayHist.percentile(0.90) ].map{ |x| (((x*100.0*10.0).round)/10.0).to_s }.join(', ') +
            "<br />" +
            "spread: " + @grayHist.spread.to_s
          end
        
          def writeCorrected(filenameOut)
            if flawClass=="fine"
              safesystem("cp #{@filename} #{filenameOut}")
              return
            end
        
            # spread out the histogram, centered at the midpoint
            midpt = 100.0*@grayHist.midpoint
        
            # map the histogram's spread to a sigmoidal concept (linearly)
            minSpread = 0.10
            maxSpread = 0.60
            minS = 1.0
            maxS = case flawClass
              when "low"    then 5.0
              when "high"   then 5.0
              when "narrow" then 6.0
            end
            s = ((1.0 - [[(@grayHist.spread - minSpread)/(maxSpread-minSpread), 0.0].max, 1.0].min) * (maxS - minS)) + minS
        
            #puts "s: #{s}"
            safesystem("convert #{@filename} -sigmoidal-contrast #{s},#{midpt}% #{filenameOut}")
          end
        end
        

        我是这样运行的:

        origThumbs = `find thumbs | grep jpg`.split("\n")
        origThumbs.each do |origThumb|
          newThumb = origThumb.gsub(/thumb/, "newthumb")
          imgCorrector = ImgCorrector.new(origThumb)
          imgCorrector.writeCorrected(newThumb)
        end
        

        【讨论】:

          【解决方案5】:

          您可以通过此链接尝试自动亮度和对比度:http://answers.opencv.org/question/75510/how-to-make-auto-adjustmentsbrightness-and-contrast-for-image-android-opencv-image-correction/

          void Utils::BrightnessAndContrastAuto(const cv::Mat &src, cv::Mat &dst, float clipHistPercent)
          {
          
              CV_Assert(clipHistPercent >= 0);
              CV_Assert((src.type() == CV_8UC1) || (src.type() == CV_8UC3) || (src.type() == CV_8UC4));
          
              int histSize = 256;
              float alpha, beta;
              double minGray = 0, maxGray = 0;
          
              //to calculate grayscale histogram
              cv::Mat gray;
              if (src.type() == CV_8UC1) gray = src;
              else if (src.type() == CV_8UC3) cvtColor(src, gray, CV_BGR2GRAY);
              else if (src.type() == CV_8UC4) cvtColor(src, gray, CV_BGRA2GRAY);
              if (clipHistPercent == 0)
              {
                  // keep full available range
                  cv::minMaxLoc(gray, &minGray, &maxGray);
              }
              else
              {
                  cv::Mat hist; //the grayscale histogram
          
                  float range[] = { 0, 256 };
                  const float* histRange = { range };
                  bool uniform = true;
                  bool accumulate = false;
                  calcHist(&gray, 1, 0, cv::Mat(), hist, 1, &histSize, &histRange, uniform, accumulate);
          
                  // calculate cumulative distribution from the histogram
                  std::vector<float> accumulator(histSize);
                  accumulator[0] = hist.at<float>(0);
                  for (int i = 1; i < histSize; i++)
                  {
                      accumulator[i] = accumulator[i - 1] + hist.at<float>(i);
                  }
          
                  // locate points that cuts at required value
                  float max = accumulator.back();
                  clipHistPercent *= (max / 100.0); //make percent as absolute
                  clipHistPercent /= 2.0; // left and right wings
                  // locate left cut
                  minGray = 0;
                  while (accumulator[minGray] < clipHistPercent)
                      minGray++;
          
                  // locate right cut
                  maxGray = histSize - 1;
                  while (accumulator[maxGray] >= (max - clipHistPercent))
                      maxGray--;
              }
          
              // current range
              float inputRange = maxGray - minGray;
          
              alpha = (histSize - 1) / inputRange;   // alpha expands current range to histsize range
              beta = -minGray * alpha;             // beta shifts current range so that minGray will go to 0
          
              // Apply brightness and contrast normalization
              // convertTo operates with saurate_cast
              src.convertTo(dst, -1, alpha, beta);
          
              // restore alpha channel from source 
              if (dst.type() == CV_8UC4)
              {
                  int from_to[] = { 3, 3 };
                  cv::mixChannels(&src, 4, &dst, 1, from_to, 1);
              }
              return;
          }
          

          或从此链接应用自动色彩平衡:http://www.morethantechnical.com/2015/01/14/simplest-color-balance-with-opencv-wcode/

          void Utils::SimplestCB(Mat& in, Mat& out, float percent) {
              assert(in.channels() == 3);
              assert(percent > 0 && percent < 100);
          
              float half_percent = percent / 200.0f;
          
              vector<Mat> tmpsplit; split(in, tmpsplit);
              for (int i = 0; i < 3; i++) {
                  //find the low and high precentile values (based on the input percentile)
                  Mat flat; tmpsplit[i].reshape(1, 1).copyTo(flat);
                  cv::sort(flat, flat, CV_SORT_EVERY_ROW + CV_SORT_ASCENDING);
                  int lowval = flat.at<uchar>(cvFloor(((float)flat.cols) * half_percent));
                  int highval = flat.at<uchar>(cvCeil(((float)flat.cols) * (1.0 - half_percent)));
                  cout << lowval << " " << highval << endl;
          
                  //saturate below the low percentile and above the high percentile
                  tmpsplit[i].setTo(lowval, tmpsplit[i] < lowval);
                  tmpsplit[i].setTo(highval, tmpsplit[i] > highval);
          
                  //scale the channel
                  normalize(tmpsplit[i], tmpsplit[i], 0, 255, NORM_MINMAX);
              }
              merge(tmpsplit, out);
          }
          

          或将 CLAHE 应用于 BGR 图像

          【讨论】:

            最近更新 更多