Commit bfd1fcf35 for imagemagick.org

commit bfd1fcf351277688ba82d88d3d70e7f0d6811a48
Author: Cristy <urban-warrior@imagemagick.org>
Date:   Sun Apr 19 18:58:10 2026 -0400

    support pixel difference count (PDC) compare metric

diff --git a/MagickCore/compare.c b/MagickCore/compare.c
index 2da0a8b6d..b29bf4f18 100644
--- a/MagickCore/compare.c
+++ b/MagickCore/compare.c
@@ -1523,6 +1523,141 @@ static MagickBooleanType GetPASimilarity(const Image *image,
   return(status);
 }

+static MagickBooleanType GetPDCSimilarity(const Image *image,
+  const Image *reconstruct_image,double *similarity,ExceptionInfo *exception)
+{
+  CacheView
+    *image_view,
+    *reconstruct_view;
+
+  double
+    area,
+    fuzz;
+
+  MagickBooleanType
+    status = MagickTrue;
+
+  size_t
+    columns,
+    rows;
+
+  ssize_t
+    k,
+    y;
+
+  /*
+    Compute the pixel difference count similarity.
+  */
+  fuzz=GetFuzzyColorDistance(image,reconstruct_image);
+  (void) memset(similarity,0,(MaxPixelChannels+1)*sizeof(*similarity));
+  SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
+  image_view=AcquireVirtualCacheView(image,exception);
+  reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
+#if defined(MMAGICKCORE_OPENMP_SUPPORT)
+  #pragma omp parallel for schedule(static) shared(similarity,status) \
+    magick_number_threads(image,image,rows,1)
+#endif
+  for (y=0; y < (ssize_t) rows; y++)
+  {
+    const Quantum
+      *magick_restrict p,
+      *magick_restrict q;
+
+    double
+      channel_similarity[MaxPixelChannels+1] = { 0.0 };
+
+    ssize_t
+      x;
+
+    if (status == MagickFalse)
+      continue;
+    p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
+    q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
+    if ((p == (const Quantum *) NULL) || (q == (const Quantum *) NULL))
+      {
+        status=MagickFalse;
+        continue;
+      }
+    for (x=0; x < (ssize_t) columns; x++)
+    {
+      double
+        Da,
+        Sa;
+
+      size_t
+        count = 0;
+
+      ssize_t
+        i;
+
+      if ((GetPixelReadMask(image,p) <= (QuantumRange/2)) ||
+          (GetPixelReadMask(reconstruct_image,q) <= (QuantumRange/2)))
+        {
+          p+=(ptrdiff_t) GetPixelChannels(image);
+          q+=(ptrdiff_t) GetPixelChannels(reconstruct_image);
+          continue;
+        }
+      Sa=QuantumScale*(double) GetPixelAlpha(image,p);
+      Da=QuantumScale*(double) GetPixelAlpha(reconstruct_image,q);
+      for (i=0; i < (ssize_t) GetPixelChannels(image); i++)
+      {
+        double
+          error;
+
+        PixelChannel channel = GetPixelChannelChannel(image,i);
+        PixelTrait traits = GetPixelChannelTraits(image,channel);
+        PixelTrait reconstruct_traits = GetPixelChannelTraits(reconstruct_image,
+          channel);
+        if (((traits & UpdatePixelTrait) == 0) ||
+            ((reconstruct_traits & UpdatePixelTrait) == 0))
+          continue;
+        if (channel == AlphaPixelChannel)
+          error=(double) p[i]-(double) GetPixelChannel(reconstruct_image,
+            channel,q);
+        else
+          error=Sa*p[i]-Da*GetPixelChannel(reconstruct_image,channel,q);
+        if ((error*error) > fuzz)
+          {
+            channel_similarity[i]++;
+            count++;
+          }
+      }
+      if (count != 0)
+        channel_similarity[CompositePixelChannel]++;
+      p+=(ptrdiff_t) GetPixelChannels(image);
+      q+=(ptrdiff_t) GetPixelChannels(reconstruct_image);
+    }
+#if defined(MAGICKCORE_OPENMP_SUPPORT)
+    #pragma omp critical (MagickCore_GetPDCSimilarity)
+#endif
+    {
+      ssize_t
+        j;
+
+      for (j=0; j < (ssize_t) GetPixelChannels(image); j++)
+      {
+        PixelChannel channel = GetPixelChannelChannel(image,j);
+        PixelTrait traits = GetPixelChannelTraits(image,channel);
+        PixelTrait reconstruct_traits = GetPixelChannelTraits(reconstruct_image,
+          channel);
+        if (((traits & UpdatePixelTrait) == 0) ||
+            ((reconstruct_traits & UpdatePixelTrait) == 0))
+          continue;
+        similarity[j]+=channel_similarity[j];
+      }
+      similarity[CompositePixelChannel]+=
+        channel_similarity[CompositePixelChannel];
+    }
+  }
+  reconstruct_view=DestroyCacheView(reconstruct_view);
+  image_view=DestroyCacheView(image_view);
+  area=MagickSafeReciprocal((double) columns*rows);
+  for (k=0; k < (ssize_t) GetPixelChannels(image); k++)
+    similarity[k]*=area;
+  similarity[CompositePixelChannel]*=area;
+  return(status);
+}
+
 static MagickBooleanType DFTPhaseSpectrum(const Image *image,const ssize_t u,
   const ssize_t v,double *phase,ExceptionInfo *exception)
 {
@@ -2270,6 +2405,12 @@ MagickExport MagickBooleanType GetImageDistortion(Image *image,
         exception);
       break;
     }
