From: Cristy Date: Thu, 22 Nov 2018 00:13:46 +0000 (-0500) Subject: add support for the -clahe option: contrast limited adaptive histogram equalization... X-Git-Tag: 7.0.8-15~65 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=9f25254459f8a3d8dcc1669467c48539d78a23b6;p=imagemagick add support for the -clahe option: contrast limited adaptive histogram equalization (note it does not work yet!) --- diff --git a/ChangeLog b/ChangeLog index c7019ed71..7c7d802f5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2018-11-21 7.0.8-15 Cristy + * added support for the -clahe option: contrast limited adaptive histogram + equalization + 2018-11-13 7.0.8-15 Dirk Lemstra * Added support for GIMP 2.10 files (reference https://github.com/ImageMagick/ImageMagick/pull/1381). diff --git a/MagickCore/option.c b/MagickCore/option.c index 1d7a76d8c..cb585f271 100644 --- a/MagickCore/option.c +++ b/MagickCore/option.c @@ -612,6 +612,8 @@ static const OptionInfo { "-charcoal", 1L, SimpleOperatorFlag, MagickFalse }, { "+chop", 1L, DeprecateOptionFlag, MagickTrue }, { "-chop", 1L, SimpleOperatorFlag, MagickFalse }, + { "+clahe", 1L, DeprecateOptionFlag, MagickTrue }, + { "-clahe", 1L, SimpleOperatorFlag, MagickFalse }, { "+clamp", 0L, DeprecateOptionFlag, MagickTrue }, { "-clamp", 0L, SimpleOperatorFlag, MagickFalse }, { "-clip", 0L, SimpleOperatorFlag, MagickFalse }, diff --git a/MagickCore/threshold.c b/MagickCore/threshold.c index 4463a232a..1fd5d5360 100644 --- a/MagickCore/threshold.c +++ b/MagickCore/threshold.c @@ -209,8 +209,7 @@ MagickExport Image *AdaptiveThresholdImage(const Image *image, (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename); assert(exception != (ExceptionInfo *) NULL); assert(exception->signature == MagickCoreSignature); - threshold_image=CloneImage(image,0,0,MagickTrue, - exception); + threshold_image=CloneImage(image,0,0,MagickTrue,exception); if (threshold_image == (Image *) NULL) return((Image *) NULL); status=SetImageStorageClass(threshold_image,DirectClass,exception); @@ -1059,6 +1058,220 @@ MagickExport MagickBooleanType BlackThresholdImage(Image *image, % % % % % % +% C A L H E I m a g e % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% CLAHEImage() is a variant of adaptive histogram equalization in which the +% contrast amplification is limited, so as to reduce this problem of noise +% amplification. +% +% Image *CLAHEImage(const Image *image,const size_t width, +% const size_t height,const double bias,const double sans, +% ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image: the image. +% +% o width: the width of the local neighborhood. +% +% o height: the height of the local neighborhood. +% +% o bias: the mean bias. +% +% o sans: not used +% +% o exception: return any errors or warnings in this structure. +% +*/ +MagickExport Image *CLAHEImage(const Image *image, + const size_t width,const size_t height,const double bias,const double sans, + ExceptionInfo *exception) +{ +#define CLAHEImageTag "CLAHE/Image" + + CacheView + *image_view, + *threshold_view; + + Image + *threshold_image; + + MagickBooleanType + status; + + MagickOffsetType + progress; + + MagickSizeType + number_pixels; + + ssize_t + y; + + /* + Initialize threshold image attributes. + */ + assert(image != (Image *) NULL); + assert(image->signature == MagickCoreSignature); + if (image->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename); + assert(exception != (ExceptionInfo *) NULL); + assert(exception->signature == MagickCoreSignature); + (void) sans; + threshold_image=CloneImage(image,0,0,MagickTrue,exception); + if (threshold_image == (Image *) NULL) + return((Image *) NULL); + status=SetImageStorageClass(threshold_image,DirectClass,exception); + if (status == MagickFalse) + { + threshold_image=DestroyImage(threshold_image); + return((Image *) NULL); + } + /* + Threshold image. + */ + status=MagickTrue; + progress=0; + number_pixels=(MagickSizeType) width*height; + image_view=AcquireVirtualCacheView(image,exception); + threshold_view=AcquireAuthenticCacheView(threshold_image,exception); +#if defined(MAGICKCORE_OPENMP_SUPPORT) + #pragma omp parallel for schedule(static) shared(progress,status) \ + magick_number_threads(image,threshold_image,image->rows,1) +#endif + for (y=0; y < (ssize_t) image->rows; y++) + { + double + channel_bias[MaxPixelChannels], + channel_sum[MaxPixelChannels]; + + register const Quantum + *magick_restrict p, + *magick_restrict pixels; + + register Quantum + *magick_restrict q; + + register ssize_t + i, + x; + + ssize_t + center, + u, + v; + + if (status == MagickFalse) + continue; + p=GetCacheViewVirtualPixels(image_view,-((ssize_t) width/2L),y-(ssize_t) + (height/2L),image->columns+width,height,exception); + q=QueueCacheViewAuthenticPixels(threshold_view,0,y,threshold_image->columns, + 1,exception); + if ((p == (const Quantum *) NULL) || (q == (Quantum *) NULL)) + { + status=MagickFalse; + continue; + } + center=(ssize_t) GetPixelChannels(image)*(image->columns+width)*(height/2L)+ + GetPixelChannels(image)*(width/2); + for (i=0; i < (ssize_t) GetPixelChannels(image); i++) + { + PixelChannel channel = GetPixelChannelChannel(image,i); + PixelTrait traits = GetPixelChannelTraits(image,channel); + PixelTrait threshold_traits=GetPixelChannelTraits(threshold_image, + channel); + if ((traits == UndefinedPixelTrait) || + (threshold_traits == UndefinedPixelTrait)) + continue; + if ((threshold_traits & CopyPixelTrait) != 0) + { + SetPixelChannel(threshold_image,channel,p[center+i],q); + continue; + } + pixels=p; + channel_bias[channel]=0.0; + channel_sum[channel]=0.0; + for (v=0; v < (ssize_t) height; v++) + { + for (u=0; u < (ssize_t) width; u++) + { + if (u == (ssize_t) (width-1)) + channel_bias[channel]+=pixels[i]; + channel_sum[channel]+=pixels[i]; + pixels+=GetPixelChannels(image); + } + pixels+=GetPixelChannels(image)*image->columns; + } + } + for (x=0; x < (ssize_t) image->columns; x++) + { + for (i=0; i < (ssize_t) GetPixelChannels(image); i++) + { + double + mean; + + PixelChannel channel = GetPixelChannelChannel(image,i); + PixelTrait traits = GetPixelChannelTraits(image,channel); + PixelTrait threshold_traits=GetPixelChannelTraits(threshold_image, + channel); + if ((traits == UndefinedPixelTrait) || + (threshold_traits == UndefinedPixelTrait)) + continue; + if ((threshold_traits & CopyPixelTrait) != 0) + { + SetPixelChannel(threshold_image,channel,p[center+i],q); + continue; + } + channel_sum[channel]-=channel_bias[channel]; + channel_bias[channel]=0.0; + pixels=p; + for (v=0; v < (ssize_t) height; v++) + { + channel_bias[channel]+=pixels[i]; + pixels+=(width-1)*GetPixelChannels(image); + channel_sum[channel]+=pixels[i]; + pixels+=GetPixelChannels(image)*(image->columns+1); + } + mean=(double) (channel_sum[channel]/number_pixels+bias); + SetPixelChannel(threshold_image,channel,(Quantum) ((double) + p[center+i] <= mean ? 0 : QuantumRange),q); + } + p+=GetPixelChannels(image); + q+=GetPixelChannels(threshold_image); + } + if (SyncCacheViewAuthenticPixels(threshold_view,exception) == MagickFalse) + status=MagickFalse; + if (image->progress_monitor != (MagickProgressMonitor) NULL) + { + MagickBooleanType + proceed; + +#if defined(MAGICKCORE_OPENMP_SUPPORT) + #pragma omp atomic +#endif + progress++; + proceed=SetImageProgress(image,CLAHEImageTag,progress,image->rows); + if (proceed == MagickFalse) + status=MagickFalse; + } + } + threshold_image->type=image->type; + threshold_view=DestroyCacheView(threshold_view); + image_view=DestroyCacheView(image_view); + if (status == MagickFalse) + threshold_image=DestroyImage(threshold_image); + return(threshold_image); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % % C l a m p I m a g e % % % % % diff --git a/MagickCore/threshold.h b/MagickCore/threshold.h index 731ae68aa..63a766300 100644 --- a/MagickCore/threshold.h +++ b/MagickCore/threshold.h @@ -35,6 +35,8 @@ typedef struct _ThresholdMap extern MagickExport Image *AdaptiveThresholdImage(const Image *,const size_t,const size_t,const double, + ExceptionInfo *), + *CLAHEImage(const Image *,const size_t,const size_t,const double,const double, ExceptionInfo *); extern MagickExport ThresholdMap diff --git a/MagickWand/convert.c b/MagickWand/convert.c index 7a02c39a5..cab2a11af 100644 --- a/MagickWand/convert.c +++ b/MagickWand/convert.c @@ -192,6 +192,7 @@ static MagickBooleanType ConvertUsage(void) "-channel mask set the image channel mask", "-charcoal radius simulate a charcoal drawing", "-chop geometry remove pixels from the image interior", + "-clahe geometry contrast limited adaptive histogram equalization", "-clamp keep pixel values in range (0-QuantumRange)", "-colorize value colorize the image with the fill color", "-color-matrix matrix apply color correction to the image", @@ -758,7 +759,7 @@ WandExport MagickBooleanType ConvertImageCommand(ImageInfo *image_info, { ssize_t method; - + if (*option == '+') break; i++; @@ -990,6 +991,17 @@ WandExport MagickBooleanType ConvertImageCommand(ImageInfo *image_info, ThrowConvertInvalidArgumentException(option,argv[i]); break; } + if (LocaleCompare("clahe",option+1) == 0) + { + if (*option == '+') + break; + i++; + if (i == (ssize_t) argc) + ThrowConvertException(OptionError,"MissingArgument",option); + if (IsGeometry(argv[i]) == MagickFalse) + ThrowConvertInvalidArgumentException(option,argv[i]); + break; + } if (LocaleCompare("clamp",option+1) == 0) break; if (LocaleCompare("clip",option+1) == 0) @@ -1015,7 +1027,7 @@ WandExport MagickBooleanType ConvertImageCommand(ImageInfo *image_info, Image *clone_images, *clone_list; - + clone_list=CloneImageList(image,exception); if (k != 0) clone_list=CloneImageList(image_stack[k-1].image,exception); @@ -1025,7 +1037,7 @@ WandExport MagickBooleanType ConvertImageCommand(ImageInfo *image_info, if (*option == '+') clone_images=CloneImages(clone_list,"-1",exception); else - { + { i++; if (i == (ssize_t) argc) ThrowConvertException(OptionError,"MissingArgument",option); diff --git a/MagickWand/magick-image.c b/MagickWand/magick-image.c index 79cc6ca7b..d174b3c7f 100644 --- a/MagickWand/magick-image.c +++ b/MagickWand/magick-image.c @@ -1254,6 +1254,60 @@ WandExport MagickBooleanType MagickChopImage(MagickWand *wand, % % % % % % +% M a g i c k C L A H E I m a g e % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% MagickCLAHEImage() selects an individual threshold for each pixel +% based on the range of intensity values in its local neighborhood. This +% allows for thresholding of an image whose global intensity histogram +% doesn't contain distinctive peaks. +% +% The format of the CLAHEImage method is: +% +% MagickBooleanType MagickCLAHEImage(MagickWand *wand,const size_t width, +% const size_t height,const double bias,const double sans) +% +% A description of each parameter follows: +% +% o wand: the magick wand. +% +% o width: the width of the local neighborhood. +% +% o height: the height of the local neighborhood. +% +% o offset: the mean bias. +% +% o sans: not used. +% +*/ +WandExport MagickBooleanType MagickCLAHEImage(MagickWand *wand, + const size_t width,const size_t height,const double bias,const double sans) +{ + Image + *threshold_image; + + assert(wand != (MagickWand *) NULL); + assert(wand->signature == MagickWandSignature); + if (wand->debug != MagickFalse) + (void) LogMagickEvent(WandEvent,GetMagickModule(),"%s",wand->name); + if (wand->images == (Image *) NULL) + ThrowWandException(WandError,"ContainsNoImages",wand->name); + threshold_image=CLAHEImage(wand->images,width,height,bias,sans, + wand->exception); + if (threshold_image == (Image *) NULL) + return(MagickFalse); + ReplaceImageInList(&wand->images,threshold_image); + return(MagickTrue); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % % M a g i c k C l a m p I m a g e % % % % % diff --git a/MagickWand/magick-image.h b/MagickWand/magick-image.h index c9000a909..9f57aded9 100644 --- a/MagickWand/magick-image.h +++ b/MagickWand/magick-image.h @@ -101,6 +101,8 @@ extern WandExport MagickBooleanType MagickCharcoalImage(MagickWand *,const double,const double), MagickChopImage(MagickWand *,const size_t,const size_t,const ssize_t, const ssize_t), + MagickCLAHEImage(MagickWand *,const size_t,const size_t,const double, + const double), MagickClampImage(MagickWand *), MagickClipImage(MagickWand *), MagickClipImagePath(MagickWand *,const char *,const MagickBooleanType), diff --git a/MagickWand/mogrify.c b/MagickWand/mogrify.c index 7ffcf8e18..ff3962ed2 100644 --- a/MagickWand/mogrify.c +++ b/MagickWand/mogrify.c @@ -1080,6 +1080,18 @@ WandExport MagickBooleanType MogrifyImage(ImageInfo *image_info,const int argc, mogrify_image=ChopImage(*image,&geometry,exception); break; } + if (LocaleCompare("clahe",option+1) == 0) + { + /* + Contrast limited adaptive histogram equalization. + */ + (void) SyncImageSettings(mogrify_info,*image,exception); + flags=ParseGeometry(argv[i+1],&geometry_info); + mogrify_image=CLAHEImage(*image,(size_t) geometry_info.rho, + (size_t) geometry_info.sigma,(double) geometry_info.xi, + geometry_info.psi,exception); + break; + } if (LocaleCompare("clip",option+1) == 0) { (void) SyncImageSettings(mogrify_info,*image,exception); @@ -3473,6 +3485,7 @@ static MagickBooleanType MogrifyUsage(void) "-channel mask set the image channel mask", "-charcoal geometry simulate a charcoal drawing", "-chop geometry remove pixels from the image interior", + "-clahe geometry contrast limited adaptive histogram equalization", "-clamp keep pixel values in range (0-QuantumRange)", "-clip clip along the first path from the 8BIM profile", "-clip-mask filename associate a clip mask with the image", @@ -4333,6 +4346,17 @@ WandExport MagickBooleanType MogrifyImageCommand(ImageInfo *image_info, ThrowMogrifyInvalidArgumentException(option,argv[i]); break; } + if (LocaleCompare("clahe",option+1) == 0) + { + if (*option == '+') + break; + i++; + if (i == (ssize_t) argc) + ThrowMogrifyException(OptionError,"MissingArgument",option); + if (IsGeometry(argv[i]) == MagickFalse) + ThrowMogrifyInvalidArgumentException(option,argv[i]); + break; + } if (LocaleCompare("clamp",option+1) == 0) break; if (LocaleCompare("clip",option+1) == 0) diff --git a/MagickWand/operation.c b/MagickWand/operation.c index cf7bd2ca0..ed675030d 100644 --- a/MagickWand/operation.c +++ b/MagickWand/operation.c @@ -1966,6 +1966,16 @@ static MagickBooleanType CLISimpleOperatorImage(MagickCLI *cli_wand, new_image=ChopImage(_image,&geometry,_exception); break; } + if (LocaleCompare("clahe",option+1) == 0) + { + flags=ParseGeometry(arg1,&geometry_info); + if ((flags & (RhoValue|SigmaValue)) == 0) + CLIWandExceptArgBreak(OptionError,"InvalidArgument",option,arg1); + new_image=CLAHEImage(_image,(size_t) geometry_info.rho, + (size_t) geometry_info.sigma,geometry_info.xi,geometry_info.xi, + _exception); + break; + } if (LocaleCompare("clamp",option+1) == 0) { (void) ClampImage(_image,_exception); @@ -1976,7 +1986,8 @@ static MagickBooleanType CLISimpleOperatorImage(MagickCLI *cli_wand, if (IfNormalOp) (void) ClipImage(_image,_exception); else /* "+mask" remove the write mask */ - (void) SetImageMask(_image,WritePixelMask,(Image *) NULL,_exception); + (void) SetImageMask(_image,WritePixelMask,(Image *) NULL, + _exception); break; } if (LocaleCompare("clip-mask",option+1) == 0) diff --git a/PerlMagick/Magick.xs b/PerlMagick/Magick.xs index 6651242db..fdb10a5fe 100644 --- a/PerlMagick/Magick.xs +++ b/PerlMagick/Magick.xs @@ -412,7 +412,8 @@ static struct {"background", StringReference} } }, { "Difference", { {"image", ImageReference}, {"fuzz", StringReference} } }, { "AdaptiveThreshold", { {"geometry", StringReference}, - {"width", IntegerReference}, {"height", IntegerReference} } }, + {"width", IntegerReference}, {"height", IntegerReference}, + {"bias", RealReference} } }, { "Resample", { {"density", StringReference}, {"x", RealReference}, {"y", RealReference}, {"filter", MagickFilterOptions}, {"support", RealReference } } }, @@ -570,6 +571,9 @@ static struct {"low-black", RealReference}, {"low-white", RealReference}, {"high-white", RealReference}, {"high-black", RealReference}, {"channel", MagickChannelOptions} } }, + { "CLAHE", { {"geometry", StringReference}, + {"width", IntegerReference}, {"height", IntegerReference}, + {"bias", RealReference}, {"sans", RealReference} } }, }; static SplayTreeInfo @@ -7640,6 +7644,8 @@ Mogrify(ref,...) AutoThresholdImage = 294 RangeThreshold = 295 RangeThresholdImage= 296 + CLAHE = 297 + CLAHEImage = 298 MogrifyRegion = 666 PPCODE: { @@ -11471,6 +11477,28 @@ Mogrify(ref,...) (void) SetImageChannelMask(image,channel_mask); break; } + case 149: /* CLAHE */ + { + if (attribute_flag[0] != 0) + { + flags=ParseGeometry(argument_list[0].string_reference, + &geometry_info); + if ((flags & PercentValue) != 0) + geometry_info.xi=QuantumRange*geometry_info.xi/100.0; + } + if (attribute_flag[1] != 0) + geometry_info.rho=argument_list[1].integer_reference; + if (attribute_flag[2] != 0) + geometry_info.sigma=argument_list[2].integer_reference; + if (attribute_flag[3] != 0) + geometry_info.xi=argument_list[3].integer_reference;; + if (attribute_flag[4] != 0) + geometry_info.psi=argument_list[4].integer_reference;; + image=CLAHEImage(image,(size_t) geometry_info.rho, + (size_t) geometry_info.sigma,geometry_info.xi,geometry_info.psi, + exception); + break; + } } if (next != (Image *) NULL) (void) CatchImageException(next); diff --git a/PerlMagick/quantum/quantum.xs.in b/PerlMagick/quantum/quantum.xs.in index ba6a3eddf..a86d6293b 100644 --- a/PerlMagick/quantum/quantum.xs.in +++ b/PerlMagick/quantum/quantum.xs.in @@ -412,7 +412,8 @@ static struct {"background", StringReference} } }, { "Difference", { {"image", ImageReference}, {"fuzz", StringReference} } }, { "AdaptiveThreshold", { {"geometry", StringReference}, - {"width", IntegerReference}, {"height", IntegerReference} } }, + {"width", IntegerReference}, {"height", IntegerReference}, + {"bias", RealReference} } }, { "Resample", { {"density", StringReference}, {"x", RealReference}, {"y", RealReference}, {"filter", MagickFilterOptions}, {"support", RealReference } } }, @@ -570,6 +571,9 @@ static struct {"low-black", RealReference}, {"low-white", RealReference}, {"high-white", RealReference}, {"high-black", RealReference}, {"channel", MagickChannelOptions} } }, + { "CLAHE", { {"geometry", StringReference}, + {"width", IntegerReference}, {"height", IntegerReference}, + {"bias", RealReference}, {"sans", RealReference} } }, }; static SplayTreeInfo @@ -7639,6 +7643,8 @@ Mogrify(ref,...) AutoThresholdImage = 294 RangeThreshold = 295 RangeThresholdImage= 296 + CLAHE = 297 + CLAHEImage = 298 MogrifyRegion = 666 PPCODE: { @@ -11476,6 +11482,28 @@ Mogrify(ref,...) (void) SetImageChannelMask(image,channel_mask); break; } + case 149: /* CLAHE */ + { + if (attribute_flag[0] != 0) + { + flags=ParseGeometry(argument_list[0].string_reference, + &geometry_info); + if ((flags & PercentValue) != 0) + geometry_info.xi=QuantumRange*geometry_info.xi/100.0; + } + if (attribute_flag[1] != 0) + geometry_info.rho=argument_list[1].integer_reference; + if (attribute_flag[2] != 0) + geometry_info.sigma=argument_list[2].integer_reference; + if (attribute_flag[3] != 0) + geometry_info.xi=argument_list[3].integer_reference;; + if (attribute_flag[4] != 0) + geometry_info.psi=argument_list[4].integer_reference;; + image=CLAHEImage(image,(size_t) geometry_info.rho, + (size_t) geometry_info.sigma,geometry_info.xi,geometry.psi, + exception); + break; + } } if (next != (Image *) NULL) (void) CatchImageException(next); diff --git a/utilities/convert.1.in b/utilities/convert.1.in index b61ac55b0..fc32a964a 100644 --- a/utilities/convert.1.in +++ b/utilities/convert.1.in @@ -141,6 +141,7 @@ Image Operators: \-channel mask set the image channel mask \-charcoal radius simulate a charcoal drawing \-chop geometry remove pixels from the image interior + \-clahe geometry contrast limited adaptive histogram equalization \-clamp keep pixel values in range (0-QuantumRange) \-clip clip along the first path from the 8BIM profile \-clip-mask filename associate a clip mask with the image diff --git a/utilities/mogrify.1.in b/utilities/mogrify.1.in index 08ee5c370..b94a274fe 100644 --- a/utilities/mogrify.1.in +++ b/utilities/mogrify.1.in @@ -139,6 +139,7 @@ Image Operators: \-channel mask set the image channel mask \-charcoal radius simulate a charcoal drawing \-chop geometry remove pixels from the image interior + \-clahe geometry contrast limited adaptive histogram equalization \-clamp keep pixel values in range (0-QuantumRange) \-clip clip along the first path from the 8BIM profile \-clip-mask filename associate a clip mask with the image