自适应直方图均衡(CLAHE) 代码及详细注释【OpenCV】

举报
ShaderJoy 发表于 2021/12/30 01:45:42 2021/12/30
【摘要】    效果图   标题   CLAHE简介        HE 直方图增强,大家都不陌生,是一种比较古老的对比度增强算法,它有两种变体:AHE 和 CLAHE;两者都是自适应的增强算法,功能差不多,但是前...

 

 效果图

 

标题

 

CLAHE简介

       HE 直方图增强,大家都不陌生,是一种比较古老的对比度增强算法,它有两种变体:AHE 和 CLAHE;两者都是自适应的增强算法,功能差不多,但是前者有一个很大的缺陷,就是有时候会过度放大图像中相同区域的噪声,为了解决这一问题,出现了 HE 的另一种改进算法,就是 CLAHE;CLAHE 是另外一种直方图均衡算法,CLAHE 和 AHE 的区别在于前者对区域对比度实行了限制,并且利用插值来加快计算。它能有效的增强或改善图像(局部)对比度,从而获取更多图像相关边缘信息有利于分割。还能够有效改善 AHE 中放大噪声的问题。另外,CLAHE 的有一个用途是被用来对图像去雾。

详细理论请参考博客 

OpenCV源码的本地路径: %OPENCV%\opencv\sources\modules\imgproc\src\clahe.cpp