+    case PixelDifferenceCountErrorMetric:
+    {
+      status=GetPDCSimilarity(image,reconstruct_image,channel_similarity,
+        exception);
+      break;
+    }
     case RootMeanSquaredErrorMetric:
     case UndefinedErrorMetric:
     default:
@@ -2443,6 +2584,12 @@ MagickExport double *GetImageDistortions(Image *image,
         exception);
       break;
     }
+    case PixelDifferenceCountErrorMetric:
+    {
+      status=GetPDCSimilarity(image,reconstruct_image,channel_similarity,
+        exception);
+      break;
+    }
     case RootMeanSquaredErrorMetric:
     case UndefinedErrorMetric:
     default:
@@ -4480,6 +4627,12 @@ static double GetSimilarityMetric(const Image *image,
         channel_similarity,exception);
       break;
     }
+    case PixelDifferenceCountErrorMetric:
+    {
+      status=GetPDCSimilarity(similarity_image,reconstruct_image,
+        channel_similarity,exception);
+      break;
+    }
     case RootMeanSquaredErrorMetric:
     case UndefinedErrorMetric:
     default:
diff --git a/MagickCore/compare.h b/MagickCore/compare.h
index dd2a48c47..3991b8ea8 100644
--- a/MagickCore/compare.h
+++ b/MagickCore/compare.h
@@ -40,7 +40,8 @@ typedef enum
   StructuralSimilarityErrorMetric,
   StructuralDissimilarityErrorMetric,
   PhaseCorrelationErrorMetric,
-  DotProductCorrelationErrorMetric
+  DotProductCorrelationErrorMetric,
+  PixelDifferenceCountErrorMetric
 } MetricType;

 extern MagickExport double
diff --git a/MagickCore/option.c b/MagickCore/option.c
index 1f020e5a3..31be5efe4 100644
--- a/MagickCore/option.c
+++ b/MagickCore/option.c
@@ -1853,6 +1853,7 @@ static const OptionInfo
     { "MSE", MeanSquaredErrorMetric, UndefinedOptionFlag, MagickFalse },
     { "NCC", NormalizedCrossCorrelationErrorMetric, UndefinedOptionFlag, MagickFalse },
     { "PAE", PeakAbsoluteErrorMetric, UndefinedOptionFlag, MagickFalse },
+    { "PDC", PixelDifferenceCountErrorMetric, UndefinedOptionFlag, MagickFalse },
     { "PHASE", PhaseCorrelationErrorMetric, UndefinedOptionFlag, MagickFalse },
     { "PHASH", PerceptualHashErrorMetric, UndefinedOptionFlag, MagickFalse },
     { "PSNR", PeakSignalToNoiseRatioErrorMetric, UndefinedOptionFlag, MagickFalse },
diff --git a/MagickWand/compare.c b/MagickWand/compare.c
index 7fc191e97..3ceb2652d 100644
--- a/MagickWand/compare.c
+++ b/MagickWand/compare.c
@@ -1243,6 +1243,7 @@ WandExport MagickBooleanType CompareImagesCommand(ImageInfo *image_info,
   switch (metric)
   {
     case AbsoluteErrorMetric:
+    case PixelDifferenceCountErrorMetric:
     {
       size_t
         columns,
@@ -1323,6 +1324,7 @@ WandExport MagickBooleanType CompareImagesCommand(ImageInfo *image_info,
           switch (metric)
           {
             case AbsoluteErrorMetric:
+            case PixelDifferenceCountErrorMetric:
             {
               (void) FormatLocaleFile(stderr,"%.*g (%.*g)",GetMagickPrecision(),
                 ceil(scale*distortion),GetMagickPrecision(),distortion);
@@ -1453,6 +1455,7 @@ WandExport MagickBooleanType CompareImagesCommand(ImageInfo *image_info,
             case PeakSignalToNoiseRatioErrorMetric:
             case PerceptualHashErrorMetric:
             case PhaseCorrelationErrorMetric:
+            case PixelDifferenceCountErrorMetric:
             case StructuralSimilarityErrorMetric:
             case StructuralDissimilarityErrorMetric:
             {