1- # -*- coding: utf-8 -*- 
21import  warnings 
32
3+ import  cv2 
44import  numpy  as  np 
55from  scipy .ndimage  import  convolve 
66from  scipy .ndimage  import  distance_transform_edt  as  bwdist 
7+ from  skimage  import  measure , morphology 
78
89from  .utils  import  EPS , TYPE , get_adaptive_threshold , validate_and_normalize_input 
910
@@ -380,9 +381,7 @@ def cal_em_with_threshold(self, pred: np.ndarray, gt: np.ndarray, threshold: flo
380381            results_parts  =  []
381382            for  i , (part_numel , combination ) in  enumerate (zip (parts_numel , combinations )):
382383                align_matrix_value  =  (
383-                     2 
384-                     *  (combination [0 ] *  combination [1 ])
385-                     /  (combination [0 ] **  2  +  combination [1 ] **  2  +  EPS )
384+                     2  *  (combination [0 ] *  combination [1 ]) /  (combination [0 ] **  2  +  combination [1 ] **  2  +  EPS )
386385                )
387386                enhanced_matrix_value  =  (align_matrix_value  +  1 ) **  2  /  4 
388387                results_parts .append (enhanced_matrix_value  *  part_numel )
@@ -424,9 +423,7 @@ def cal_em_with_cumsumhistogram(self, pred: np.ndarray, gt: np.ndarray) -> np.nd
424423            results_parts  =  np .empty (shape = (4 , 256 ), dtype = np .float64 )
425424            for  i , (part_numel , combination ) in  enumerate (zip (parts_numel_w_thrs , combinations )):
426425                align_matrix_value  =  (
427-                     2 
428-                     *  (combination [0 ] *  combination [1 ])
429-                     /  (combination [0 ] **  2  +  combination [1 ] **  2  +  EPS )
426+                     2  *  (combination [0 ] *  combination [1 ]) /  (combination [0 ] **  2  +  combination [1 ] **  2  +  EPS )
430427                )
431428                enhanced_matrix_value  =  (align_matrix_value  +  1 ) **  2  /  4 
432429                results_parts [i ] =  enhanced_matrix_value  *  part_numel 
@@ -435,9 +432,7 @@ def cal_em_with_cumsumhistogram(self, pred: np.ndarray, gt: np.ndarray) -> np.nd
435432        em  =  enhanced_matrix_sum  /  (self .gt_size  -  1  +  EPS )
436433        return  em 
437434
438-     def  generate_parts_numel_combinations (
439-         self , fg_fg_numel , fg_bg_numel , pred_fg_numel , pred_bg_numel 
440-     ):
435+     def  generate_parts_numel_combinations (self , fg_fg_numel , fg_bg_numel , pred_fg_numel , pred_bg_numel ):
441436        bg_fg_numel  =  self .gt_fg_numel  -  fg_fg_numel 
442437        bg_bg_numel  =  pred_bg_numel  -  bg_fg_numel 
443438
@@ -571,3 +566,170 @@ def get_results(self) -> dict:
571566        """ 
572567        weighted_fm  =  np .mean (np .array (self .weighted_fms , dtype = TYPE ))
573568        return  dict (wfm = weighted_fm )
569+ 
570+ 
571+ class  HumanCorrectionEffortMeasure (object ):
572+     def  __init__ (self , relax : int  =  5 , epsilon : float  =  2.0 ):
573+         """Human Correction Effort Measure for Dichotomous Image Segmentation. 
574+ 
575+         ``` 
576+         @inproceedings{HumanCorrectionEffortMeasure, 
577+             title = {Highly Accurate Dichotomous Image Segmentation}, 
578+             author = {Xuebin Qin and Hang Dai and Xiaobin Hu and Deng-Ping Fan and Ling Shao and Luc Van Gool}, 
579+             booktitle = ECCV, 
580+             year = {2022} 
581+         } 
582+         ``` 
583+         """ 
584+ 
585+         self .hces  =  []
586+         self .relax  =  relax 
587+         self .epsilon  =  epsilon 
588+         self .morphology_kernel  =  morphology .disk (1 )
589+ 
590+     def  step (self , pred : np .ndarray , gt : np .ndarray , normalize : bool  =  True ):
591+         """Statistics the metric for the pair of pred and gt. 
592+ 
593+         Args: 
594+             pred (np.ndarray): Prediction, gray scale image. 
595+             gt (np.ndarray): Ground truth, gray scale image. 
596+             normalize (bool, optional): Whether to normalize the input data. Defaults to True. 
597+         """ 
598+         pred , gt  =  validate_and_normalize_input (pred , gt , normalize )
599+ 
600+         hce  =  self .cal_hce (pred , gt )
601+         self .hces .append (hce )
602+ 
603+     def  cal_hce (self , pred : np .ndarray , gt : np .ndarray ) ->  float :
604+         gt_skeleton  =  morphology .skeletonize (gt ).astype (bool )
605+         pred  =  pred  >  0.5 
606+ 
607+         union  =  np .logical_or (gt , pred )
608+         TP  =  np .logical_and (gt , pred )
609+         FP  =  np .logical_xor (pred , TP )
610+         FN  =  np .logical_xor (gt , TP )
611+ 
612+         # relax the union of gt and pred 
613+         eroded_union  =  cv2 .erode (union .astype (np .uint8 ), self .morphology_kernel , iterations = self .relax )
614+ 
615+         # get the relaxed FP regions for computing the human efforts in correcting them --- 
616+         FP_  =  np .logical_and (FP , eroded_union )  # get the relaxed FP 
617+         for  i  in  range (0 , self .relax ):
618+             FP_  =  cv2 .dilate (FP_ .astype (np .uint8 ), self .morphology_kernel )
619+             FP_  =  np .logical_and (FP_ .astype (bool ), ~ gt )
620+         FP_  =  np .logical_and (FP , FP_ )
621+ 
622+         # get the relaxed FN regions for computing the human efforts in correcting them --- 
623+         FN_  =  np .logical_and (FN , eroded_union )  # preserve the structural components of FN 
624+         # recover the FN, where pixels are not close to the TP borders 
625+         for  i  in  range (0 , self .relax ):
626+             FN_  =  cv2 .dilate (FN_ .astype (np .uint8 ), self .morphology_kernel )
627+             FN_  =  np .logical_and (FN_ , ~ pred )
628+         FN_  =  np .logical_and (FN , FN_ )
629+         # preserve the structural components of FN 
630+         FN_  =  np .logical_or (FN_ , np .logical_xor (gt_skeleton , np .logical_and (TP , gt_skeleton )))
631+ 
632+         # Find exact polygon control points and independent regions. 
633+         # find contours from FP_ and control points and independent regions for human correction 
634+         contours_FP , _  =  cv2 .findContours (FP_ .astype (np .uint8 ), cv2 .RETR_TREE , cv2 .CHAIN_APPROX_NONE )
635+         condition_FP  =  np .logical_or (TP , FN_ )
636+         bdies_FP , indep_cnt_FP  =  self .filter_conditional_boundary (contours_FP , FP_ , condition_FP )
637+         # find contours from FN_ and control points and independent regions for human correction 
638+         contours_FN , _  =  cv2 .findContours (FN_ .astype (np .uint8 ), cv2 .RETR_TREE , cv2 .CHAIN_APPROX_NONE )
639+         condition_FN  =  1  -  np .logical_or (np .logical_or (TP , FP_ ), FN_ )
640+         bdies_FN , indep_cnt_FN  =  self .filter_conditional_boundary (contours_FN , FN_ , condition_FN )
641+ 
642+         poly_FP_point_cnt  =  self .count_polygon_control_points (bdies_FP , epsilon = self .epsilon )
643+         poly_FN_point_cnt  =  self .count_polygon_control_points (bdies_FN , epsilon = self .epsilon )
644+         return  poly_FP_point_cnt  +  indep_cnt_FP  +  poly_FN_point_cnt  +  indep_cnt_FN 
645+ 
646+     def  filter_conditional_boundary (self , contours : list , mask : np .ndarray , condition : np .ndarray ):
647+         """ 
648+         Filter boundary segments based on a given condition mask and compute 
649+         the number of independent connected regions that require human correction. 
650+ 
651+         Args: 
652+             contours (List[np.ndarray]): List of boundary contours (OpenCV format). 
653+             mask (np.ndarray): Binary mask representing the region of interest. 
654+             condition (np.ndarray): Condition mask used to determine which 
655+                 boundary points need to be considered. 
656+ 
657+         Returns: 
658+             Tuple[List[np.ndarray], int]: 
659+                 - boundaries (List[np.ndarray]): Filtered boundary segments that require correction. 
660+                 - independent_count (int): Number of independent connected regions 
661+                 that need correction (i.e., human editing effort). 
662+         """ 
663+         condition  =  cv2 .dilate (condition .astype (np .uint8 ), self .morphology_kernel )
664+ 
665+         labels  =  measure .label (mask )  # find the connected regions 
666+         independent_flags  =  np .ones (labels .max () +  1 , dtype = int )  # the label of each connected regions 
667+         independent_flags [0 ] =  0   # 0 indicate the background region 
668+ 
669+         boundaries  =  []
670+         visited_map  =  np .zeros (condition .shape [:2 ], dtype = int )
671+         for  i  in  range (len (contours )):
672+             temp_boundaries  =  []
673+             temp_boundary  =  []
674+             for  pt  in  contours [i ]:
675+                 row , col  =  pt [0 , 1 ], pt [0 , 0 ]
676+ 
677+                 if  condition [row , col ].sum () ==  0  or  visited_map [row , col ] !=  0 :
678+                     if  temp_boundary :  # if the previous point is not a boundary point, append the previous boundary 
679+                         temp_boundaries .append (temp_boundary )
680+                         temp_boundary  =  []
681+                     continue 
682+ 
683+                 temp_boundary .append ([col , row ])
684+                 visited_map [row , col ] =  visited_map [row , col ] +  1 
685+                 independent_flags [labels [row , col ]] =  0   # mark region as requiring correction 
686+ 
687+             if  temp_boundary :
688+                 temp_boundaries .append (temp_boundary )
689+ 
690+             # check if the first and the last boundaries are connected. 
691+             # if yes, invert the first boundary and attach it after the last boundary 
692+             if  len (temp_boundaries ) >  1 :
693+                 first_x , first_y  =  temp_boundaries [0 ][0 ]
694+                 last_x , last_y  =  temp_boundaries [- 1 ][- 1 ]
695+                 if  (
696+                     (abs (first_x  -  last_x ) ==  1  and  first_y  ==  last_y )
697+                     or  (first_x  ==  last_x  and  abs (first_y  -  last_y ) ==  1 )
698+                     or  (abs (first_x  -  last_x ) ==  1  and  abs (first_y  -  last_y ) ==  1 )
699+                 ):
700+                     temp_boundaries [- 1 ].extend (temp_boundaries [0 ][::- 1 ])
701+                     del  temp_boundaries [0 ]
702+ 
703+             for  k  in  range (len (temp_boundaries )):
704+                 temp_boundaries [k ] =  np .array (temp_boundaries [k ])[:, np .newaxis , :]
705+ 
706+             if  temp_boundaries :
707+                 boundaries .extend (temp_boundaries )
708+         return  boundaries , independent_flags .sum ()
709+ 
710+     def  count_polygon_control_points (self , boundaries : list , epsilon : float  =  1.0 ) ->  int :
711+         """ 
712+         Approximate each boundary using the Ramer-Douglas-Peucker (RDP) algorithm 
713+         and count the total number of control points of all approximated polygons. 
714+ 
715+         Args: 
716+             boundaries (List[np.ndarray]): List of boundary contours. 
717+                 Each contour is an Nx1x2 numpy array (OpenCV contour format). 
718+             epsilon (float): RDP approximation tolerance. 
719+                 Larger values result in fewer control points. 
720+ 
721+         Returns: 
722+             int: The total number of control points across all approximated polygons. 
723+ 
724+         Reference: 
725+             https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm 
726+         """ 
727+         num_points  =  0 
728+         for  boundary  in  boundaries :
729+             approx_poly  =  cv2 .approxPolyDP (boundary , epsilon , False )  # approximate boundary 
730+             num_points  +=  len (approx_poly )  # count vertices (control points) 
731+         return  num_points 
732+ 
733+     def  get_results (self ) ->  dict :
734+         hce  =  np .mean (np .array (self .hces , dtype = TYPE ))
735+         return  dict (hce = hce )
0 commit comments