clahe.cpp


  
  1. // ----------------------------------------------------------------------
  2. // CLAHE
  3. namespace
  4. {
  5. class CLAHE_CalcLut_Body : public cv::ParallelLoopBody
  6. {
  7. public:
  8. CLAHE_CalcLut_Body(const cv::Mat& src, cv::Mat& lut, cv::Size tileSize, int tilesX, int clipLimit, float lutScale) :
  9. src_(src), lut_(lut), tileSize_(tileSize), tilesX_(tilesX), clipLimit_(clipLimit), lutScale_(lutScale)
  10. {
  11. }
  12. void operator ()(const cv::Range& range) const;
  13. private:
  14. cv::Mat src_;
  15. mutable cv::Mat lut_;
  16. cv::Size tileSize_;
  17. int tilesX_;
  18. int clipLimit_;
  19. float lutScale_;
  20. };
  21. // 计算直方图查找表
  22. void CLAHE_CalcLut_Body::operator ()(const cv::Range& range) const
  23. {
  24. const int histSize = 256;
  25. uchar* tileLut = lut_.ptr(range.start);
  26. const size_t lut_step = lut_.step; // size = tilesX_*tilesY_ * lut_step
  27. // Range(0, tilesX_ * tilesY_),全图被分为tilesX_*tiles_Y个块
  28. for (int k = range.start; k < range.end; ++k, tileLut += lut_step)
  29. {
  30. // (tx, ty)表示当前所在是哪一块
  31. // (0, 0) (1, 0)...(tilesX_-1, 0)
  32. // (0, 1) (1, 1)...(tilesX_-1, 1)
  33. // ...
  34. // (0, tilesY_-1)... (tilesX_-1, tilesY_-1)
  35. const int ty = k / tilesX_;
  36. const int tx = k % tilesX_;
  37. // retrieve tile submatrix
  38. // 注意:tileSize.width表示分块的宽度,tileSize.height表示分块高度
  39. cv::Rect tileROI;
  40. tileROI.x = tx * tileSize_.width; // 换算为全局坐标
  41. tileROI.y = ty * tileSize_.height;
  42. tileROI.width = tileSize_.width;
  43. tileROI.height = tileSize_.height;
  44. const cv::Mat tile = src_(tileROI);
  45. // calc histogram
  46. int tileHist[histSize] = { 0, };
  47. // 统计 ROI 的直方图
  48. int height = tileROI.height;
  49. const size_t sstep = tile.step;
  50. for (const uchar* ptr = tile.ptr<uchar>(0); height--; ptr += sstep)
  51. {
  52. int x = 0;
  53. for (; x <= tileROI.width - 4; x += 4)
  54. {
  55. int t0 = ptr[x], t1 = ptr[x + 1];
  56. tileHist[t0]++; tileHist[t1]++;
  57. t0 = ptr[x + 2]; t1 = ptr[x + 3];
  58. tileHist[t0]++; tileHist[t1]++;
  59. }
  60. for (; x < tileROI.width; ++x)
  61. tileHist[ptr[x]]++;
  62. }
  63. // clip histogram
  64. if (clipLimit_ > 0)
  65. {
  66. // how many pixels were clipped
  67. int clipped = 0;
  68. for (int i = 0; i < histSize; ++i)
  69. {
  70. // 超过裁剪阈值
  71. if (tileHist[i] > clipLimit_)
  72. {
  73. clipped += tileHist[i] - clipLimit_;
  74. tileHist[i] = clipLimit_;
  75. }
  76. }
  77. // redistribute clipped pixels
  78. int redistBatch = clipped / histSize;
  79. int residual = clipped - redistBatch * histSize;
  80. // 平均分配裁剪的差值到所有直方图
  81. for (int i = 0; i < histSize; ++i)
  82. tileHist[i] += redistBatch;
  83. // 处理差值的余数
  84. for (int i = 0; i < residual; ++i)
  85. tileHist[i]++;
  86. }
  87. // calc Lut
  88. int sum = 0;
  89. for (int i = 0; i < histSize; ++i)
  90. {
  91. // 累加直方图
  92. sum += tileHist[i];
  93. tileLut[i] = cv::saturate_cast<uchar>(sum * lutScale_); // static_cast<float>(histSize - 1) / tileSizeTotal
  94. }
  95. }
  96. }
  97. class CLAHE_Interpolation_Body : public cv::ParallelLoopBody
  98. {
  99. public:
  100. CLAHE_Interpolation_Body(const cv::Mat& src, cv::Mat& dst, const cv::Mat& lut, cv::Size tileSize, int tilesX, int tilesY) :
  101. src_(src), dst_(dst), lut_(lut), tileSize_(tileSize), tilesX_(tilesX), tilesY_(tilesY)
  102. {
  103. }
  104. void operator ()(const cv::Range& range) const;
  105. private:
  106. cv::Mat src_;
  107. mutable cv::Mat dst_;
  108. cv::Mat lut_;
  109. cv::Size tileSize_;
  110. int tilesX_;
  111. int tilesY_;
  112. };
  113. // 根据相邻4块的直方图插值
  114. void CLAHE_Interpolation_Body::operator ()(const cv::Range& range) const
  115. {
  116. const size_t lut_step = lut_.step;
  117. // Range(0, src.rows)
  118. for (int y = range.start; y < range.end; ++y)
  119. {
  120. const uchar* srcRow = src_.ptr<uchar>(y);
  121. uchar* dstRow = dst_.ptr<uchar>(y);
  122. const float tyf = (static_cast<float>(y) / tileSize_.height) - 0.5f;
  123. int ty1 = cvFloor(tyf);
  124. int ty2 = ty1 + 1;
  125. // 差值作为插值的比例
  126. const float ya = tyf - ty1;
  127. ty1 = std::max(ty1, 0);
  128. ty2 = std::min(ty2, tilesY_ - 1);
  129. const uchar* lutPlane1 = lut_.ptr(ty1 * tilesX_); // 当前块的直方图
  130. const uchar* lutPlane2 = lut_.ptr(ty2 * tilesX_); // 向下一块的直方图
  131. for (int x = 0; x < src_.cols; ++x)
  132. {
  133. const float txf = (static_cast<float>(x) / tileSize_.width) - 0.5f;
  134. int tx1 = cvFloor(txf);
  135. int tx2 = tx1 + 1;
  136. // 差值作为插值的比例
  137. const float xa = txf - tx1;
  138. tx1 = std::max(tx1, 0);
  139. tx2 = std::min(tx2, tilesX_ - 1);
  140. // src_.ptr<uchar>(y)[x]
  141. const int srcVal = srcRow[x];
  142. // 索引 LUT
  143. const size_t ind1 = tx1 * lut_step + srcVal;
  144. const size_t ind2 = tx2 * lut_step + srcVal; // 向右一块的直方图
  145. float res = 0;
  146. // 根据直方图的值进行插值
  147. // lut_.ptr(ty1 * tilesX_)[tx1 * lut_step + srcVa] => lut_[ty1][tx1][srcVal]
  148. res += lutPlane1[ind1] * ((1.0f - xa) * (1.0f - ya));
  149. res += lutPlane1[ind2] * ((xa) * (1.0f - ya));
  150. res += lutPlane2[ind1] * ((1.0f - xa) * (ya));
  151. res += lutPlane2[ind2] * ((xa) * (ya));
  152. dstRow[x] = cv::saturate_cast<uchar>(res);
  153. }
  154. }
  155. }
  156. class CLAHE_Impl : public cv::CLAHE
  157. {
  158. public:
  159. CLAHE_Impl(double clipLimit = 40.0, int tilesX = 8, int tilesY = 8);
  160. cv::AlgorithmInfo* info() const; // Algorithm类工厂方法封装相关
  161. void apply(cv::InputArray src, cv::OutputArray dst);
  162. void setClipLimit(double clipLimit);
  163. double getClipLimit() const;
  164. void setTilesGridSize(cv::Size tileGridSize);
  165. cv::Size getTilesGridSize() const;
  166. void collectGarbage();
  167. private:
  168. double clipLimit_;
  169. int tilesX_;
  170. int tilesY_;
  171. cv::Mat srcExt_;
  172. cv::Mat lut_;
  173. };
  174. CLAHE_Impl::CLAHE_Impl(double clipLimit, int tilesX, int tilesY) :
  175. clipLimit_(clipLimit), tilesX_(tilesX), tilesY_(tilesY)
  176. {
  177. }
  178. // Algorithm类工厂方法封装相关
  179. //CV_INIT_ALGORITHM(CLAHE_Impl, "CLAHE",
  180. // obj.info()->addParam(obj, "clipLimit", obj.clipLimit_);
  181. //obj.info()->addParam(obj, "tilesX", obj.tilesX_);
  182. //obj.info()->addParam(obj, "tilesY", obj.tilesY_))
  183. void CLAHE_Impl::apply(cv::InputArray _src, cv::OutputArray _dst)
  184. {
  185. cv::Mat src = _src.getMat();
  186. CV_Assert(src.type() == CV_8UC1);
  187. _dst.create(src.size(), src.type());
  188. cv::Mat dst = _dst.getMat();
  189. const int histSize = 256;
  190. // 准备 LUT,tilesX_*tilesY_个块,每个块都有256个柱子的直方图
  191. lut_.create(tilesX_ * tilesY_, histSize, CV_8UC1);
  192. cv::Size tileSize;
  193. cv::Mat srcForLut;
  194. // 如果分块刚好(整除)
  195. if (src.cols % tilesX_ == 0 && src.rows % tilesY_ == 0)
  196. {
  197. tileSize = cv::Size(src.cols / tilesX_, src.rows / tilesY_);
  198. srcForLut = src;
  199. }
  200. // 否则对原图进行扩充
  201. else
  202. {
  203. cv::copyMakeBorder(src, srcExt_, 0, tilesY_ - (src.rows % tilesY_), 0, tilesX_ - (src.cols % tilesX_), cv::BORDER_REFLECT_101);
  204. tileSize = cv::Size(srcExt_.cols / tilesX_, srcExt_.rows / tilesY_);
  205. srcForLut = srcExt_;
  206. }
  207. const int tileSizeTotal = tileSize.area();
  208. const float lutScale = static_cast<float>(histSize - 1) / tileSizeTotal; // △
  209. // 计算实际的clipLimit
  210. int clipLimit = 0;
  211. if (clipLimit_ > 0.0)
  212. {
  213. clipLimit = static_cast<int>(clipLimit_ * tileSizeTotal / histSize);
  214. clipLimit = std::max(clipLimit, 1);
  215. }
  216. // 分块并行计算: LUT
  217. CLAHE_CalcLut_Body calcLutBody(srcForLut, lut_, tileSize, tilesX_, clipLimit, lutScale);
  218. cv::parallel_for_(cv::Range(0, tilesX_ * tilesY_), calcLutBody);
  219. // 分块并行计算: 根据直方图插值
  220. CLAHE_Interpolation_Body interpolationBody(src, dst, lut_, tileSize, tilesX_, tilesY_);
  221. cv::parallel_for_(cv::Range(0, src.rows), interpolationBody);
  222. }
  223. void CLAHE_Impl::setClipLimit(double clipLimit)
  224. {
  225. clipLimit_ = clipLimit;
  226. }
  227. double CLAHE_Impl::getClipLimit() const
  228. {
  229. return clipLimit_;
  230. }
  231. void CLAHE_Impl::setTilesGridSize(cv::Size tileGridSize)
  232. {
  233. tilesX_ = tileGridSize.width;
  234. tilesY_ = tileGridSize.height;
  235. }
  236. cv::Size CLAHE_Impl::getTilesGridSize() const
  237. {
  238. return cv::Size(tilesX_, tilesY_);
  239. }
  240. void CLAHE_Impl::collectGarbage()
  241. {
  242. srcExt_.release();
  243. lut_.release();
  244. }
  245. }
  246. cv::Ptr<cv::CLAHE> cv::createCLAHE(double clipLimit, cv::Size tileGridSize)
  247. {
  248. return new CLAHE_Impl(clipLimit, tileGridSize.width, tileGridSize.height);
  249. }


