From 3551c28f8d9870a794156a1e83a1de1c2acdd2c9 Mon Sep 17 00:00:00 2001 From: Cristy Date: Sun, 17 Mar 2019 17:15:35 -0400 Subject: [PATCH] OTSU thresholding framework --- MagickCore/option.c | 3 + MagickCore/threshold.c | 158 +++++++++++++++++++++++++++++++ MagickCore/threshold.h | 1 + MagickWand/magick-image.c | 36 +++++++ MagickWand/magick-image.h | 1 + MagickWand/mogrify.c | 8 ++ MagickWand/operation.c | 5 + PerlMagick/Magick.xs | 13 +++ PerlMagick/quantum/quantum.xs.in | 13 +++ 9 files changed, 238 insertions(+) diff --git a/MagickCore/option.c b/MagickCore/option.c index daac3b5d8..5dd2fa365 100644 --- a/MagickCore/option.c +++ b/MagickCore/option.c @@ -341,6 +341,7 @@ static const OptionInfo { " normalize", 0, UndefinedOptionFlag, MagickFalse }, { " opaque", 0, UndefinedOptionFlag, MagickFalse }, { " ordered-dither", 0, UndefinedOptionFlag, MagickFalse }, + { " otsu-threshold", 0, UndefinedOptionFlag, MagickFalse }, { " paint", 0, UndefinedOptionFlag, MagickFalse }, { " posterize", 0, UndefinedOptionFlag, MagickFalse }, { " raise", 0, UndefinedOptionFlag, MagickFalse }, @@ -894,6 +895,8 @@ static const OptionInfo { "-ordered-dither", 1L, SimpleOperatorFlag, MagickFalse }, { "+orient", 0L, ImageInfoOptionFlag, MagickFalse }, { "-orient", 1L, ImageInfoOptionFlag, MagickFalse }, + { "+otsu-threshold", 0L, DeprecateOptionFlag | FireOptionFlag, MagickTrue }, + { "-otsu-threshold", 0L, ListOperatorFlag | FireOptionFlag, MagickFalse }, { "+page", 0L, ImageInfoOptionFlag, MagickFalse }, { "-page", 1L, ImageInfoOptionFlag, MagickFalse }, { "+paint", 0L, DeprecateOptionFlag, MagickTrue }, diff --git a/MagickCore/threshold.c b/MagickCore/threshold.c index 00b875ad1..447019fe0 100644 --- a/MagickCore/threshold.c +++ b/MagickCore/threshold.c @@ -1836,6 +1836,164 @@ MagickExport MagickBooleanType OrderedDitherImage(Image *image, % % % % % % +% O T S U T h r e s h o l d I m a g e % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% OTSUThresholdImage() automatically performs clustering-based image +% thresholding. The algorithm calculates the optimum threshold separates into +% two classes so that their combined spread (intra-class variance) is minimal, +% and their inter-class variance is maximal. +% +% The format of the OTSUThresholdImage method is: +% +% MagickBooleanType OTSUThresholdImage(Image *image, +% ExceptionInfo *exception) +% +% A description of each parameter follows: +% +% o image: the image. +% +% o exception: return any errors or warnings in this structure. +% +*/ +MagickExport MagickBooleanType OTSUThresholdImage(Image *image, + ExceptionInfo *exception) +{ +#define ThresholdImageTag "Threshold/Image" + + CacheView + *image_view; + + GeometryInfo + geometry_info; + + MagickBooleanType + status; + + MagickOffsetType + progress; + + PixelInfo + threshold; + + MagickStatusType + flags; + + ssize_t + y; + + assert(image != (Image *) NULL); + assert(image->signature == MagickCoreSignature); + if (image->debug != MagickFalse) + (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename); + if (SetImageStorageClass(image,DirectClass,exception) == MagickFalse) + return(MagickFalse); + if (IsGrayColorspace(image->colorspace) != MagickFalse) + (void) TransformImageColorspace(image,sRGBColorspace,exception); + GetPixelInfo(image,&threshold); + flags=ParseGeometry("100",&geometry_info); + threshold.red=geometry_info.rho; + threshold.green=geometry_info.rho; + threshold.blue=geometry_info.rho; + threshold.black=geometry_info.rho; + threshold.alpha=100.0; + if ((flags & SigmaValue) != 0) + threshold.green=geometry_info.sigma; + if ((flags & XiValue) != 0) + threshold.blue=geometry_info.xi; + if ((flags & PsiValue) != 0) + threshold.alpha=geometry_info.psi; + if (threshold.colorspace == CMYKColorspace) + { + if ((flags & PsiValue) != 0) + threshold.black=geometry_info.psi; + if ((flags & ChiValue) != 0) + threshold.alpha=geometry_info.chi; + } + if ((flags & PercentValue) != 0) + { + threshold.red*=(MagickRealType) (QuantumRange/100.0); + threshold.green*=(MagickRealType) (QuantumRange/100.0); + threshold.blue*=(MagickRealType) (QuantumRange/100.0); + threshold.black*=(MagickRealType) (QuantumRange/100.0); + threshold.alpha*=(MagickRealType) (QuantumRange/100.0); + } + /* + OTSU threshold image. + */ + status=MagickTrue; + progress=0; + image_view=AcquireAuthenticCacheView(image,exception); +#if defined(MAGICKCORE_OPENMP_SUPPORT) + #pragma omp parallel for schedule(static) shared(progress,status) \ + magick_number_threads(image,image,image->rows,1) +#endif + for (y=0; y < (ssize_t) image->rows; y++) + { + register ssize_t + x; + + register Quantum + *magick_restrict q; + + if (status == MagickFalse) + continue; + q=GetCacheViewAuthenticPixels(image_view,0,y,image->columns,1,exception); + if (q == (Quantum *) NULL) + { + status=MagickFalse; + continue; + } + for (x=0; x < (ssize_t) image->columns; x++) + { + double + pixel; + + register ssize_t + i; + + pixel=GetPixelIntensity(image,q); + for (i=0; i < (ssize_t) GetPixelChannels(image); i++) + { + PixelChannel channel = GetPixelChannelChannel(image,i); + PixelTrait traits = GetPixelChannelTraits(image,channel); + if ((traits & UpdatePixelTrait) == 0) + continue; + if (image->channel_mask != DefaultChannels) + pixel=(double) q[i]; + if (pixel > GetPixelInfoChannel(&threshold,channel)) + q[i]=QuantumRange; + } + q+=GetPixelChannels(image); + } + if (SyncCacheViewAuthenticPixels(image_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,ThresholdImageTag,progress,image->rows); + if (proceed == MagickFalse) + status=MagickFalse; + } + } + image_view=DestroyCacheView(image_view); + return(status); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % % P e r c e p t i b l e I m a g e % % % % % diff --git a/MagickCore/threshold.h b/MagickCore/threshold.h index 91bb1da23..1008fa47a 100644 --- a/MagickCore/threshold.h +++ b/MagickCore/threshold.h @@ -48,6 +48,7 @@ extern MagickExport MagickBooleanType ClampImage(Image *,ExceptionInfo *), ListThresholdMaps(FILE *,ExceptionInfo *), OrderedDitherImage(Image *,const char *,ExceptionInfo *), + OTSUThresholdImage(Image *,ExceptionInfo *), PerceptibleImage(Image *,const double,ExceptionInfo *), RandomThresholdImage(Image *,const double,const double,ExceptionInfo *), RangeThresholdImage(Image *,const double,const double,const double, diff --git a/MagickWand/magick-image.c b/MagickWand/magick-image.c index f6cf1e272..c5f193ebb 100644 --- a/MagickWand/magick-image.c +++ b/MagickWand/magick-image.c @@ -7559,6 +7559,42 @@ WandExport MagickBooleanType MagickOrderedDitherImage(MagickWand *wand, % % % % % % +% M a g i c k O T S U T h r e s h o l d I m a g e % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% +% MagickOTSUThresholdImage() automatically performs clustering-based image +% thresholding. The algorithm calculates the optimum threshold separates into +% two classes so that their combined spread (intra-class variance) is minimal, +% and their inter-class variance is maximal. +% +% The format of the MagickOTSUThresholdImage method is: +% +% MagickBooleanType MagickOTSUThresholdImage(MagickWand *wand) +% +% A description of each parameter follows: +% +% o wand: the magick wand. +% +*/ +WandExport MagickBooleanType MagickOTSUThresholdImage(MagickWand *wand) +{ + 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); + return(OTSUThresholdImage(wand->images,wand->exception)); +} + +/* +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % % M a g i c k P i n g I m a g e % % % % % diff --git a/MagickWand/magick-image.h b/MagickWand/magick-image.h index d6db943e3..61643e048 100644 --- a/MagickWand/magick-image.h +++ b/MagickWand/magick-image.h @@ -208,6 +208,7 @@ extern WandExport MagickBooleanType const double,const MagickBooleanType), MagickOptimizeImageTransparency(MagickWand *), MagickOrderedDitherImage(MagickWand *,const char *), + MagickOTSUThresholdImage(MagickWand *), MagickTransparentPaintImage(MagickWand *,const PixelWand *, const double,const double,const MagickBooleanType invert), MagickPingImage(MagickWand *,const char *), diff --git a/MagickWand/mogrify.c b/MagickWand/mogrify.c index 9eba2f096..ecbae9d1d 100644 --- a/MagickWand/mogrify.c +++ b/MagickWand/mogrify.c @@ -2336,6 +2336,12 @@ WandExport MagickBooleanType MogrifyImage(ImageInfo *image_info,const int argc, (void) OrderedDitherImage(*image,argv[i+1],exception); break; } + if (LocaleCompare("otsu-threshold",option+1) == 0) + { + (void) SyncImageSettings(mogrify_info,*image,exception); + (void) OTSUThresholdImage(*image,exception); + break; + } break; } case 'p': @@ -5623,6 +5629,8 @@ WandExport MagickBooleanType MogrifyImageCommand(ImageInfo *image_info, argv[i]); break; } + if (LocaleCompare("otsu-threshold",option+1) == 0) + break; ThrowMogrifyException(OptionError,"UnrecognizedOption",option) } case 'p': diff --git a/MagickWand/operation.c b/MagickWand/operation.c index c93c123e1..0f0dbecca 100644 --- a/MagickWand/operation.c +++ b/MagickWand/operation.c @@ -2895,6 +2895,11 @@ static MagickBooleanType CLISimpleOperatorImage(MagickCLI *cli_wand, (void) OrderedDitherImage(_image,arg1,_exception); break; } + if (LocaleCompare("otsu-threshold",option+1) == 0) + { + (void) OTSUThresholdImage(_image,_exception); + break; + } CLIWandExceptionBreak(OptionError,"UnrecognizedOption",option); } case 'p': diff --git a/PerlMagick/Magick.xs b/PerlMagick/Magick.xs index f63c7ecb5..24788ec5d 100644 --- a/PerlMagick/Magick.xs +++ b/PerlMagick/Magick.xs @@ -575,6 +575,7 @@ static struct { "CLAHE", { {"geometry", StringReference}, {"width", IntegerReference}, {"height", IntegerReference}, {"number-bins", IntegerReference}, {"clip-limit", RealReference} } }, + { "OTSUThreshold", { {"channel", MagickChannelOptions} } }, }; static SplayTreeInfo @@ -7647,6 +7648,8 @@ Mogrify(ref,...) RangeThresholdImage= 296 CLAHE = 297 CLAHEImage = 298 + OTSUThreshold = 299 + OTSUThresholdImage = 300 MogrifyRegion = 666 PPCODE: { @@ -11501,6 +11504,16 @@ Mogrify(ref,...) geometry_info.psi,exception); break; } + case 150: /* OTSUThreshold */ + { + if (attribute_flag[0] != 0) + channel=(ChannelType) argument_list[0].integer_reference; + channel_mask=SetImageChannelMask(image,channel); + (void) OTSUThresholdImage(image,exception); + if (image != (Image *) NULL) + (void) SetImageChannelMask(image,channel_mask); + break; + } } if (next != (Image *) NULL) (void) CatchImageException(next); diff --git a/PerlMagick/quantum/quantum.xs.in b/PerlMagick/quantum/quantum.xs.in index c70f208fb..664c9e826 100644 --- a/PerlMagick/quantum/quantum.xs.in +++ b/PerlMagick/quantum/quantum.xs.in @@ -575,6 +575,7 @@ static struct { "CLAHE", { {"geometry", StringReference}, {"width", IntegerReference}, {"height", IntegerReference}, {"number-bins", IntegerReference}, {"clip-limit", RealReference} } }, + { "OTSUThreshold", { {"channel", MagickChannelOptions} } }, }; static SplayTreeInfo @@ -7646,6 +7647,8 @@ Mogrify(ref,...) RangeThresholdImage= 296 CLAHE = 297 CLAHEImage = 298 + OTSUThreshold = 299 + OTSUThresholdImage = 300 MogrifyRegion = 666 PPCODE: { @@ -11507,6 +11510,16 @@ Mogrify(ref,...) geometry_info.psi,exception); break; } + case 150: /* OTSUThreshold */ + { + if (attribute_flag[0] != 0) + channel=(ChannelType) argument_list[0].integer_reference; + channel_mask=SetImageChannelMask(image,channel); + (void) OTSUThresholdImage(image,exception); + if (image != (Image *) NULL) + (void) SetImageChannelMask(image,channel_mask); + break; + } } if (next != (Image *) NULL) (void) CatchImageException(next); -- 2.50.1