MagickCore 6.9.13-46
Convert, Edit, Or Compose Bitmap Images
Loading...
Searching...
No Matches
compare.c
1/*
2%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
3% %
4% %
5% %
6% CCCC OOO M M PPPP AAA RRRR EEEEE %
7% C O O MM MM P P A A R R E %
8% C O O M M M PPPP AAAAA RRRR EEE %
9% C O O M M P A A R R E %
10% CCCC OOO M M P A A R R EEEEE %
11% %
12% %
13% MagickCore Image Comparison Methods %
14% %
15% Software Design %
16% Cristy %
17% December 2003 %
18% %
19% %
20% Copyright 1999 ImageMagick Studio LLC, a non-profit organization dedicated %
21% to making software imaging solutions freely available. %
22% %
23% You may not use this file except in compliance with the License. You may %
24% obtain a copy of the License at %
25% %
26% https://imagemagick.org/license/ %
27% %
28% Unless required by applicable law or agreed to in writing, software %
29% distributed under the License is distributed on an "AS IS" BASIS, %
30% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %
31% See the License for the specific language governing permissions and %
32% limitations under the License. %
33% %
34%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
35%
36%
37%
38*/
39
40/*
41 Include declarations.
42*/
43#include "magick/studio.h"
44#include "magick/artifact.h"
45#include "magick/attribute.h"
46#include "magick/cache-view.h"
47#include "magick/channel.h"
48#include "magick/client.h"
49#include "magick/color.h"
50#include "magick/color-private.h"
51#include "magick/colorspace.h"
52#include "magick/colorspace-private.h"
53#include "magick/compare.h"
54#include "magick/compare-private.h"
55#include "magick/composite-private.h"
56#include "magick/constitute.h"
57#include "magick/exception-private.h"
58#include "magick/geometry.h"
59#include "magick/image-private.h"
60#include "magick/list.h"
61#include "magick/log.h"
62#include "magick/memory_.h"
63#include "magick/monitor.h"
64#include "magick/monitor-private.h"
65#include "magick/option.h"
66#include "magick/pixel-private.h"
67#include "magick/property.h"
68#include "magick/resource_.h"
69#include "magick/statistic-private.h"
70#include "magick/string_.h"
71#include "magick/string-private.h"
72#include "magick/statistic.h"
73#include "magick/thread-private.h"
74#include "magick/transform.h"
75#include "magick/utility.h"
76#include "magick/version.h"
77
78/*
79%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
80% %
81% %
82% %
83% C o m p a r e I m a g e C h a n n e l s %
84% %
85% %
86% %
87%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
88%
89% CompareImageChannels() compares one or more image channels of an image
90% to a reconstructed image and returns the difference image.
91%
92% The format of the CompareImageChannels method is:
93%
94% Image *CompareImageChannels(const Image *image,
95% const Image *reconstruct_image,const ChannelType channel,
96% const MetricType metric,double *distortion,ExceptionInfo *exception)
97%
98% A description of each parameter follows:
99%
100% o image: the image.
101%
102% o reconstruct_image: the reconstruct image.
103%
104% o channel: the channel.
105%
106% o metric: the metric.
107%
108% o distortion: the computed distortion between the images.
109%
110% o exception: return any errors or warnings in this structure.
111%
112*/
113
114MagickExport Image *CompareImages(Image *image,const Image *reconstruct_image,
115 const MetricType metric,double *distortion,ExceptionInfo *exception)
116{
117 Image
118 *highlight_image;
119
120 highlight_image=CompareImageChannels(image,reconstruct_image,
121 CompositeChannels,metric,distortion,exception);
122 return(highlight_image);
123}
124
125static size_t GetNumberChannels(const Image *image,const ChannelType channel)
126{
127 size_t
128 channels;
129
130 channels=0;
131 if ((channel & RedChannel) != 0)
132 channels++;
133 if ((channel & GreenChannel) != 0)
134 channels++;
135 if ((channel & BlueChannel) != 0)
136 channels++;
137 if (((channel & OpacityChannel) != 0) && (image->matte != MagickFalse))
138 channels++;
139 if (((channel & IndexChannel) != 0) && (image->colorspace == CMYKColorspace))
140 channels++;
141 return(channels == 0 ? 1UL : channels);
142}
143
144static inline MagickBooleanType ValidateImageMorphology(
145 const Image *magick_restrict image,
146 const Image *magick_restrict reconstruct_image)
147{
148 /*
149 Does the image match the reconstructed image morphology?
150 */
151 if (GetNumberChannels(image,DefaultChannels) !=
152 GetNumberChannels(reconstruct_image,DefaultChannels))
153 return(MagickFalse);
154 return(MagickTrue);
155}
156
157MagickExport Image *CompareImageChannels(Image *image,
158 const Image *reconstruct_image,const ChannelType channel,
159 const MetricType metric,double *distortion,ExceptionInfo *exception)
160{
161 CacheView
162 *highlight_view,
163 *image_view,
164 *reconstruct_view;
165
166 const char
167 *artifact;
168
169 Image
170 *clone_image,
171 *difference_image,
172 *highlight_image;
173
174 MagickBooleanType
175 status = MagickTrue;
176
177 MagickPixelPacket
178 highlight,
179 lowlight,
180 zero;
181
182 size_t
183 columns,
184 rows;
185
186 ssize_t
187 y;
188
189 assert(image != (Image *) NULL);
190 assert(image->signature == MagickCoreSignature);
191 assert(reconstruct_image != (const Image *) NULL);
192 assert(reconstruct_image->signature == MagickCoreSignature);
193 assert(distortion != (double *) NULL);
194 if (IsEventLogging() != MagickFalse)
195 (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);
196 *distortion=0.0;
197 if (metric != PerceptualHashErrorMetric)
198 if (ValidateImageMorphology(image,reconstruct_image) == MagickFalse)
199 ThrowImageException(ImageError,"ImageMorphologyDiffers");
200 status=GetImageChannelDistortion(image,reconstruct_image,channel,metric,
201 distortion,exception);
202 if (status == MagickFalse)
203 return((Image *) NULL);
204 clone_image=CloneImage(image,0,0,MagickTrue,exception);
205 if (clone_image == (Image *) NULL)
206 return((Image *) NULL);
207 (void) SetImageMask(clone_image,(Image *) NULL);
208 difference_image=CloneImage(clone_image,0,0,MagickTrue,exception);
209 clone_image=DestroyImage(clone_image);
210 if (difference_image == (Image *) NULL)
211 return((Image *) NULL);
212 (void) SetImageAlphaChannel(difference_image,OpaqueAlphaChannel);
213 SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
214 highlight_image=CloneImage(image,columns,rows,MagickTrue,exception);
215 if (highlight_image == (Image *) NULL)
216 {
217 difference_image=DestroyImage(difference_image);
218 return((Image *) NULL);
219 }
220 if (SetImageStorageClass(highlight_image,DirectClass) == MagickFalse)
221 {
222 InheritException(exception,&highlight_image->exception);
223 difference_image=DestroyImage(difference_image);
224 highlight_image=DestroyImage(highlight_image);
225 return((Image *) NULL);
226 }
227 (void) SetImageMask(highlight_image,(Image *) NULL);
228 (void) SetImageAlphaChannel(highlight_image,OpaqueAlphaChannel);
229 (void) QueryMagickColor("#f1001ecc",&highlight,exception);
230 artifact=GetImageArtifact(image,"compare:highlight-color");
231 if (artifact != (const char *) NULL)
232 (void) QueryMagickColor(artifact,&highlight,exception);
233 (void) QueryMagickColor("#ffffffcc",&lowlight,exception);
234 artifact=GetImageArtifact(image,"compare:lowlight-color");
235 if (artifact != (const char *) NULL)
236 (void) QueryMagickColor(artifact,&lowlight,exception);
237 if (highlight_image->colorspace == CMYKColorspace)
238 {
239 ConvertRGBToCMYK(&highlight);
240 ConvertRGBToCMYK(&lowlight);
241 }
242 /*
243 Generate difference image.
244 */
245 GetMagickPixelPacket(image,&zero);
246 image_view=AcquireVirtualCacheView(image,exception);
247 reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
248 highlight_view=AcquireAuthenticCacheView(highlight_image,exception);
249#if defined(MAGICKCORE_OPENMP_SUPPORT)
250 #pragma omp parallel for schedule(static) shared(status) \
251 magick_number_threads(image,highlight_image,rows,1)
252#endif
253 for (y=0; y < (ssize_t) rows; y++)
254 {
255 MagickBooleanType
256 sync;
257
258 MagickPixelPacket
259 pixel,
260 reconstruct_pixel;
261
262 const IndexPacket
263 *magick_restrict indexes,
264 *magick_restrict reconstruct_indexes;
265
266 const PixelPacket
267 *magick_restrict p,
268 *magick_restrict q;
269
270 IndexPacket
271 *magick_restrict highlight_indexes;
272
273 PixelPacket
274 *magick_restrict r;
275
276 ssize_t
277 x;
278
279 if (status == MagickFalse)
280 continue;
281 p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
282 q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
283 r=QueueCacheViewAuthenticPixels(highlight_view,0,y,columns,1,exception);
284 if ((p == (const PixelPacket *) NULL) ||
285 (q == (const PixelPacket *) NULL) || (r == (PixelPacket *) NULL))
286 {
287 status=MagickFalse;
288 continue;
289 }
290 indexes=GetCacheViewVirtualIndexQueue(image_view);
291 reconstruct_indexes=GetCacheViewVirtualIndexQueue(reconstruct_view);
292 highlight_indexes=GetCacheViewAuthenticIndexQueue(highlight_view);
293 pixel=zero;
294 reconstruct_pixel=zero;
295 for (x=0; x < (ssize_t) columns; x++)
296 {
297 SetMagickPixelPacket(image,p,indexes == (IndexPacket *) NULL ? NULL :
298 indexes+x,&pixel);
299 SetMagickPixelPacket(reconstruct_image,q,reconstruct_indexes ==
300 (IndexPacket *) NULL ? NULL : reconstruct_indexes+x,&reconstruct_pixel);
301 if (IsMagickColorSimilar(&pixel,&reconstruct_pixel) == MagickFalse)
302 SetPixelPacket(highlight_image,&highlight,r,highlight_indexes ==
303 (IndexPacket *) NULL ? NULL : highlight_indexes+x);
304 else
305 SetPixelPacket(highlight_image,&lowlight,r,highlight_indexes ==
306 (IndexPacket *) NULL ? NULL : highlight_indexes+x);
307 p++;
308 q++;
309 r++;
310 }
311 sync=SyncCacheViewAuthenticPixels(highlight_view,exception);
312 if (sync == MagickFalse)
313 status=MagickFalse;
314 }
315 highlight_view=DestroyCacheView(highlight_view);
316 reconstruct_view=DestroyCacheView(reconstruct_view);
317 image_view=DestroyCacheView(image_view);
318 (void) CompositeImage(difference_image,image->compose,highlight_image,0,0);
319 highlight_image=DestroyImage(highlight_image);
320 if (status == MagickFalse)
321 difference_image=DestroyImage(difference_image);
322 return(difference_image);
323}
324
325/*
326%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
327% %
328% %
329% %
330% G e t I m a g e C h a n n e l D i s t o r t i o n %
331% %
332% %
333% %
334%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
335%
336% GetImageChannelDistortion() compares one or more image channels of an image
337% to a reconstructed image and returns the specified distortion metric.
338%
339% The format of the GetImageChannelDistortion method is:
340%
341% MagickBooleanType GetImageChannelDistortion(const Image *image,
342% const Image *reconstruct_image,const ChannelType channel,
343% const MetricType metric,double *distortion,ExceptionInfo *exception)
344%
345% A description of each parameter follows:
346%
347% o image: the image.
348%
349% o reconstruct_image: the reconstruct image.
350%
351% o channel: the channel.
352%
353% o metric: the metric.
354%
355% o distortion: the computed distortion between the images.
356%
357% o exception: return any errors or warnings in this structure.
358%
359*/
360
361MagickExport MagickBooleanType GetImageDistortion(Image *image,
362 const Image *reconstruct_image,const MetricType metric,double *distortion,
363 ExceptionInfo *exception)
364{
365 MagickBooleanType
366 status;
367
368 status=GetImageChannelDistortion(image,reconstruct_image,CompositeChannels,
369 metric,distortion,exception);
370 return(status);
371}
372
373static MagickBooleanType GetAESimilarity(const Image *image,
374 const Image *reconstruct_image,const ChannelType channel,double *similarity,
375 ExceptionInfo *exception)
376{
377 CacheView
378 *image_view,
379 *reconstruct_view;
380
381 double
382 area,
383 fuzz;
384
385 MagickBooleanType
386 status = MagickTrue;
387
388 size_t
389 columns,
390 rows;
391
392 ssize_t
393 j,
394 y;
395
396 /*
397 Compute the absolute difference in pixels between two images.
398 */
399 fuzz=GetFuzzyColorDistance(image,reconstruct_image);
400 SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
401 image_view=AcquireVirtualCacheView(image,exception);
402 reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
403#if defined(MAGICKCORE_OPENMP_SUPPORT)
404 #pragma omp parallel for schedule(static) shared(similarity,status) \
405 magick_number_threads(image,image,rows,1)
406#endif
407 for (y=0; y < (ssize_t) rows; y++)
408 {
409 const IndexPacket
410 *magick_restrict indexes,
411 *magick_restrict reconstruct_indexes;
412
413 const PixelPacket
414 *magick_restrict p,
415 *magick_restrict q;
416
417 double
418 channel_similarity[CompositeChannels+1] = { 0.0 };
419
420 ssize_t
421 i,
422 x;
423
424 if (status == MagickFalse)
425 continue;
426 p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
427 q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
428 if ((p == (const PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
429 {
430 status=MagickFalse;
431 continue;
432 }
433 indexes=GetCacheViewVirtualIndexQueue(image_view);
434 reconstruct_indexes=GetCacheViewVirtualIndexQueue(reconstruct_view);
435 (void) memset(channel_similarity,0,sizeof(channel_similarity));
436 for (x=0; x < (ssize_t) columns; x++)
437 {
438 double
439 Da,
440 error,
441 Sa;
442
443 size_t
444 count = 0;
445
446 Sa=QuantumScale*(image->matte != MagickFalse ? (double) GetPixelAlpha(p) :
447 ((double) QuantumRange-(double) OpaqueOpacity));
448 Da=QuantumScale*(image->matte != MagickFalse ? (double) GetPixelAlpha(q) :
449 ((double) QuantumRange-(double) OpaqueOpacity));
450 if ((channel & RedChannel) != 0)
451 {
452 error=Sa*(double) GetPixelRed(p)-Da*(double)
453 GetPixelRed(q);
454 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
455 {
456 channel_similarity[RedChannel]++;
457 count++;
458 }
459 }
460 if ((channel & GreenChannel) != 0)
461 {
462 error=Sa*(double) GetPixelGreen(p)-Da*(double)
463 GetPixelGreen(q);
464 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
465 {
466 channel_similarity[GreenChannel]++;
467 count++;
468 }
469 }
470 if ((channel & BlueChannel) != 0)
471 {
472 error=Sa*(double) GetPixelBlue(p)-Da*(double)
473 GetPixelBlue(q);
474 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
475 {
476 channel_similarity[BlueChannel]++;
477 count++;
478 }
479 }
480 if (((channel & OpacityChannel) != 0) &&
481 (image->matte != MagickFalse))
482 {
483 error=(double) GetPixelOpacity(p)-(double) GetPixelOpacity(q);
484 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
485 {
486 channel_similarity[OpacityChannel]++;
487 count++;
488 }
489 }
490 if (((channel & IndexChannel) != 0) &&
491 (image->colorspace == CMYKColorspace))
492 {
493 error=Sa*(double) indexes[x]-Da*(double) reconstruct_indexes[x];
494 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
495 {
496 channel_similarity[IndexChannel]++;
497 count++;
498 }
499 }
500 if (count != 0)
501 channel_similarity[CompositeChannels]++;
502 p++;
503 q++;
504 }
505#if defined(MAGICKCORE_OPENMP_SUPPORT)
506 #pragma omp critical (MagickCore_GetAESimilarity)
507#endif
508 for (i=0; i <= (ssize_t) CompositeChannels; i++)
509 similarity[i]+=channel_similarity[i];
510 }
511 reconstruct_view=DestroyCacheView(reconstruct_view);
512 image_view=DestroyCacheView(image_view);
513 area=MagickSafeReciprocal((double) columns*rows);
514 for (j=0; j <= CompositeChannels; j++)
515 similarity[j]*=area;
516 return(status);
517}
518
519static MagickBooleanType GetFUZZSimilarity(const Image *image,
520 const Image *reconstruct_image,const ChannelType channel,
521 double *similarity,ExceptionInfo *exception)
522{
523 CacheView
524 *image_view,
525 *reconstruct_view;
526
527 double
528 area = 0.0,
529 fuzz;
530
531 MagickBooleanType
532 status = MagickTrue;
533
534 size_t
535 columns,
536 rows;
537
538 ssize_t
539 i,
540 y;
541
542 fuzz=GetFuzzyColorDistance(image,reconstruct_image);
543 SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
544 image_view=AcquireVirtualCacheView(image,exception);
545 reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
546#if defined(MAGICKCORE_OPENMP_SUPPORT)
547 #pragma omp parallel for schedule(static) shared(status) \
548 magick_number_threads(image,image,rows,1)
549#endif
550 for (y=0; y < (ssize_t) rows; y++)
551 {
552 double
553 channel_area = 0.0,
554 channel_similarity[CompositeChannels+1] = { 0.0 };
555
556 const IndexPacket
557 *magick_restrict indexes,
558 *magick_restrict reconstruct_indexes;
559
560 const PixelPacket
561 *magick_restrict p,
562 *magick_restrict q;
563
564 ssize_t
565 i,
566 x;
567
568 if (status == MagickFalse)
569 continue;
570 p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
571 q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
572 if ((p == (const PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
573 {
574 status=MagickFalse;
575 continue;
576 }
577 indexes=GetCacheViewVirtualIndexQueue(image_view);
578 reconstruct_indexes=GetCacheViewVirtualIndexQueue(reconstruct_view);
579 for (x=0; x < (ssize_t) columns; x++)
580 {
581 MagickRealType
582 Da,
583 error,
584 Sa;
585
586 Sa=QuantumScale*(image->matte != MagickFalse ? (double)
587 GetPixelAlpha(p) : ((double) QuantumRange-(double) OpaqueOpacity));
588 Da=QuantumScale*(reconstruct_image->matte != MagickFalse ?
589 (double) GetPixelAlpha(q) : ((double) QuantumRange-(double)
590 OpaqueOpacity));
591 if ((channel & RedChannel) != 0)
592 {
593 error=QuantumScale*(Sa*GetPixelRed(p)-Da*GetPixelRed(q));
594 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
595 {
596 channel_similarity[RedChannel]+=error*error;
597 channel_similarity[CompositeChannels]+=error*error;
598 channel_area++;
599 }
600 }
601 if ((channel & GreenChannel) != 0)
602 {
603 error=QuantumScale*(Sa*GetPixelGreen(p)-Da*GetPixelGreen(q));
604 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
605 {
606 channel_similarity[GreenChannel]+=error*error;
607 channel_similarity[CompositeChannels]+=error*error;
608 channel_area++;
609 }
610 }
611 if ((channel & BlueChannel) != 0)
612 {
613 error=QuantumScale*(Sa*GetPixelBlue(p)-Da*GetPixelBlue(q));
614 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
615 {
616 channel_similarity[BlueChannel]+=error*error;
617 channel_similarity[CompositeChannels]+=error*error;
618 channel_area++;
619 }
620 }
621 if (((channel & OpacityChannel) != 0) && (image->matte != MagickFalse))
622 {
623 error=QuantumScale*((double) GetPixelOpacity(p)-GetPixelOpacity(q));
624 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
625 {
626 channel_similarity[OpacityChannel]+=error*error;
627 channel_similarity[CompositeChannels]+=error*error;
628 channel_area++;
629 }
630 }
631 if (((channel & IndexChannel) != 0) &&
632 (image->colorspace == CMYKColorspace))
633 {
634 error=QuantumScale*(Sa*GetPixelIndex(indexes+x)-Da*
635 GetPixelIndex(reconstruct_indexes+x));
636 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
637 {
638 channel_similarity[BlackChannel]+=error*error;
639 channel_similarity[CompositeChannels]+=error*error;
640 channel_area++;
641 }
642 }
643 p++;
644 q++;
645 }
646#if defined(MAGICKCORE_OPENMP_SUPPORT)
647 #pragma omp critical (MagickCore_GetMeanAbsoluteError)
648#endif
649 {
650 area+=channel_area;
651 for (i=0; i <= (ssize_t) CompositeChannels; i++)
652 similarity[i]+=channel_similarity[i];
653 }
654 }
655 reconstruct_view=DestroyCacheView(reconstruct_view);
656 image_view=DestroyCacheView(image_view);
657 area=MagickSafeReciprocal(area);
658 for (i=0; i <= (ssize_t) CompositeChannels; i++)
659 similarity[i]*=area;
660 return(status);
661}
662
663static MagickBooleanType GetPDCSimilarity(const Image *image,
664 const Image *reconstruct_image,const ChannelType channel,double *similarity,
665 ExceptionInfo *exception)
666{
667 CacheView
668 *image_view,
669 *reconstruct_view;
670
671 double
672 area,
673 fuzz;
674
675 MagickBooleanType
676 status = MagickTrue;
677
678 size_t
679 columns,
680 rows;
681
682 ssize_t
683 j,
684 y;
685
686 /*
687 Compute the absolute difference in pixels between two images.
688 */
689 fuzz=GetFuzzyColorDistance(image,reconstruct_image);
690 SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
691 image_view=AcquireVirtualCacheView(image,exception);
692 reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
693#if defined(MAGICKCORE_OPENMP_SUPPORT)
694 #pragma omp parallel for schedule(static) shared(similarity,status) \
695 magick_number_threads(image,image,rows,1)
696#endif
697 for (y=0; y < (ssize_t) rows; y++)
698 {
699 const IndexPacket
700 *magick_restrict indexes,
701 *magick_restrict reconstruct_indexes;
702
703 const PixelPacket
704 *magick_restrict p,
705 *magick_restrict q;
706
707 double
708 channel_similarity[CompositeChannels+1] = { 0.0 };
709
710 ssize_t
711 i,
712 x;
713
714 if (status == MagickFalse)
715 continue;
716 p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
717 q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
718 if ((p == (const PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
719 {
720 status=MagickFalse;
721 continue;
722 }
723 indexes=GetCacheViewVirtualIndexQueue(image_view);
724 reconstruct_indexes=GetCacheViewVirtualIndexQueue(reconstruct_view);
725 (void) memset(channel_similarity,0,sizeof(channel_similarity));
726 for (x=0; x < (ssize_t) columns; x++)
727 {
728 double
729 Da,
730 error,
731 Sa;
732
733 size_t
734 count = 0;
735
736 Sa=QuantumScale*(image->matte != MagickFalse ? (double) GetPixelAlpha(p) :
737 ((double) QuantumRange-(double) OpaqueOpacity));
738 Da=QuantumScale*(image->matte != MagickFalse ? (double) GetPixelAlpha(q) :
739 ((double) QuantumRange-(double) OpaqueOpacity));
740 if ((channel & RedChannel) != 0)
741 {
742 error=Sa*(double) GetPixelRed(p)-Da*(double)
743 GetPixelRed(q);
744 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
745 {
746 channel_similarity[RedChannel]++;
747 count++;
748 }
749 }
750 if ((channel & GreenChannel) != 0)
751 {
752 error=Sa*(double) GetPixelGreen(p)-Da*(double)
753 GetPixelGreen(q);
754 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
755 {
756 channel_similarity[GreenChannel]++;
757 count++;
758 }
759 }
760 if ((channel & BlueChannel) != 0)
761 {
762 error=Sa*(double) GetPixelBlue(p)-Da*(double)
763 GetPixelBlue(q);
764 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
765 {
766 channel_similarity[BlueChannel]++;
767 count++;
768 }
769 }
770 if (((channel & OpacityChannel) != 0) &&
771 (image->matte != MagickFalse))
772 {
773 error=(double) GetPixelOpacity(p)-(double) GetPixelOpacity(q);
774 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
775 {
776 channel_similarity[OpacityChannel]++;
777 count++;
778 }
779 }
780 if (((channel & IndexChannel) != 0) &&
781 (image->colorspace == CMYKColorspace))
782 {
783 error=Sa*(double) indexes[x]-Da*(double) reconstruct_indexes[x];
784 if (MagickSafeSignificantError(error*error,fuzz) != MagickFalse)
785 {
786 channel_similarity[IndexChannel]++;
787 count++;
788 }
789 }
790 if (count != 0)
791 channel_similarity[CompositeChannels]++;
792 p++;
793 q++;
794 }
795#if defined(MAGICKCORE_OPENMP_SUPPORT)
796 #pragma omp critical (MagickCore_GetAESimilarity)
797#endif
798 for (i=0; i <= (ssize_t) CompositeChannels; i++)
799 similarity[i]+=channel_similarity[i];
800 }
801 reconstruct_view=DestroyCacheView(reconstruct_view);
802 image_view=DestroyCacheView(image_view);
803 area=MagickSafeReciprocal((double) columns*rows);
804 for (j=0; j <= CompositeChannels; j++)
805 similarity[j]*=area;
806 return(status);
807}
808
809static MagickBooleanType GetMAESimilarity(const Image *image,
810 const Image *reconstruct_image,const ChannelType channel,
811 double *similarity,ExceptionInfo *exception)
812{
813 CacheView
814 *image_view,
815 *reconstruct_view;
816
817 MagickBooleanType
818 status;
819
820 size_t
821 columns,
822 rows;
823
824 ssize_t
825 i,
826 y;
827
828 status=MagickTrue;
829 SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
830 image_view=AcquireVirtualCacheView(image,exception);
831 reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
832#if defined(MAGICKCORE_OPENMP_SUPPORT)
833 #pragma omp parallel for schedule(static) shared(status) \
834 magick_number_threads(image,image,rows,1)
835#endif
836 for (y=0; y < (ssize_t) rows; y++)
837 {
838 double
839 channel_similarity[CompositeChannels+1];
840
841 const IndexPacket
842 *magick_restrict indexes,
843 *magick_restrict reconstruct_indexes;
844
845 const PixelPacket
846 *magick_restrict p,
847 *magick_restrict q;
848
849 ssize_t
850 i,
851 x;
852
853 if (status == MagickFalse)
854 continue;
855 p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
856 q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
857 if ((p == (const PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
858 {
859 status=MagickFalse;
860 continue;
861 }
862 indexes=GetCacheViewVirtualIndexQueue(image_view);
863 reconstruct_indexes=GetCacheViewVirtualIndexQueue(reconstruct_view);
864 (void) memset(channel_similarity,0,sizeof(channel_similarity));
865 for (x=0; x < (ssize_t) columns; x++)
866 {
867 MagickRealType
868 distance,
869 Da,
870 Sa;
871
872 Sa=QuantumScale*(image->matte != MagickFalse ? (double) GetPixelAlpha(p) :
873 ((double) QuantumRange-(double) OpaqueOpacity));
874 Da=QuantumScale*(reconstruct_image->matte != MagickFalse ?
875 (double) GetPixelAlpha(q) : ((double) QuantumRange-(double)
876 OpaqueOpacity));
877 if ((channel & RedChannel) != 0)
878 {
879 distance=QuantumScale*fabs(Sa*(double) GetPixelRed(p)-Da*
880 (double) GetPixelRed(q));
881 channel_similarity[RedChannel]+=distance;
882 channel_similarity[CompositeChannels]+=distance;
883 }
884 if ((channel & GreenChannel) != 0)
885 {
886 distance=QuantumScale*fabs(Sa*(double) GetPixelGreen(p)-Da*
887 (double) GetPixelGreen(q));
888 channel_similarity[GreenChannel]+=distance;
889 channel_similarity[CompositeChannels]+=distance;
890 }
891 if ((channel & BlueChannel) != 0)
892 {
893 distance=QuantumScale*fabs(Sa*(double) GetPixelBlue(p)-Da*
894 (double) GetPixelBlue(q));
895 channel_similarity[BlueChannel]+=distance;
896 channel_similarity[CompositeChannels]+=distance;
897 }
898 if (((channel & OpacityChannel) != 0) &&
899 (image->matte != MagickFalse))
900 {
901 distance=QuantumScale*fabs((double) GetPixelOpacity(p)-(double)
902 GetPixelOpacity(q));
903 channel_similarity[OpacityChannel]+=distance;
904 channel_similarity[CompositeChannels]+=distance;
905 }
906 if (((channel & IndexChannel) != 0) &&
907 (image->colorspace == CMYKColorspace))
908 {
909 distance=QuantumScale*fabs(Sa*(double) GetPixelIndex(indexes+x)-Da*
910 (double) GetPixelIndex(reconstruct_indexes+x));
911 channel_similarity[BlackChannel]+=distance;
912 channel_similarity[CompositeChannels]+=distance;
913 }
914 p++;
915 q++;
916 }
917#if defined(MAGICKCORE_OPENMP_SUPPORT)
918 #pragma omp critical (MagickCore_GetMeanAbsoluteError)
919#endif
920 for (i=0; i <= (ssize_t) CompositeChannels; i++)
921 similarity[i]+=channel_similarity[i];
922 }
923 reconstruct_view=DestroyCacheView(reconstruct_view);
924 image_view=DestroyCacheView(image_view);
925 for (i=0; i <= (ssize_t) CompositeChannels; i++)
926 similarity[i]/=((double) columns*rows);
927 similarity[CompositeChannels]/=(double) GetNumberChannels(image,channel);
928 return(status);
929}
930
931static MagickBooleanType GetMEPPSimilarity(Image *image,
932 const Image *reconstruct_image,const ChannelType channel,double *similarity,
933 ExceptionInfo *exception)
934{
935 CacheView
936 *image_view,
937 *reconstruct_view;
938
939 double
940 maximum_error = -MagickMaximumValue,
941 mean_error = 0.0;
942
943 MagickBooleanType
944 status;
945
946 size_t
947 columns,
948 rows;
949
950 ssize_t
951 i,
952 y;
953
954 status=MagickTrue;
955 SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
956 image_view=AcquireVirtualCacheView(image,exception);
957 reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
958#if defined(MAGICKCORE_OPENMP_SUPPORT)
959 #pragma omp parallel for schedule(static) shared(maximum_error,status) \
960 magick_number_threads(image,image,rows,1)
961#endif
962 for (y=0; y < (ssize_t) rows; y++)
963 {
964 double
965 channel_similarity[CompositeChannels+1] = { 0.0 },
966 local_maximum = maximum_error,
967 local_mean_error = 0.0;
968
969 const IndexPacket
970 *magick_restrict indexes,
971 *magick_restrict reconstruct_indexes;
972
973 const PixelPacket
974 *magick_restrict p,
975 *magick_restrict q;
976
977 ssize_t
978 i,
979 x;
980
981 if (status == MagickFalse)
982 continue;
983 p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
984 q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
985 if ((p == (const PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
986 {
987 status=MagickFalse;
988 continue;
989 }
990 indexes=GetCacheViewVirtualIndexQueue(image_view);
991 reconstruct_indexes=GetCacheViewVirtualIndexQueue(reconstruct_view);
992 (void) memset(channel_similarity,0,sizeof(channel_similarity));
993 for (x=0; x < (ssize_t) columns; x++)
994 {
995 MagickRealType
996 distance,
997 Da,
998 Sa;
999
1000 Sa=QuantumScale*(image->matte != MagickFalse ? (double) GetPixelAlpha(p) :
1001 ((double) QuantumRange-(double) OpaqueOpacity));
1002 Da=QuantumScale*(reconstruct_image->matte != MagickFalse ?
1003 (double) GetPixelAlpha(q) : ((double) QuantumRange-(double)
1004 OpaqueOpacity));
1005 if ((channel & RedChannel) != 0)
1006 {
1007 distance=QuantumScale*fabs(Sa*(double) GetPixelRed(p)-Da*
1008 (double) GetPixelRed(q));
1009 channel_similarity[RedChannel]+=distance;
1010 channel_similarity[CompositeChannels]+=distance;
1011 local_mean_error+=distance*distance;
1012 if (distance > local_maximum)
1013 local_maximum=distance;
1014 }
1015 if ((channel & GreenChannel) != 0)
1016 {
1017 distance=QuantumScale*fabs(Sa*(double) GetPixelGreen(p)-Da*
1018 (double) GetPixelGreen(q));
1019 channel_similarity[GreenChannel]+=distance;
1020 channel_similarity[CompositeChannels]+=distance;
1021 local_mean_error+=distance*distance;
1022 if (distance > local_maximum)
1023 local_maximum=distance;
1024 }
1025 if ((channel & BlueChannel) != 0)
1026 {
1027 distance=QuantumScale*fabs(Sa*(double) GetPixelBlue(p)-Da*
1028 (double) GetPixelBlue(q));
1029 channel_similarity[BlueChannel]+=distance;
1030 channel_similarity[CompositeChannels]+=distance;
1031 local_mean_error+=distance*distance;
1032 if (distance > local_maximum)
1033 local_maximum=distance;
1034 }
1035 if (((channel & OpacityChannel) != 0) &&
1036 (image->matte != MagickFalse))
1037 {
1038 distance=QuantumScale*fabs((double) GetPixelOpacity(p)-(double)
1039 GetPixelOpacity(q));
1040 channel_similarity[OpacityChannel]+=distance;
1041 channel_similarity[CompositeChannels]+=distance;
1042 local_mean_error+=distance*distance;
1043 if (distance > local_maximum)
1044 local_maximum=distance;
1045 }
1046 if (((channel & IndexChannel) != 0) &&
1047 (image->colorspace == CMYKColorspace))
1048 {
1049 distance=QuantumScale*fabs(Sa*(double) GetPixelIndex(indexes+x)-Da*
1050 (double) GetPixelIndex(reconstruct_indexes+x));
1051 channel_similarity[BlackChannel]+=distance;
1052 channel_similarity[CompositeChannels]+=distance;
1053 local_mean_error+=distance*distance;
1054 if (distance > local_maximum)
1055 local_maximum=distance;
1056 }
1057 p++;
1058 q++;
1059 }
1060#if defined(MAGICKCORE_OPENMP_SUPPORT)
1061 #pragma omp critical (MagickCore_GetMeanAbsoluteError)
1062#endif
1063 {
1064 for (i=0; i <= (ssize_t) CompositeChannels; i++)
1065 similarity[i]+=channel_similarity[i];
1066 mean_error+=local_mean_error;
1067 if (local_maximum > maximum_error)
1068 maximum_error=local_maximum;
1069 }
1070 }
1071 reconstruct_view=DestroyCacheView(reconstruct_view);
1072 image_view=DestroyCacheView(image_view);
1073 for (i=0; i <= (ssize_t) CompositeChannels; i++)
1074 similarity[i]/=((double) columns*rows);
1075 similarity[CompositeChannels]/=(double) GetNumberChannels(image,channel);
1076 image->error.mean_error_per_pixel=QuantumRange*similarity[CompositeChannels];
1077 image->error.normalized_mean_error=mean_error/((double) columns*rows);
1078 image->error.normalized_maximum_error=maximum_error;
1079 return(status);
1080}
1081
1082static MagickBooleanType GetMSESimilarity(const Image *image,
1083 const Image *reconstruct_image,const ChannelType channel,
1084 double *similarity,ExceptionInfo *exception)
1085{
1086 CacheView
1087 *image_view,
1088 *reconstruct_view;
1089
1090 double
1091 area = 0.0;
1092
1093 MagickBooleanType
1094 status;
1095
1096 size_t
1097 columns,
1098 rows;
1099
1100 ssize_t
1101 i,
1102 y;
1103
1104 status=MagickTrue;
1105 SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
1106 image_view=AcquireVirtualCacheView(image,exception);
1107 reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
1108#if defined(MAGICKCORE_OPENMP_SUPPORT)
1109 #pragma omp parallel for schedule(static) shared(similarity,status) \
1110 magick_number_threads(image,image,rows,1)
1111#endif
1112 for (y=0; y < (ssize_t) rows; y++)
1113 {
1114 double
1115 channel_similarity[CompositeChannels+1] = { 0.0 };
1116
1117 const IndexPacket
1118 *magick_restrict indexes,
1119 *magick_restrict reconstruct_indexes;
1120
1121 const PixelPacket
1122 *magick_restrict p,
1123 *magick_restrict q;
1124
1125 ssize_t
1126 i,
1127 x;
1128
1129 if (status == MagickFalse)
1130 continue;
1131 p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
1132 q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
1133 if ((p == (const PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
1134 {
1135 status=MagickFalse;
1136 continue;
1137 }
1138 indexes=GetCacheViewVirtualIndexQueue(image_view);
1139 reconstruct_indexes=GetCacheViewVirtualIndexQueue(reconstruct_view);
1140 for (x=0; x < (ssize_t) columns; x++)
1141 {
1142 double
1143 distance,
1144 Da,
1145 Sa;
1146
1147 Sa=QuantumScale*(image->matte != MagickFalse ? (double) GetPixelAlpha(p) :
1148 ((double) QuantumRange-(double) OpaqueOpacity));
1149 Da=QuantumScale*(reconstruct_image->matte != MagickFalse ?
1150 (double) GetPixelAlpha(q) : ((double) QuantumRange-(double)
1151 OpaqueOpacity));
1152 if ((channel & RedChannel) != 0)
1153 {
1154 distance=QuantumScale*(Sa*(double) GetPixelRed(p)-Da*(double)
1155 GetPixelRed(q));
1156 channel_similarity[RedChannel]+=distance*distance;
1157 channel_similarity[CompositeChannels]+=distance*distance;
1158 }
1159 if ((channel & GreenChannel) != 0)
1160 {
1161 distance=QuantumScale*(Sa*(double) GetPixelGreen(p)-Da*(double)
1162 GetPixelGreen(q));
1163 channel_similarity[GreenChannel]+=distance*distance;
1164 channel_similarity[CompositeChannels]+=distance*distance;
1165 }
1166 if ((channel & BlueChannel) != 0)
1167 {
1168 distance=QuantumScale*(Sa*(double) GetPixelBlue(p)-Da*(double)
1169 GetPixelBlue(q));
1170 channel_similarity[BlueChannel]+=distance*distance;
1171 channel_similarity[CompositeChannels]+=distance*distance;
1172 }
1173 if (((channel & OpacityChannel) != 0) &&
1174 (image->matte != MagickFalse))
1175 {
1176 distance=QuantumScale*((double) GetPixelOpacity(p)-(double)
1177 GetPixelOpacity(q));
1178 channel_similarity[OpacityChannel]+=distance*distance;
1179 channel_similarity[CompositeChannels]+=distance*distance;
1180 }
1181 if (((channel & IndexChannel) != 0) &&
1182 (image->colorspace == CMYKColorspace) &&
1183 (reconstruct_image->colorspace == CMYKColorspace))
1184 {
1185 distance=QuantumScale*(Sa*(double) GetPixelIndex(indexes+x)-Da*
1186 (double) GetPixelIndex(reconstruct_indexes+x));
1187 channel_similarity[BlackChannel]+=distance*distance;
1188 channel_similarity[CompositeChannels]+=distance*distance;
1189 }
1190 p++;
1191 q++;
1192 }
1193#if defined(MAGICKCORE_OPENMP_SUPPORT)
1194 #pragma omp critical (MagickCore_GetMeanSquaredError)
1195#endif
1196 for (i=0; i <= (ssize_t) CompositeChannels; i++)
1197 similarity[i]+=channel_similarity[i];
1198 }
1199 reconstruct_view=DestroyCacheView(reconstruct_view);
1200 image_view=DestroyCacheView(image_view);
1201 area=MagickSafeReciprocal((double) columns*rows);
1202 for (i=0; i <= (ssize_t) CompositeChannels; i++)
1203 similarity[i]*=area;
1204 similarity[CompositeChannels]/=(double) GetNumberChannels(image,channel);
1205 return(status);
1206}
1207
1208static MagickBooleanType GetNCCSimilarity(const Image *image,
1209 const Image *reconstruct_image,const ChannelType channel,double *similarity,
1210 ExceptionInfo *exception)
1211{
1212#define SimilarityImageTag "Similarity/Image"
1213
1214 CacheView
1215 *image_view,
1216 *reconstruct_view;
1217
1218 ChannelStatistics
1219 *image_statistics,
1220 *reconstruct_statistics;
1221
1222 double
1223 alpha_variance[CompositeChannels+1] = { 0.0 },
1224 beta_variance[CompositeChannels+1] = { 0.0 };
1225
1226 MagickBooleanType
1227 status;
1228
1229 MagickOffsetType
1230 progress;
1231
1232 size_t
1233 columns,
1234 rows;
1235
1236 ssize_t
1237 i,
1238 y;
1239
1240 /*
1241 Normalize to account for variation due to lighting and exposure condition.
1242 */
1243 image_statistics=GetImageChannelStatistics(image,exception);
1244 reconstruct_statistics=GetImageChannelStatistics(reconstruct_image,exception);
1245 if ((image_statistics == (ChannelStatistics *) NULL) ||
1246 (reconstruct_statistics == (ChannelStatistics *) NULL))
1247 {
1248 if (image_statistics != (ChannelStatistics *) NULL)
1249 image_statistics=(ChannelStatistics *) RelinquishMagickMemory(
1250 image_statistics);
1251 if (reconstruct_statistics != (ChannelStatistics *) NULL)
1252 reconstruct_statistics=(ChannelStatistics *) RelinquishMagickMemory(
1253 reconstruct_statistics);
1254 return(MagickFalse);
1255 }
1256 (void) memset(similarity,0,(CompositeChannels+1)*sizeof(*similarity));
1257 status=MagickTrue;
1258 progress=0;
1259 SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
1260 image_view=AcquireVirtualCacheView(image,exception);
1261 reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
1262#if defined(MAGICKCORE_OPENMP_SUPPORT)
1263 #pragma omp parallel for schedule(static) shared(status) \
1264 magick_number_threads(image,image,rows,1)
1265#endif
1266 for (y=0; y < (ssize_t) rows; y++)
1267 {
1268 const IndexPacket
1269 *magick_restrict indexes,
1270 *magick_restrict reconstruct_indexes;
1271
1272 const PixelPacket
1273 *magick_restrict p,
1274 *magick_restrict q;
1275
1276 double
1277 channel_alpha_variance[CompositeChannels+1] = { 0.0 },
1278 channel_beta_variance[CompositeChannels+1] = { 0.0 },
1279 channel_similarity[CompositeChannels+1] = { 0.0 };
1280
1281 ssize_t
1282 x;
1283
1284 if (status == MagickFalse)
1285 continue;
1286 p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
1287 q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
1288 if ((p == (const PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
1289 {
1290 status=MagickFalse;
1291 continue;
1292 }
1293 indexes=GetCacheViewVirtualIndexQueue(image_view);
1294 reconstruct_indexes=GetCacheViewVirtualIndexQueue(reconstruct_view);
1295 for (x=0; x < (ssize_t) columns; x++)
1296 {
1297 MagickRealType
1298 alpha,
1299 beta,
1300 Da,
1301 Sa;
1302
1303 Sa=QuantumScale*(image->matte != MagickFalse ? (double) GetPixelAlpha(p) :
1304 (double) QuantumRange);
1305 Da=QuantumScale*(reconstruct_image->matte != MagickFalse ?
1306 (double) GetPixelAlpha(q) : (double) QuantumRange);
1307 if ((channel & RedChannel) != 0)
1308 {
1309 alpha=QuantumScale*(Sa*(double) GetPixelRed(p)-
1310 image_statistics[RedChannel].mean);
1311 beta=QuantumScale*(Da*(double) GetPixelRed(q)-
1312 reconstruct_statistics[RedChannel].mean);
1313 channel_similarity[RedChannel]+=alpha*beta;
1314 channel_similarity[CompositeChannels]+=alpha*beta;
1315 channel_alpha_variance[RedChannel]+=alpha*alpha;
1316 channel_alpha_variance[CompositeChannels]+=alpha*alpha;
1317 channel_beta_variance[RedChannel]+=beta*beta;
1318 channel_beta_variance[CompositeChannels]+=beta*beta;
1319 }
1320 if ((channel & GreenChannel) != 0)
1321 {
1322 alpha=QuantumScale*(Sa*(double) GetPixelGreen(p)-
1323 image_statistics[GreenChannel].mean);
1324 beta=QuantumScale*(Da*(double) GetPixelGreen(q)-
1325 reconstruct_statistics[GreenChannel].mean);
1326 channel_similarity[GreenChannel]+=alpha*beta;
1327 channel_similarity[CompositeChannels]+=alpha*beta;
1328 channel_alpha_variance[GreenChannel]+=alpha*alpha;
1329 channel_alpha_variance[CompositeChannels]+=alpha*alpha;
1330 channel_beta_variance[GreenChannel]+=beta*beta;
1331 channel_beta_variance[CompositeChannels]+=beta*beta;
1332 }
1333 if ((channel & BlueChannel) != 0)
1334 {
1335 alpha=QuantumScale*(Sa*(double) GetPixelBlue(p)-
1336 image_statistics[BlueChannel].mean);
1337 beta=QuantumScale*(Da*(double) GetPixelBlue(q)-
1338 reconstruct_statistics[BlueChannel].mean);
1339 channel_similarity[BlueChannel]+=alpha*beta;
1340 channel_alpha_variance[BlueChannel]+=alpha*alpha;
1341 channel_beta_variance[BlueChannel]+=beta*beta;
1342 }
1343 if (((channel & OpacityChannel) != 0) && (image->matte != MagickFalse))
1344 {
1345 alpha=QuantumScale*((double) GetPixelAlpha(p)-
1346 image_statistics[AlphaChannel].mean);
1347 beta=QuantumScale*((double) GetPixelAlpha(q)-
1348 reconstruct_statistics[AlphaChannel].mean);
1349 channel_similarity[OpacityChannel]+=alpha*beta;
1350 channel_similarity[CompositeChannels]+=alpha*beta;
1351 channel_alpha_variance[OpacityChannel]+=alpha*alpha;
1352 channel_alpha_variance[CompositeChannels]+=alpha*alpha;
1353 channel_beta_variance[OpacityChannel]+=beta*beta;
1354 channel_beta_variance[CompositeChannels]+=beta*beta;
1355 }
1356 if (((channel & IndexChannel) != 0) &&
1357 (image->colorspace == CMYKColorspace) &&
1358 (reconstruct_image->colorspace == CMYKColorspace))
1359 {
1360 alpha=QuantumScale*(Sa*(double) GetPixelIndex(indexes+x)-
1361 image_statistics[BlackChannel].mean);
1362 beta=QuantumScale*(Da*(double) GetPixelIndex(reconstruct_indexes+
1363 x)-reconstruct_statistics[BlackChannel].mean);
1364 channel_similarity[BlackChannel]+=alpha*beta;
1365 channel_similarity[CompositeChannels]+=alpha*beta;
1366 channel_alpha_variance[BlackChannel]+=alpha*alpha;
1367 channel_alpha_variance[CompositeChannels]+=alpha*alpha;
1368 channel_beta_variance[BlackChannel]+=beta*beta;
1369 channel_beta_variance[CompositeChannels]+=beta*beta;
1370 }
1371 p++;
1372 q++;
1373 }
1374#if defined(MAGICKCORE_OPENMP_SUPPORT)
1375 #pragma omp critical (GetNCCSimilarity)
1376#endif
1377 {
1378 ssize_t
1379 j;
1380
1381 for (j=0; j <= (ssize_t) CompositeChannels; j++)
1382 {
1383 similarity[j]+=channel_similarity[j];
1384 alpha_variance[j]+=channel_alpha_variance[j];
1385 beta_variance[j]+=channel_beta_variance[j];
1386 }
1387 }
1388 if (image->progress_monitor != (MagickProgressMonitor) NULL)
1389 {
1390 MagickBooleanType
1391 proceed;
1392
1393#if defined(MAGICKCORE_OPENMP_SUPPORT)
1394 #pragma omp atomic
1395#endif
1396 progress++;
1397 proceed=SetImageProgress(image,SimilarityImageTag,progress,rows);
1398 if (proceed == MagickFalse)
1399 status=MagickFalse;
1400 }
1401 }
1402 reconstruct_view=DestroyCacheView(reconstruct_view);
1403 image_view=DestroyCacheView(image_view);
1404 /*
1405 Divide by the standard deviation.
1406 */
1407 for (i=0; i <= (ssize_t) CompositeChannels; i++)
1408 similarity[i]*=MagickSafeReciprocal(sqrt(alpha_variance[i])*
1409 sqrt(beta_variance[i]));
1410 /*
1411 Free resources.
1412 */
1413 reconstruct_statistics=(ChannelStatistics *) RelinquishMagickMemory(
1414 reconstruct_statistics);
1415 image_statistics=(ChannelStatistics *) RelinquishMagickMemory(
1416 image_statistics);
1417 return(status);
1418}
1419
1420static MagickBooleanType GetPASimilarity(const Image *image,
1421 const Image *reconstruct_image,const ChannelType channel,
1422 double *similarity,ExceptionInfo *exception)
1423{
1424 CacheView
1425 *image_view,
1426 *reconstruct_view;
1427
1428 MagickBooleanType
1429 status;
1430
1431 size_t
1432 columns,
1433 rows;
1434
1435 ssize_t
1436 y;
1437
1438 status=MagickTrue;
1439 (void) memset(similarity,0,(CompositeChannels+1)*sizeof(*similarity));
1440 SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
1441 image_view=AcquireVirtualCacheView(image,exception);
1442 reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
1443#if defined(MAGICKCORE_OPENMP_SUPPORT)
1444 #pragma omp parallel for schedule(static) shared(status) \
1445 magick_number_threads(image,image,rows,1)
1446#endif
1447 for (y=0; y < (ssize_t) rows; y++)
1448 {
1449 double
1450 channel_similarity[CompositeChannels+1];
1451
1452 const IndexPacket
1453 *magick_restrict indexes,
1454 *magick_restrict reconstruct_indexes;
1455
1456 const PixelPacket
1457 *magick_restrict p,
1458 *magick_restrict q;
1459
1460 ssize_t
1461 i,
1462 x;
1463
1464 if (status == MagickFalse)
1465 continue;
1466 p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
1467 q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
1468 if ((p == (const PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
1469 {
1470 status=MagickFalse;
1471 continue;
1472 }
1473 indexes=GetCacheViewVirtualIndexQueue(image_view);
1474 reconstruct_indexes=GetCacheViewVirtualIndexQueue(reconstruct_view);
1475 (void) memset(channel_similarity,0,(CompositeChannels+1)*
1476 sizeof(*channel_similarity));
1477 for (x=0; x < (ssize_t) columns; x++)
1478 {
1479 MagickRealType
1480 distance,
1481 Da,
1482 Sa;
1483
1484 Sa=QuantumScale*(image->matte != MagickFalse ? (double) GetPixelAlpha(p) :
1485 ((double) QuantumRange-(double) OpaqueOpacity));
1486 Da=QuantumScale*(reconstruct_image->matte != MagickFalse ?
1487 (double) GetPixelAlpha(q) : ((double) QuantumRange-(double)
1488 OpaqueOpacity));
1489 if ((channel & RedChannel) != 0)
1490 {
1491 distance=QuantumScale*fabs(Sa*(double) GetPixelRed(p)-Da*
1492 (double) GetPixelRed(q));
1493 if (distance > channel_similarity[RedChannel])
1494 channel_similarity[RedChannel]=distance;
1495 if (distance > channel_similarity[CompositeChannels])
1496 channel_similarity[CompositeChannels]=distance;
1497 }
1498 if ((channel & GreenChannel) != 0)
1499 {
1500 distance=QuantumScale*fabs(Sa*(double) GetPixelGreen(p)-Da*
1501 (double) GetPixelGreen(q));
1502 if (distance > channel_similarity[GreenChannel])
1503 channel_similarity[GreenChannel]=distance;
1504 if (distance > channel_similarity[CompositeChannels])
1505 channel_similarity[CompositeChannels]=distance;
1506 }
1507 if ((channel & BlueChannel) != 0)
1508 {
1509 distance=QuantumScale*fabs(Sa*(double) GetPixelBlue(p)-Da*
1510 (double) GetPixelBlue(q));
1511 if (distance > channel_similarity[BlueChannel])
1512 channel_similarity[BlueChannel]=distance;
1513 if (distance > channel_similarity[CompositeChannels])
1514 channel_similarity[CompositeChannels]=distance;
1515 }
1516 if (((channel & OpacityChannel) != 0) &&
1517 (image->matte != MagickFalse))
1518 {
1519 distance=QuantumScale*fabs((double) GetPixelOpacity(p)-(double)
1520 GetPixelOpacity(q));
1521 if (distance > channel_similarity[OpacityChannel])
1522 channel_similarity[OpacityChannel]=distance;
1523 if (distance > channel_similarity[CompositeChannels])
1524 channel_similarity[CompositeChannels]=distance;
1525 }
1526 if (((channel & IndexChannel) != 0) &&
1527 (image->colorspace == CMYKColorspace) &&
1528 (reconstruct_image->colorspace == CMYKColorspace))
1529 {
1530 distance=QuantumScale*fabs(Sa*(double) GetPixelIndex(indexes+x)-Da*
1531 (double) GetPixelIndex(reconstruct_indexes+x));
1532 if (distance > channel_similarity[BlackChannel])
1533 channel_similarity[BlackChannel]=distance;
1534 if (distance > channel_similarity[CompositeChannels])
1535 channel_similarity[CompositeChannels]=distance;
1536 }
1537 p++;
1538 q++;
1539 }
1540#if defined(MAGICKCORE_OPENMP_SUPPORT)
1541 #pragma omp critical (MagickCore_GetPeakAbsoluteError)
1542#endif
1543 for (i=0; i <= (ssize_t) CompositeChannels; i++)
1544 if (channel_similarity[i] > similarity[i])
1545 similarity[i]=channel_similarity[i];
1546 }
1547 reconstruct_view=DestroyCacheView(reconstruct_view);
1548 image_view=DestroyCacheView(image_view);
1549 return(status);
1550}
1551
1552static MagickBooleanType GetPSNRSimilarity(const Image *image,
1553 const Image *reconstruct_image,const ChannelType channel,
1554 double *similarity,ExceptionInfo *exception)
1555{
1556 MagickBooleanType
1557 status;
1558
1559 status=GetMSESimilarity(image,reconstruct_image,channel,similarity,
1560 exception);
1561 if ((channel & RedChannel) != 0)
1562 similarity[RedChannel]=10.0*MagickSafeLog10(MagickSafeReciprocal(
1563 similarity[RedChannel]))/MagickSafePSNRRecipicol(10.0);
1564 if ((channel & GreenChannel) != 0)
1565 similarity[GreenChannel]=10.0*MagickSafeLog10(MagickSafeReciprocal(
1566 similarity[GreenChannel]))/MagickSafePSNRRecipicol(10.0);
1567 if ((channel & BlueChannel) != 0)
1568 similarity[BlueChannel]=10.0*MagickSafeLog10(MagickSafeReciprocal(
1569 similarity[BlueChannel]))/MagickSafePSNRRecipicol(10.0);
1570 if (((channel & OpacityChannel) != 0) && (image->matte != MagickFalse))
1571 similarity[OpacityChannel]=10.0*MagickSafeLog10(MagickSafeReciprocal(
1572 similarity[OpacityChannel]))/MagickSafePSNRRecipicol(10.0);
1573 if (((channel & IndexChannel) != 0) && (image->colorspace == CMYKColorspace))
1574 similarity[BlackChannel]=10.0*MagickSafeLog10(MagickSafeReciprocal(
1575 similarity[BlackChannel]))/MagickSafePSNRRecipicol(10.0);
1576 similarity[CompositeChannels]=10.0*MagickSafeLog10(MagickSafeReciprocal(
1577 similarity[CompositeChannels]))/MagickSafePSNRRecipicol(10.0);
1578 return(status);
1579}
1580
1581static MagickBooleanType GetPHASHSimilarity(const Image *image,
1582 const Image *reconstruct_image,const ChannelType channel,double *similarity,
1583 ExceptionInfo *exception)
1584{
1585#define PHASHNormalizationFactor 389.373723242
1586
1587 ChannelPerceptualHash
1588 *image_phash,
1589 *reconstruct_phash;
1590
1591 double
1592 error,
1593 difference;
1594
1595 ssize_t
1596 i;
1597
1598 /*
1599 Compute perceptual hash in the sRGB colorspace.
1600 */
1601 image_phash=GetImageChannelPerceptualHash(image,exception);
1602 if (image_phash == (ChannelPerceptualHash *) NULL)
1603 return(MagickFalse);
1604 reconstruct_phash=GetImageChannelPerceptualHash(reconstruct_image,exception);
1605 if (reconstruct_phash == (ChannelPerceptualHash *) NULL)
1606 {
1607 image_phash=(ChannelPerceptualHash *) RelinquishMagickMemory(image_phash);
1608 return(MagickFalse);
1609 }
1610 for (i=0; i < MaximumNumberOfImageMoments; i++)
1611 {
1612 /*
1613 Compute sum of moment differences squared.
1614 */
1615 if ((channel & RedChannel) != 0)
1616 {
1617 error=reconstruct_phash[RedChannel].P[i]-image_phash[RedChannel].P[i];
1618 if (IsNaN(error) != 0)
1619 error=0.0;
1620 difference=error*error/PHASHNormalizationFactor;
1621 similarity[RedChannel]+=difference;
1622 similarity[CompositeChannels]+=difference;
1623 }
1624 if ((channel & GreenChannel) != 0)
1625 {
1626 error=reconstruct_phash[GreenChannel].P[i]-
1627 image_phash[GreenChannel].P[i];
1628 if (IsNaN(error) != 0)
1629 error=0.0;
1630 difference=error*error/PHASHNormalizationFactor;
1631 similarity[GreenChannel]+=difference;
1632 similarity[CompositeChannels]+=difference;
1633 }
1634 if ((channel & BlueChannel) != 0)
1635 {
1636 error=reconstruct_phash[BlueChannel].P[i]-image_phash[BlueChannel].P[i];
1637 if (IsNaN(error) != 0)
1638 error=0.0;
1639 difference=error*error/PHASHNormalizationFactor;
1640 similarity[BlueChannel]+=difference;
1641 similarity[CompositeChannels]+=difference;
1642 }
1643 if (((channel & OpacityChannel) != 0) && (image->matte != MagickFalse) &&
1644 (reconstruct_image->matte != MagickFalse))
1645 {
1646 error=reconstruct_phash[OpacityChannel].P[i]-
1647 image_phash[OpacityChannel].P[i];
1648 if (IsNaN(error) != 0)
1649 error=0.0;
1650 difference=error*error/PHASHNormalizationFactor;
1651 similarity[OpacityChannel]+=difference;
1652 similarity[CompositeChannels]+=difference;
1653 }
1654 if (((channel & IndexChannel) != 0) &&
1655 (image->colorspace == CMYKColorspace) &&
1656 (reconstruct_image->colorspace == CMYKColorspace))
1657 {
1658 error=reconstruct_phash[IndexChannel].P[i]-
1659 image_phash[IndexChannel].P[i];
1660 if (IsNaN(error) != 0)
1661 error=0.0;
1662 difference=error*error/PHASHNormalizationFactor;
1663 similarity[IndexChannel]+=difference;
1664 similarity[CompositeChannels]+=difference;
1665 }
1666 }
1667 /*
1668 Compute perceptual hash in the HCLP colorspace.
1669 */
1670 for (i=0; i < MaximumNumberOfImageMoments; i++)
1671 {
1672 /*
1673 Compute sum of moment differences squared.
1674 */
1675 if ((channel & RedChannel) != 0)
1676 {
1677 error=reconstruct_phash[RedChannel].Q[i]-image_phash[RedChannel].Q[i];
1678 if (IsNaN(error) != 0)
1679 error=0.0;
1680 difference=error*error/PHASHNormalizationFactor;
1681 similarity[RedChannel]+=difference;
1682 similarity[CompositeChannels]+=difference;
1683 }
1684 if ((channel & GreenChannel) != 0)
1685 {
1686 error=reconstruct_phash[GreenChannel].Q[i]-
1687 image_phash[GreenChannel].Q[i];
1688 if (IsNaN(error) != 0)
1689 error=0.0;
1690 difference=error*error/PHASHNormalizationFactor;
1691 similarity[GreenChannel]+=difference;
1692 similarity[CompositeChannels]+=difference;
1693 }
1694 if ((channel & BlueChannel) != 0)
1695 {
1696 error=reconstruct_phash[BlueChannel].Q[i]-image_phash[BlueChannel].Q[i];
1697 if (IsNaN(error) != 0)
1698 error=0.0;
1699 difference=error*error/PHASHNormalizationFactor;
1700 similarity[BlueChannel]+=difference;
1701 similarity[CompositeChannels]+=difference;
1702 }
1703 if (((channel & OpacityChannel) != 0) && (image->matte != MagickFalse) &&
1704 (reconstruct_image->matte != MagickFalse))
1705 {
1706 error=reconstruct_phash[OpacityChannel].Q[i]-
1707 image_phash[OpacityChannel].Q[i];
1708 if (IsNaN(error) != 0)
1709 error=0.0;
1710 difference=error*error/PHASHNormalizationFactor;
1711 similarity[OpacityChannel]+=difference;
1712 similarity[CompositeChannels]+=difference;
1713 }
1714 if (((channel & IndexChannel) != 0) &&
1715 (image->colorspace == CMYKColorspace) &&
1716 (reconstruct_image->colorspace == CMYKColorspace))
1717 {
1718 error=reconstruct_phash[IndexChannel].Q[i]-
1719 image_phash[IndexChannel].Q[i];
1720 if (IsNaN(error) != 0)
1721 error=0.0;
1722 difference=error*error/PHASHNormalizationFactor;
1723 similarity[IndexChannel]+=difference;
1724 similarity[CompositeChannels]+=difference;
1725 }
1726 }
1727 similarity[CompositeChannels]/=(double) GetNumberChannels(image,channel);
1728 /*
1729 Free resources.
1730 */
1731 reconstruct_phash=(ChannelPerceptualHash *) RelinquishMagickMemory(
1732 reconstruct_phash);
1733 image_phash=(ChannelPerceptualHash *) RelinquishMagickMemory(image_phash);
1734 return(MagickTrue);
1735}
1736
1737static MagickBooleanType GetRMSESimilarity(const Image *image,
1738 const Image *reconstruct_image,const ChannelType channel,double *similarity,
1739 ExceptionInfo *exception)
1740{
1741#define RMSESquareRoot(x) sqrt((x) < 0.0 ? 0.0 : (x))
1742
1743 MagickBooleanType
1744 status;
1745
1746 status=GetMSESimilarity(image,reconstruct_image,channel,similarity,
1747 exception);
1748 if ((channel & RedChannel) != 0)
1749 similarity[RedChannel]=RMSESquareRoot(similarity[RedChannel]);
1750 if ((channel & GreenChannel) != 0)
1751 similarity[GreenChannel]=RMSESquareRoot(similarity[GreenChannel]);
1752 if ((channel & BlueChannel) != 0)
1753 similarity[BlueChannel]=RMSESquareRoot(similarity[BlueChannel]);
1754 if (((channel & OpacityChannel) != 0) &&
1755 (image->matte != MagickFalse))
1756 similarity[OpacityChannel]=RMSESquareRoot(similarity[OpacityChannel]);
1757 if (((channel & IndexChannel) != 0) &&
1758 (image->colorspace == CMYKColorspace))
1759 similarity[BlackChannel]=RMSESquareRoot(similarity[BlackChannel]);
1760 similarity[CompositeChannels]=RMSESquareRoot(similarity[CompositeChannels]);
1761 return(status);
1762}
1763
1764MagickExport MagickBooleanType GetImageChannelDistortion(Image *image,
1765 const Image *reconstruct_image,const ChannelType channel,
1766 const MetricType metric,double *distortion,ExceptionInfo *exception)
1767{
1768 double
1769 *channel_similarity;
1770
1771 MagickBooleanType
1772 status;
1773
1774 size_t
1775 length;
1776
1777 assert(image != (Image *) NULL);
1778 assert(image->signature == MagickCoreSignature);
1779 assert(reconstruct_image != (const Image *) NULL);
1780 assert(reconstruct_image->signature == MagickCoreSignature);
1781 assert(distortion != (double *) NULL);
1782 if (IsEventLogging() != MagickFalse)
1783 (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);
1784 *distortion=0.0;
1785 if (metric != PerceptualHashErrorMetric)
1786 if (ValidateImageMorphology(image,reconstruct_image) == MagickFalse)
1787 ThrowBinaryException(ImageError,"ImageMorphologyDiffers",image->filename);
1788 /*
1789 Get image distortion.
1790 */
1791 length=CompositeChannels+1UL;
1792 channel_similarity=(double *) AcquireQuantumMemory(length,
1793 sizeof(*channel_similarity));
1794 if (channel_similarity == (double *) NULL)
1795 ThrowFatalException(ResourceLimitFatalError,"MemoryAllocationFailed");
1796 (void) memset(channel_similarity,0,length*sizeof(*channel_similarity));
1797 switch (metric)
1798 {
1799 case AbsoluteErrorMetric:
1800 {
1801 status=GetAESimilarity(image,reconstruct_image,channel,
1802 channel_similarity,exception);
1803 break;
1804 }
1805 case FuzzErrorMetric:
1806 {
1807 status=GetFUZZSimilarity(image,reconstruct_image,channel,
1808 channel_similarity,exception);
1809 break;
1810 }
1811 case MeanAbsoluteErrorMetric:
1812 {
1813 status=GetMAESimilarity(image,reconstruct_image,channel,
1814 channel_similarity,exception);
1815 break;
1816 }
1817 case MeanErrorPerPixelMetric:
1818 {
1819 status=GetMEPPSimilarity(image,reconstruct_image,channel,
1820 channel_similarity,exception);
1821 break;
1822 }
1823 case MeanSquaredErrorMetric:
1824 {
1825 status=GetMSESimilarity(image,reconstruct_image,channel,
1826 channel_similarity,exception);
1827 break;
1828 }
1829 case NormalizedCrossCorrelationErrorMetric:
1830 {
1831 status=GetNCCSimilarity(image,reconstruct_image,channel,
1832 channel_similarity,exception);
1833 break;
1834 }
1835 case PeakAbsoluteErrorMetric:
1836 {
1837 status=GetPASimilarity(image,reconstruct_image,channel,
1838 channel_similarity,exception);
1839 break;
1840 }
1841 case PeakSignalToNoiseRatioMetric:
1842 {
1843 status=GetPSNRSimilarity(image,reconstruct_image,channel,
1844 channel_similarity,exception);
1845 break;
1846 }
1847 case PerceptualHashErrorMetric:
1848 {
1849 status=GetPHASHSimilarity(image,reconstruct_image,channel,
1850 channel_similarity,exception);
1851 break;
1852 }
1853 case PixelDifferenceCountErrorMetric:
1854 {
1855 status=GetPDCSimilarity(image,reconstruct_image,channel,
1856 channel_similarity,exception);
1857 break;
1858 }
1859 case RootMeanSquaredErrorMetric:
1860 case UndefinedErrorMetric:
1861 default:
1862 {
1863 status=GetRMSESimilarity(image,reconstruct_image,channel,
1864 channel_similarity,exception);
1865 break;
1866 }
1867 }
1868 *distortion=channel_similarity[CompositeChannels];
1869 switch (metric)
1870 {
1871 case NormalizedCrossCorrelationErrorMetric:
1872 {
1873 *distortion=(1.0-(*distortion))/2.0;
1874 break;
1875 }
1876 default: break;
1877 }
1878 if (fabs(*distortion) < MagickEpsilon)
1879 *distortion=0.0;
1880 channel_similarity=(double *) RelinquishMagickMemory(channel_similarity);
1881 (void) FormatImageProperty(image,"distortion","%.*g",GetMagickPrecision(),
1882 *distortion);
1883 return(status);
1884}
1885
1886/*
1887%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1888% %
1889% %
1890% %
1891% G e t I m a g e C h a n n e l D i s t o r t i o n s %
1892% %
1893% %
1894% %
1895%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
1896%
1897% GetImageChannelDistortions() compares the image channels of an image to a
1898% reconstructed image and returns the specified distortion metric for each
1899% channel.
1900%
1901% The format of the GetImageChannelDistortions method is:
1902%
1903% double *GetImageChannelDistortions(const Image *image,
1904% const Image *reconstruct_image,const MetricType metric,
1905% ExceptionInfo *exception)
1906%
1907% A description of each parameter follows:
1908%
1909% o image: the image.
1910%
1911% o reconstruct_image: the reconstruct image.
1912%
1913% o metric: the metric.
1914%
1915% o exception: return any errors or warnings in this structure.
1916%
1917*/
1918MagickExport double *GetImageChannelDistortions(Image *image,
1919 const Image *reconstruct_image,const MetricType metric,
1920 ExceptionInfo *exception)
1921{
1922 double
1923 *distortion,
1924 *similarity;
1925
1926 MagickBooleanType
1927 status;
1928
1929 size_t
1930 length;
1931
1932 ssize_t
1933 i;
1934
1935 assert(image != (Image *) NULL);
1936 assert(image->signature == MagickCoreSignature);
1937 assert(reconstruct_image != (const Image *) NULL);
1938 assert(reconstruct_image->signature == MagickCoreSignature);
1939 if (IsEventLogging() != MagickFalse)
1940 (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);
1941 if (metric != PerceptualHashErrorMetric)
1942 if (ValidateImageMorphology(image,reconstruct_image) == MagickFalse)
1943 {
1944 (void) ThrowMagickException(&image->exception,GetMagickModule(),
1945 ImageError,"ImageMorphologyDiffers","`%s'",image->filename);
1946 return((double *) NULL);
1947 }
1948 /*
1949 Get image distortion.
1950 */
1951 length=CompositeChannels+1UL;
1952 similarity=(double *) AcquireQuantumMemory(length,
1953 sizeof(*similarity));
1954 if (similarity == (double *) NULL)
1955 ThrowFatalException(ResourceLimitFatalError,"MemoryAllocationFailed");
1956 (void) memset(similarity,0,length*sizeof(*similarity));
1957 status=MagickTrue;
1958 switch (metric)
1959 {
1960 case AbsoluteErrorMetric:
1961 {
1962 status=GetAESimilarity(image,reconstruct_image,CompositeChannels,
1963 similarity,exception);
1964 break;
1965 }
1966 case FuzzErrorMetric:
1967 {
1968 status=GetFUZZSimilarity(image,reconstruct_image,CompositeChannels,
1969 similarity,exception);
1970 break;
1971 }
1972 case MeanAbsoluteErrorMetric:
1973 {
1974 status=GetMAESimilarity(image,reconstruct_image,CompositeChannels,
1975 similarity,exception);
1976 break;
1977 }
1978 case MeanErrorPerPixelMetric:
1979 {
1980 status=GetMEPPSimilarity(image,reconstruct_image,CompositeChannels,
1981 similarity,exception);
1982 break;
1983 }
1984 case MeanSquaredErrorMetric:
1985 {
1986 status=GetMSESimilarity(image,reconstruct_image,CompositeChannels,
1987 similarity,exception);
1988 break;
1989 }
1990 case NormalizedCrossCorrelationErrorMetric:
1991 {
1992 status=GetNCCSimilarity(image,reconstruct_image,CompositeChannels,
1993 similarity,exception);
1994 break;
1995 }
1996 case PeakAbsoluteErrorMetric:
1997 {
1998 status=GetPASimilarity(image,reconstruct_image,CompositeChannels,
1999 similarity,exception);
2000 break;
2001 }
2002 case PeakSignalToNoiseRatioMetric:
2003 {
2004 status=GetPSNRSimilarity(image,reconstruct_image,CompositeChannels,
2005 similarity,exception);
2006 break;
2007 }
2008 case PerceptualHashErrorMetric:
2009 {
2010 status=GetPHASHSimilarity(image,reconstruct_image,CompositeChannels,
2011 similarity,exception);
2012 break;
2013 }
2014 case PixelDifferenceCountErrorMetric:
2015 {
2016 status=GetPDCSimilarity(image,reconstruct_image,CompositeChannels,
2017 similarity,exception);
2018 break;
2019 }
2020 case RootMeanSquaredErrorMetric:
2021 case UndefinedErrorMetric:
2022 default:
2023 {
2024 status=GetRMSESimilarity(image,reconstruct_image,CompositeChannels,
2025 similarity,exception);
2026 break;
2027 }
2028 }
2029 if (status == MagickFalse)
2030 {
2031 similarity=(double *) RelinquishMagickMemory(similarity);
2032 return((double *) NULL);
2033 }
2034 distortion=similarity;
2035 switch (metric)
2036 {
2037 case NormalizedCrossCorrelationErrorMetric:
2038 {
2039 for (i=0; i <= (ssize_t) CompositeChannels; i++)
2040 distortion[i]=(1.0-distortion[i])/2.0;
2041 break;
2042 }
2043 default: break;
2044 }
2045 for (i=0; i <= (ssize_t) CompositeChannels; i++)
2046 if (fabs(distortion[i]) < MagickEpsilon)
2047 distortion[i]=0.0;
2048 return(distortion);
2049}
2050
2051/*
2052%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2053% %
2054% %
2055% %
2056% I s I m a g e s E q u a l %
2057% %
2058% %
2059% %
2060%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2061%
2062% IsImagesEqual() measures the difference between colors at each pixel
2063% location of two images. A value other than 0 means the colors match
2064% exactly. Otherwise an error measure is computed by summing over all
2065% pixels in an image the distance squared in RGB space between each image
2066% pixel and its corresponding pixel in the reconstruct image. The error
2067% measure is assigned to these image members:
2068%
2069% o mean_error_per_pixel: The mean error for any single pixel in
2070% the image.
2071%
2072% o normalized_mean_error: The normalized mean quantization error for
2073% any single pixel in the image. This distance measure is normalized to
2074% a range between 0 and 1. It is independent of the range of red, green,
2075% and blue values in the image.
2076%
2077% o normalized_maximum_error: The normalized maximum quantization
2078% error for any single pixel in the image. This distance measure is
2079% normalized to a range between 0 and 1. It is independent of the range
2080% of red, green, and blue values in your image.
2081%
2082% A small normalized mean square error, accessed as
2083% image->normalized_mean_error, suggests the images are very similar in
2084% spatial layout and color.
2085%
2086% The format of the IsImagesEqual method is:
2087%
2088% MagickBooleanType IsImagesEqual(Image *image,
2089% const Image *reconstruct_image)
2090%
2091% A description of each parameter follows.
2092%
2093% o image: the image.
2094%
2095% o reconstruct_image: the reconstruct image.
2096%
2097*/
2098MagickExport MagickBooleanType IsImagesEqual(Image *image,
2099 const Image *reconstruct_image)
2100{
2101 CacheView
2102 *image_view,
2103 *reconstruct_view;
2104
2105 ExceptionInfo
2106 *exception;
2107
2108 MagickBooleanType
2109 status;
2110
2111 MagickRealType
2112 area,
2113 gamma,
2114 maximum_error,
2115 mean_error,
2116 mean_error_per_pixel;
2117
2118 size_t
2119 columns,
2120 rows;
2121
2122 ssize_t
2123 y;
2124
2125 assert(image != (Image *) NULL);
2126 assert(image->signature == MagickCoreSignature);
2127 assert(reconstruct_image != (const Image *) NULL);
2128 assert(reconstruct_image->signature == MagickCoreSignature);
2129 exception=(&image->exception);
2130 if (ValidateImageMorphology(image,reconstruct_image) == MagickFalse)
2131 ThrowBinaryException(ImageError,"ImageMorphologyDiffers",image->filename);
2132 area=0.0;
2133 maximum_error=0.0;
2134 mean_error_per_pixel=0.0;
2135 mean_error=0.0;
2136 SetImageCompareBounds(image,reconstruct_image,&columns,&rows);
2137 image_view=AcquireVirtualCacheView(image,exception);
2138 reconstruct_view=AcquireVirtualCacheView(reconstruct_image,exception);
2139 for (y=0; y < (ssize_t) rows; y++)
2140 {
2141 const IndexPacket
2142 *magick_restrict indexes,
2143 *magick_restrict reconstruct_indexes;
2144
2145 const PixelPacket
2146 *magick_restrict p,
2147 *magick_restrict q;
2148
2149 ssize_t
2150 x;
2151
2152 p=GetCacheViewVirtualPixels(image_view,0,y,columns,1,exception);
2153 q=GetCacheViewVirtualPixels(reconstruct_view,0,y,columns,1,exception);
2154 if ((p == (const PixelPacket *) NULL) || (q == (const PixelPacket *) NULL))
2155 break;
2156 indexes=GetCacheViewVirtualIndexQueue(image_view);
2157 reconstruct_indexes=GetCacheViewVirtualIndexQueue(reconstruct_view);
2158 for (x=0; x < (ssize_t) columns; x++)
2159 {
2160 MagickRealType
2161 distance;
2162
2163 distance=fabs((double) GetPixelRed(p)-(double) GetPixelRed(q));
2164 mean_error_per_pixel+=distance;
2165 mean_error+=distance*distance;
2166 if (distance > maximum_error)
2167 maximum_error=distance;
2168 area++;
2169 distance=fabs((double) GetPixelGreen(p)-(double) GetPixelGreen(q));
2170 mean_error_per_pixel+=distance;
2171 mean_error+=distance*distance;
2172 if (distance > maximum_error)
2173 maximum_error=distance;
2174 area++;
2175 distance=fabs((double) GetPixelBlue(p)-(double) GetPixelBlue(q));
2176 mean_error_per_pixel+=distance;
2177 mean_error+=distance*distance;
2178 if (distance > maximum_error)
2179 maximum_error=distance;
2180 area++;
2181 if (image->matte != MagickFalse)
2182 {
2183 distance=fabs((double) GetPixelOpacity(p)-(double)
2184 GetPixelOpacity(q));
2185 mean_error_per_pixel+=distance;
2186 mean_error+=distance*distance;
2187 if (distance > maximum_error)
2188 maximum_error=distance;
2189 area++;
2190 }
2191 if ((image->colorspace == CMYKColorspace) &&
2192 (reconstruct_image->colorspace == CMYKColorspace))
2193 {
2194 distance=fabs((double) GetPixelIndex(indexes+x)-(double)
2195 GetPixelIndex(reconstruct_indexes+x));
2196 mean_error_per_pixel+=distance;
2197 mean_error+=distance*distance;
2198 if (distance > maximum_error)
2199 maximum_error=distance;
2200 area++;
2201 }
2202 p++;
2203 q++;
2204 }
2205 }
2206 reconstruct_view=DestroyCacheView(reconstruct_view);
2207 image_view=DestroyCacheView(image_view);
2208 gamma=MagickSafeReciprocal(area);
2209 image->error.mean_error_per_pixel=gamma*mean_error_per_pixel;
2210 image->error.normalized_mean_error=gamma*QuantumScale*QuantumScale*mean_error;
2211 image->error.normalized_maximum_error=QuantumScale*maximum_error;
2212 status=image->error.mean_error_per_pixel == 0.0 ? MagickTrue : MagickFalse;
2213 return(status);
2214}
2215
2216/*
2217%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2218% %
2219% %
2220% %
2221% S i m i l a r i t y I m a g e %
2222% %
2223% %
2224% %
2225%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
2226%
2227% SimilarityImage() compares the reference image of the image and returns the
2228% best match offset. In addition, it returns a similarity image such that an
2229% exact match location is completely white and if none of the pixels match,
2230% black, otherwise some gray level in-between.
2231%
2232% The format of the SimilarityImageImage method is:
2233%
2234% Image *SimilarityImage(const Image *image,const Image *reference,
2235% RectangleInfo *offset,double *similarity,ExceptionInfo *exception)
2236%
2237% A description of each parameter follows:
2238%
2239% o image: the image.
2240%
2241% o reference: find an area of the image that closely resembles this image.
2242%
2243% o the best match offset of the reference image within the image.
2244%
2245% o similarity: the computed similarity between the images.
2246%
2247% o exception: return any errors or warnings in this structure.
2248%
2249*/
2250
2251static double GetSimilarityMetric(const Image *image,
2252 const Image *reconstruct_image,const MetricType metric,
2253 const ssize_t x_offset,const ssize_t y_offset,ExceptionInfo *exception)
2254{
2255 double
2256 *channel_similarity,
2257 similarity = 0.0;
2258
2259 ExceptionInfo
2260 *sans_exception = AcquireExceptionInfo();
2261
2262 Image
2263 *similarity_image;
2264
2265 MagickBooleanType
2266 status = MagickTrue;
2267
2268 RectangleInfo
2269 geometry;
2270
2271 size_t
2272 length = CompositeChannels+1UL;
2273
2274 SetGeometry(reconstruct_image,&geometry);
2275 geometry.x=x_offset;
2276 geometry.y=y_offset;
2277 similarity_image=CropImage(image,&geometry,sans_exception);
2278 sans_exception=DestroyExceptionInfo(sans_exception);
2279 if (similarity_image == (Image *) NULL)
2280 return(NAN);
2281 /*
2282 Get image distortion.
2283 */
2284 channel_similarity=(double *) AcquireQuantumMemory(length,
2285 sizeof(*channel_similarity));
2286 if (channel_similarity == (double *) NULL)
2287 ThrowFatalException(ResourceLimitFatalError,"MemoryAllocationFailed");
2288 (void) memset(channel_similarity,0,length*sizeof(*channel_similarity));
2289 switch (metric)
2290 {
2291 case AbsoluteErrorMetric:
2292 {
2293 status=GetAESimilarity(similarity_image,reconstruct_image,
2294 CompositeChannels,channel_similarity,exception);
2295 break;
2296 }
2297 case FuzzErrorMetric:
2298 {
2299 status=GetFUZZSimilarity(similarity_image,reconstruct_image,
2300 CompositeChannels,channel_similarity,exception);
2301 break;
2302 }
2303 case MeanAbsoluteErrorMetric:
2304 {
2305 status=GetMAESimilarity(similarity_image,reconstruct_image,
2306 CompositeChannels,channel_similarity,exception);
2307 break;
2308 }
2309 case MeanErrorPerPixelMetric:
2310 {
2311 status=GetMEPPSimilarity(similarity_image,reconstruct_image,
2312 CompositeChannels,channel_similarity,exception);
2313 break;
2314 }
2315 case MeanSquaredErrorMetric:
2316 {
2317 status=GetMSESimilarity(similarity_image,reconstruct_image,
2318 CompositeChannels,channel_similarity,exception);
2319 break;
2320 }
2321 case NormalizedCrossCorrelationErrorMetric:
2322 {
2323 status=GetNCCSimilarity(similarity_image,reconstruct_image,
2324 CompositeChannels,channel_similarity,exception);
2325 break;
2326 }
2327 case PeakAbsoluteErrorMetric:
2328 {
2329 status=GetPASimilarity(similarity_image,reconstruct_image,
2330 CompositeChannels,channel_similarity,exception);
2331 break;
2332 }
2333 case PeakSignalToNoiseRatioMetric:
2334 {
2335 status=GetPSNRSimilarity(similarity_image,reconstruct_image,
2336 CompositeChannels,channel_similarity,exception);
2337 break;
2338 }
2339 case PerceptualHashErrorMetric:
2340 {
2341 status=GetPHASHSimilarity(similarity_image,reconstruct_image,
2342 CompositeChannels,channel_similarity,exception);
2343 break;
2344 }
2345 case PixelDifferenceCountErrorMetric:
2346 {
2347 status=GetPDCSimilarity(similarity_image,reconstruct_image,
2348 CompositeChannels,channel_similarity,exception);
2349 break;
2350 }
2351 case RootMeanSquaredErrorMetric:
2352 case UndefinedErrorMetric:
2353 default:
2354 {
2355 status=GetRMSESimilarity(similarity_image,reconstruct_image,
2356 CompositeChannels,channel_similarity,exception);
2357 break;
2358 }
2359 }
2360 similarity_image=DestroyImage(similarity_image);
2361 similarity=channel_similarity[CompositeChannels];
2362 channel_similarity=(double *) RelinquishMagickMemory(channel_similarity);
2363 if (status == MagickFalse)
2364 return(NAN);
2365 return(similarity);
2366}
2367
2368MagickExport Image *SimilarityImage(Image *image,const Image *reference,
2369 RectangleInfo *offset,double *similarity_metric,ExceptionInfo *exception)
2370{
2371 Image
2372 *similarity_image;
2373
2374 similarity_image=SimilarityMetricImage(image,reference,
2375 RootMeanSquaredErrorMetric,offset,similarity_metric,exception);
2376 return(similarity_image);
2377}
2378
2379MagickExport Image *SimilarityMetricImage(Image *image,const Image *reconstruct,
2380 const MetricType metric,RectangleInfo *offset,double *similarity_metric,
2381 ExceptionInfo *exception)
2382{
2383#define SimilarityImageTag "Similarity/Image"
2384
2385 typedef struct
2386 {
2387 double
2388 similarity;
2389
2390 ssize_t
2391 x,
2392 y;
2393 } SimilarityInfo;
2394
2395 CacheView
2396 *similarity_view;
2397
2398 const char
2399 *artifact;
2400
2401 double
2402 similarity_threshold;
2403
2404 Image
2405 *similarity_image = (Image *) NULL;
2406
2407 MagickBooleanType
2408 status;
2409
2410 MagickOffsetType
2411 progress;
2412
2413 SimilarityInfo
2414 similarity_info = { 0 };
2415
2416 ssize_t
2417 y;
2418
2419 assert(image != (const Image *) NULL);
2420 assert(image->signature == MagickCoreSignature);
2421 assert(exception != (ExceptionInfo *) NULL);
2422 assert(exception->signature == MagickCoreSignature);
2423 assert(offset != (RectangleInfo *) NULL);
2424 if (IsEventLogging() != MagickFalse)
2425 (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);
2426 SetGeometry(reconstruct,offset);
2427 *similarity_metric=0.0;
2428 offset->x=0;
2429 offset->y=0;
2430 if (ValidateImageMorphology(image,reconstruct) == MagickFalse)
2431 ThrowImageException(ImageError,"ImageMorphologyDiffers");
2432 if ((image->columns < reconstruct->columns) ||
2433 (image->rows < reconstruct->rows))
2434 {
2435 (void) ThrowMagickException(&image->exception,GetMagickModule(),
2436 OptionWarning,"GeometryDoesNotContainImage","`%s'",image->filename);
2437 return((Image *) NULL);
2438 }
2439 similarity_image=CloneImage(image,image->columns,image->rows,MagickTrue,
2440 exception);
2441 if (similarity_image == (Image *) NULL)
2442 return((Image *) NULL);
2443 similarity_image->depth=32;
2444 similarity_image->colorspace=GRAYColorspace;
2445 similarity_image->matte=MagickFalse;
2446 status=SetImageStorageClass(similarity_image,DirectClass);
2447 if (status == MagickFalse)
2448 {
2449 InheritException(exception,&similarity_image->exception);
2450 return(DestroyImage(similarity_image));
2451 }
2452 /*
2453 Measure similarity of reconstruction image against image.
2454 */
2455 similarity_threshold=DefaultSimilarityThreshold;
2456 artifact=GetImageArtifact(image,"compare:similarity-threshold");
2457 if (artifact != (const char *) NULL)
2458 similarity_threshold=StringToDouble(artifact,(char **) NULL);
2459 status=MagickTrue;
2460 similarity_info.similarity=GetSimilarityMetric(image,reconstruct,metric,
2461 similarity_info.x,similarity_info.y,exception);
2462 progress=0;
2463 similarity_view=AcquireVirtualCacheView(similarity_image,exception);
2464#if defined(MAGICKCORE_OPENMP_SUPPORT)
2465 #pragma omp parallel for schedule(static) shared(status,similarity_info) \
2466 magick_number_threads(image,reconstruct,similarity_image->rows << 2,1)
2467#endif
2468 for (y=0; y < (ssize_t) similarity_image->rows; y++)
2469 {
2470 double
2471 similarity;
2472
2473 MagickBooleanType
2474 threshold_trigger = MagickFalse;
2475
2476 PixelPacket
2477 *magick_restrict q;
2478
2479 SimilarityInfo
2480 channel_info = similarity_info;
2481
2482 ssize_t
2483 x;
2484
2485 if (status == MagickFalse)
2486 continue;
2487 if (threshold_trigger != MagickFalse)
2488 continue;
2489 q=QueueCacheViewAuthenticPixels(similarity_view,0,y,
2490 similarity_image->columns,1,exception);
2491 if (q == (PixelPacket *) NULL)
2492 {
2493 status=MagickFalse;
2494 continue;
2495 }
2496 for (x=0; x < (ssize_t) similarity_image->columns; x++)
2497 {
2498 MagickBooleanType
2499 update = MagickFalse;
2500
2501 similarity=GetSimilarityMetric(image,reconstruct,metric,x,y,exception);
2502 switch (metric)
2503 {
2504 case NormalizedCrossCorrelationErrorMetric:
2505 case PeakSignalToNoiseRatioMetric:
2506 {
2507 if (similarity > channel_info.similarity)
2508 update=MagickTrue;
2509 break;
2510 }
2511 default:
2512 {
2513 if (similarity < channel_info.similarity)
2514 update=MagickTrue;
2515 break;
2516 }
2517 }
2518 if (update != MagickFalse)
2519 {
2520 channel_info.similarity=similarity;
2521 channel_info.x=x;
2522 channel_info.y=y;
2523 }
2524 switch (metric)
2525 {
2526 case NormalizedCrossCorrelationErrorMetric:
2527 case PeakSignalToNoiseRatioMetric:
2528 {
2529 SetPixelRed(q,ClampToQuantum((double) QuantumRange*similarity));
2530 break;
2531 }
2532 default:
2533 {
2534 SetPixelRed(q,ClampToQuantum((double) QuantumRange*(1.0-similarity)));
2535 break;
2536 }
2537 }
2538 SetPixelGreen(q,GetPixelRed(q));
2539 SetPixelBlue(q,GetPixelRed(q));
2540 q++;
2541 }
2542#if defined(MAGICKCORE_OPENMP_SUPPORT)
2543 #pragma omp critical (MagickCore_SimilarityMetricImage)
2544#endif
2545 switch (metric)
2546 {
2547 case NormalizedCrossCorrelationErrorMetric:
2548 case PeakSignalToNoiseRatioMetric:
2549 {
2550 if (similarity_threshold != DefaultSimilarityThreshold)
2551 if (channel_info.similarity >= similarity_threshold)
2552 threshold_trigger=MagickTrue;
2553 if (channel_info.similarity >= similarity_info.similarity)
2554 similarity_info=channel_info;
2555 break;
2556 }
2557 default:
2558 {
2559 if (similarity_threshold != DefaultSimilarityThreshold)
2560 if (channel_info.similarity < similarity_threshold)
2561 threshold_trigger=MagickTrue;
2562 if (channel_info.similarity < similarity_info.similarity)
2563 similarity_info=channel_info;
2564 break;
2565 }
2566 }
2567 if (SyncCacheViewAuthenticPixels(similarity_view,exception) == MagickFalse)
2568 status=MagickFalse;
2569 if (image->progress_monitor != (MagickProgressMonitor) NULL)
2570 {
2571 MagickBooleanType
2572 proceed;
2573
2574 progress++;
2575 proceed=SetImageProgress(image,SimilarityImageTag,progress,image->rows);
2576 if (proceed == MagickFalse)
2577 status=MagickFalse;
2578 }
2579 }
2580 similarity_view=DestroyCacheView(similarity_view);
2581 if (status == MagickFalse)
2582 similarity_image=DestroyImage(similarity_image);
2583 *similarity_metric=similarity_info.similarity;
2584 if (fabs(*similarity_metric) < MagickEpsilon)
2585 *similarity_metric=0.0;
2586 offset->x=similarity_info.x;
2587 offset->y=similarity_info.y;
2588 (void) FormatImageProperty((Image *) image,"similarity","%.*g",
2589 GetMagickPrecision(),*similarity_metric);
2590 (void) FormatImageProperty((Image *) image,"similarity.offset.x","%.*g",
2591 GetMagickPrecision(),(double) offset->x);
2592 (void) FormatImageProperty((Image *) image,"similarity.offset.y","%.*g",
2593 GetMagickPrecision(),(double) offset->y);
2594 return(similarity_image);
2595}