main.cpp


  
  1. int main(int argc, char** argv)
  2. {
  3. cv::Mat inp_img = cv::imread("D:/Pictures/beard.jpg");
  4. if (!inp_img.data) {
  5. cout << "Something Wrong";
  6. return -1;
  7. }
  8. namedWindow("Input Image", CV_WINDOW_AUTOSIZE);
  9. cv::imshow("Input Image", inp_img);
  10. cv::Mat clahe_img;
  11. cv::cvtColor(inp_img, clahe_img, CV_BGR2Lab);
  12. std::vector<cv::Mat> channels(3);
  13. cv::split(clahe_img, channels);
  14. cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE();
  15. // 直方图的柱子高度大于计算后的ClipLimit的部分被裁剪掉,然后将其平均分配给整张直方图
  16. // 从而提升整个图像
  17. clahe->setClipLimit(4.); // (int)(4.*(8*8)/256)
  18. //clahe->setTilesGridSize(Size(8, 8)); // 将图像分为8*8块
  19. cv::Mat dst;
  20. clahe->apply(channels[0], dst);
  21. dst.copyTo(channels[0]);
  22. cv::merge(channels, clahe_img);
  23. cv::Mat image_clahe;
  24. cv::cvtColor(clahe_img, image_clahe, CV_Lab2BGR);
  25. //cout << cvFloor(-1.5) << endl;
  26. namedWindow("CLAHE Image", CV_WINDOW_AUTOSIZE);
  27. cv::imshow("CLAHE Image", image_clahe);
  28. imwrite("out.jpg", image_clahe);
  29. cv::waitKey(0);
  30. destroyAllWindows();
  31. return 0;
  32. }

 

注意:cv::ParallelLoopBody 位于 %OpenCV%\opencv\sources\modules\core\src\parallel.cpp

延伸阅读:Algorithm类工厂方法封装相关

 

文章来源: panda1234lee.blog.csdn.net,作者:panda1234lee,版权归原作者所有,如需转载,请联系作者。

原文链接:panda1234lee.blog.csdn.net/article/details/52852765

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。