| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  | # Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved. | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
					
						
							|  |  |  | # you may not use this file except in compliance with the License. | 
					
						
							|  |  |  | # You may obtain a copy of the License at | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | #     http://www.apache.org/licenses/LICENSE-2.0 | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Unless required by applicable law or agreed to in writing, software | 
					
						
							|  |  |  | # distributed under the License is distributed on an "AS IS" BASIS, | 
					
						
							|  |  |  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
					
						
							|  |  |  | # See the License for the specific language governing permissions and | 
					
						
							|  |  |  | # limitations under the License. | 
					
						
							| 
									
										
										
										
											2021-11-04 17:34:56 +08:00
										 |  |  | """
 | 
					
						
							| 
									
										
										
										
											2021-11-04 17:40:04 +08:00
										 |  |  | This code is refered from: | 
					
						
							| 
									
										
										
										
											2021-11-04 17:34:56 +08:00
										 |  |  | https://github.com/WenmuZhou/DBNet.pytorch/blob/master/post_processing/seg_detector_representer.py | 
					
						
							|  |  |  | """
 | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  | from __future__ import absolute_import | 
					
						
							|  |  |  | from __future__ import division | 
					
						
							|  |  |  | from __future__ import print_function | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import numpy as np | 
					
						
							|  |  |  | import cv2 | 
					
						
							| 
									
										
										
										
											2020-11-09 18:19:42 +08:00
										 |  |  | import paddle | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  | from shapely.geometry import Polygon | 
					
						
							|  |  |  | import pyclipper | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class DBPostProcess(object): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     The post process for Differentiable Binarization (DB). | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-13 17:13:33 +08:00
										 |  |  |     def __init__(self, | 
					
						
							|  |  |  |                  thresh=0.3, | 
					
						
							|  |  |  |                  box_thresh=0.7, | 
					
						
							|  |  |  |                  max_candidates=1000, | 
					
						
							|  |  |  |                  unclip_ratio=2.0, | 
					
						
							| 
									
										
										
										
											2020-12-09 20:26:40 +08:00
										 |  |  |                  use_dilation=False, | 
					
						
							| 
									
										
										
										
											2021-04-23 07:43:16 +00:00
										 |  |  |                  score_mode="fast", | 
					
						
							| 
									
										
										
										
											2022-10-09 11:28:32 +08:00
										 |  |  |                  box_type='quad', | 
					
						
							| 
									
										
										
										
											2020-10-13 17:13:33 +08:00
										 |  |  |                  **kwargs): | 
					
						
							|  |  |  |         self.thresh = thresh | 
					
						
							|  |  |  |         self.box_thresh = box_thresh | 
					
						
							|  |  |  |         self.max_candidates = max_candidates | 
					
						
							|  |  |  |         self.unclip_ratio = unclip_ratio | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  |         self.min_size = 3 | 
					
						
							| 
									
										
										
										
											2021-04-23 07:43:16 +00:00
										 |  |  |         self.score_mode = score_mode | 
					
						
							| 
									
										
										
										
											2022-10-09 11:28:32 +08:00
										 |  |  |         self.box_type = box_type | 
					
						
							| 
									
										
										
										
											2021-04-23 07:43:16 +00:00
										 |  |  |         assert score_mode in [ | 
					
						
							|  |  |  |             "slow", "fast" | 
					
						
							|  |  |  |         ], "Score mode must be in [slow, fast] but got: {}".format(score_mode) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-15 23:49:50 +08:00
										 |  |  |         self.dilation_kernel = None if not use_dilation else np.array( | 
					
						
							|  |  |  |             [[1, 1], [1, 1]]) | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-04 08:40:32 +00:00
										 |  |  |     def polygons_from_bitmap(self, pred, _bitmap, dest_width, dest_height): | 
					
						
							|  |  |  |         '''
 | 
					
						
							|  |  |  |         _bitmap: single map with shape (1, H, W), | 
					
						
							|  |  |  |             whose values are binarized as {0, 1} | 
					
						
							|  |  |  |         '''
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         bitmap = _bitmap | 
					
						
							|  |  |  |         height, width = bitmap.shape | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         boxes = [] | 
					
						
							|  |  |  |         scores = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         contours, _ = cv2.findContours((bitmap * 255).astype(np.uint8), | 
					
						
							|  |  |  |                                        cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         for contour in contours[:self.max_candidates]: | 
					
						
							|  |  |  |             epsilon = 0.002 * cv2.arcLength(contour, True) | 
					
						
							|  |  |  |             approx = cv2.approxPolyDP(contour, epsilon, True) | 
					
						
							|  |  |  |             points = approx.reshape((-1, 2)) | 
					
						
							|  |  |  |             if points.shape[0] < 4: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             score = self.box_score_fast(pred, points.reshape(-1, 2)) | 
					
						
							|  |  |  |             if self.box_thresh > score: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             if points.shape[0] > 2: | 
					
						
							|  |  |  |                 box = self.unclip(points, self.unclip_ratio) | 
					
						
							|  |  |  |                 if len(box) > 1: | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             box = box.reshape(-1, 2) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             _, sside = self.get_mini_boxes(box.reshape((-1, 1, 2))) | 
					
						
							|  |  |  |             if sside < self.min_size + 2: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             box = np.array(box) | 
					
						
							|  |  |  |             box[:, 0] = np.clip( | 
					
						
							|  |  |  |                 np.round(box[:, 0] / width * dest_width), 0, dest_width) | 
					
						
							|  |  |  |             box[:, 1] = np.clip( | 
					
						
							|  |  |  |                 np.round(box[:, 1] / height * dest_height), 0, dest_height) | 
					
						
							|  |  |  |             boxes.append(box.tolist()) | 
					
						
							|  |  |  |             scores.append(score) | 
					
						
							|  |  |  |         return boxes, scores | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  |     def boxes_from_bitmap(self, pred, _bitmap, dest_width, dest_height): | 
					
						
							|  |  |  |         '''
 | 
					
						
							|  |  |  |         _bitmap: single map with shape (1, H, W), | 
					
						
							|  |  |  |                 whose values are binarized as {0, 1} | 
					
						
							|  |  |  |         '''
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         bitmap = _bitmap | 
					
						
							|  |  |  |         height, width = bitmap.shape | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-25 16:29:20 +08:00
										 |  |  |         outs = cv2.findContours((bitmap * 255).astype(np.uint8), cv2.RETR_LIST, | 
					
						
							|  |  |  |                                 cv2.CHAIN_APPROX_SIMPLE) | 
					
						
							| 
									
										
										
										
											2020-05-14 13:49:28 +08:00
										 |  |  |         if len(outs) == 3: | 
					
						
							|  |  |  |             img, contours, _ = outs[0], outs[1], outs[2] | 
					
						
							|  |  |  |         elif len(outs) == 2: | 
					
						
							|  |  |  |             contours, _ = outs[0], outs[1] | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         num_contours = min(len(contours), self.max_candidates) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-13 17:13:33 +08:00
										 |  |  |         boxes = [] | 
					
						
							|  |  |  |         scores = [] | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  |         for index in range(num_contours): | 
					
						
							|  |  |  |             contour = contours[index] | 
					
						
							|  |  |  |             points, sside = self.get_mini_boxes(contour) | 
					
						
							|  |  |  |             if sside < self.min_size: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             points = np.array(points) | 
					
						
							| 
									
										
										
										
											2021-04-23 07:43:16 +00:00
										 |  |  |             if self.score_mode == "fast": | 
					
						
							|  |  |  |                 score = self.box_score_fast(pred, points.reshape(-1, 2)) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 score = self.box_score_slow(pred, contour) | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  |             if self.box_thresh > score: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-04 08:40:32 +00:00
										 |  |  |             box = self.unclip(points, self.unclip_ratio).reshape(-1, 1, 2) | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  |             box, sside = self.get_mini_boxes(box) | 
					
						
							|  |  |  |             if sside < self.min_size + 2: | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             box = np.array(box) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             box[:, 0] = np.clip( | 
					
						
							|  |  |  |                 np.round(box[:, 0] / width * dest_width), 0, dest_width) | 
					
						
							|  |  |  |             box[:, 1] = np.clip( | 
					
						
							|  |  |  |                 np.round(box[:, 1] / height * dest_height), 0, dest_height) | 
					
						
							| 
									
										
										
										
											2022-12-23 10:02:45 +08:00
										 |  |  |             boxes.append(box.astype("int32")) | 
					
						
							| 
									
										
										
										
											2020-10-13 17:13:33 +08:00
										 |  |  |             scores.append(score) | 
					
						
							| 
									
										
										
										
											2022-12-23 10:02:45 +08:00
										 |  |  |         return np.array(boxes, dtype="int32"), scores | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-07-04 08:40:32 +00:00
										 |  |  |     def unclip(self, box, unclip_ratio): | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  |         poly = Polygon(box) | 
					
						
							|  |  |  |         distance = poly.area * unclip_ratio / poly.length | 
					
						
							|  |  |  |         offset = pyclipper.PyclipperOffset() | 
					
						
							|  |  |  |         offset.AddPath(box, pyclipper.JT_ROUND, pyclipper.ET_CLOSEDPOLYGON) | 
					
						
							|  |  |  |         expanded = np.array(offset.Execute(distance)) | 
					
						
							|  |  |  |         return expanded | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_mini_boxes(self, contour): | 
					
						
							|  |  |  |         bounding_box = cv2.minAreaRect(contour) | 
					
						
							|  |  |  |         points = sorted(list(cv2.boxPoints(bounding_box)), key=lambda x: x[0]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         index_1, index_2, index_3, index_4 = 0, 1, 2, 3 | 
					
						
							|  |  |  |         if points[1][1] > points[0][1]: | 
					
						
							|  |  |  |             index_1 = 0 | 
					
						
							|  |  |  |             index_4 = 1 | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             index_1 = 1 | 
					
						
							|  |  |  |             index_4 = 0 | 
					
						
							|  |  |  |         if points[3][1] > points[2][1]: | 
					
						
							|  |  |  |             index_2 = 2 | 
					
						
							|  |  |  |             index_3 = 3 | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             index_2 = 3 | 
					
						
							|  |  |  |             index_3 = 2 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         box = [ | 
					
						
							|  |  |  |             points[index_1], points[index_2], points[index_3], points[index_4] | 
					
						
							|  |  |  |         ] | 
					
						
							|  |  |  |         return box, min(bounding_box[1]) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def box_score_fast(self, bitmap, _box): | 
					
						
							| 
									
										
										
										
											2021-04-23 07:43:16 +00:00
										 |  |  |         '''
 | 
					
						
							|  |  |  |         box_score_fast: use bbox mean score as the mean score | 
					
						
							|  |  |  |         '''
 | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  |         h, w = bitmap.shape[:2] | 
					
						
							|  |  |  |         box = _box.copy() | 
					
						
							| 
									
										
										
										
											2022-12-23 10:02:45 +08:00
										 |  |  |         xmin = np.clip(np.floor(box[:, 0].min()).astype("int32"), 0, w - 1) | 
					
						
							|  |  |  |         xmax = np.clip(np.ceil(box[:, 0].max()).astype("int32"), 0, w - 1) | 
					
						
							|  |  |  |         ymin = np.clip(np.floor(box[:, 1].min()).astype("int32"), 0, h - 1) | 
					
						
							|  |  |  |         ymax = np.clip(np.ceil(box[:, 1].max()).astype("int32"), 0, h - 1) | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  |         mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8) | 
					
						
							|  |  |  |         box[:, 0] = box[:, 0] - xmin | 
					
						
							|  |  |  |         box[:, 1] = box[:, 1] - ymin | 
					
						
							| 
									
										
										
										
											2022-12-23 10:02:45 +08:00
										 |  |  |         cv2.fillPoly(mask, box.reshape(1, -1, 2).astype("int32"), 1) | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  |         return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-23 07:43:16 +00:00
										 |  |  |     def box_score_slow(self, bitmap, contour): | 
					
						
							|  |  |  |         '''
 | 
					
						
							|  |  |  |         box_score_slow: use polyon mean score as the mean score | 
					
						
							|  |  |  |         '''
 | 
					
						
							|  |  |  |         h, w = bitmap.shape[:2] | 
					
						
							|  |  |  |         contour = contour.copy() | 
					
						
							|  |  |  |         contour = np.reshape(contour, (-1, 2)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         xmin = np.clip(np.min(contour[:, 0]), 0, w - 1) | 
					
						
							|  |  |  |         xmax = np.clip(np.max(contour[:, 0]), 0, w - 1) | 
					
						
							|  |  |  |         ymin = np.clip(np.min(contour[:, 1]), 0, h - 1) | 
					
						
							|  |  |  |         ymax = np.clip(np.max(contour[:, 1]), 0, h - 1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         mask = np.zeros((ymax - ymin + 1, xmax - xmin + 1), dtype=np.uint8) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         contour[:, 0] = contour[:, 0] - xmin | 
					
						
							|  |  |  |         contour[:, 1] = contour[:, 1] - ymin | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-23 10:02:45 +08:00
										 |  |  |         cv2.fillPoly(mask, contour.reshape(1, -1, 2).astype("int32"), 1) | 
					
						
							| 
									
										
										
										
											2021-04-23 07:43:16 +00:00
										 |  |  |         return cv2.mean(bitmap[ymin:ymax + 1, xmin:xmax + 1], mask)[0] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-12-15 23:49:50 +08:00
										 |  |  |     def __call__(self, outs_dict, shape_list): | 
					
						
							|  |  |  |         pred = outs_dict['maps'] | 
					
						
							| 
									
										
										
										
											2020-11-09 18:19:42 +08:00
										 |  |  |         if isinstance(pred, paddle.Tensor): | 
					
						
							|  |  |  |             pred = pred.numpy() | 
					
						
							|  |  |  |         pred = pred[:, 0, :, :] | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  |         segmentation = pred > self.thresh | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         boxes_batch = [] | 
					
						
							|  |  |  |         for batch_index in range(pred.shape[0]): | 
					
						
							| 
									
										
										
										
											2020-12-09 20:44:43 +08:00
										 |  |  |             src_h, src_w, ratio_h, ratio_w = shape_list[batch_index] | 
					
						
							| 
									
										
										
										
											2020-12-09 20:26:40 +08:00
										 |  |  |             if self.dilation_kernel is not None: | 
					
						
							|  |  |  |                 mask = cv2.dilate( | 
					
						
							|  |  |  |                     np.array(segmentation[batch_index]).astype(np.uint8), | 
					
						
							|  |  |  |                     self.dilation_kernel) | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 mask = segmentation[batch_index] | 
					
						
							| 
									
										
										
										
											2022-10-10 10:12:28 +08:00
										 |  |  |             if self.box_type == 'poly': | 
					
						
							| 
									
										
										
										
											2022-07-04 08:40:32 +00:00
										 |  |  |                 boxes, scores = self.polygons_from_bitmap(pred[batch_index], | 
					
						
							|  |  |  |                                                           mask, src_w, src_h) | 
					
						
							| 
									
										
										
										
											2022-10-10 10:12:28 +08:00
										 |  |  |             elif self.box_type == 'quad': | 
					
						
							| 
									
										
										
										
											2022-07-04 08:40:32 +00:00
										 |  |  |                 boxes, scores = self.boxes_from_bitmap(pred[batch_index], mask, | 
					
						
							|  |  |  |                                                        src_w, src_h) | 
					
						
							| 
									
										
										
										
											2022-10-09 11:28:32 +08:00
										 |  |  |             else: | 
					
						
							|  |  |  |                 raise ValueError("box_type can only be one of ['quad', 'poly']") | 
					
						
							| 
									
										
										
										
											2020-05-10 16:26:57 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-13 17:13:33 +08:00
										 |  |  |             boxes_batch.append({'points': boxes}) | 
					
						
							| 
									
										
										
										
											2020-12-09 14:59:04 +08:00
										 |  |  |         return boxes_batch | 
					
						
							| 
									
										
										
										
											2021-07-07 01:54:03 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-08 14:32:44 +00:00
										 |  |  | class DistillationDBPostProcess(object): | 
					
						
							| 
									
										
										
										
											2021-11-04 17:34:56 +08:00
										 |  |  |     def __init__(self, | 
					
						
							|  |  |  |                  model_name=["student"], | 
					
						
							| 
									
										
										
										
											2021-07-07 01:54:03 +00:00
										 |  |  |                  key=None, | 
					
						
							|  |  |  |                  thresh=0.3, | 
					
						
							| 
									
										
										
										
											2021-07-08 14:32:44 +00:00
										 |  |  |                  box_thresh=0.6, | 
					
						
							| 
									
										
										
										
											2021-07-07 01:54:03 +00:00
										 |  |  |                  max_candidates=1000, | 
					
						
							| 
									
										
										
										
											2021-07-08 14:32:44 +00:00
										 |  |  |                  unclip_ratio=1.5, | 
					
						
							| 
									
										
										
										
											2021-07-07 01:54:03 +00:00
										 |  |  |                  use_dilation=False, | 
					
						
							|  |  |  |                  score_mode="fast", | 
					
						
							| 
									
										
										
										
											2022-10-09 11:28:32 +08:00
										 |  |  |                  box_type='quad', | 
					
						
							| 
									
										
										
										
											2021-07-07 01:54:03 +00:00
										 |  |  |                  **kwargs): | 
					
						
							|  |  |  |         self.model_name = model_name | 
					
						
							|  |  |  |         self.key = key | 
					
						
							| 
									
										
										
										
											2021-11-04 17:34:56 +08:00
										 |  |  |         self.post_process = DBPostProcess( | 
					
						
							|  |  |  |             thresh=thresh, | 
					
						
							|  |  |  |             box_thresh=box_thresh, | 
					
						
							|  |  |  |             max_candidates=max_candidates, | 
					
						
							|  |  |  |             unclip_ratio=unclip_ratio, | 
					
						
							|  |  |  |             use_dilation=use_dilation, | 
					
						
							| 
									
										
										
										
											2022-07-04 08:40:32 +00:00
										 |  |  |             score_mode=score_mode, | 
					
						
							| 
									
										
										
										
											2022-10-09 11:28:32 +08:00
										 |  |  |             box_type=box_type) | 
					
						
							| 
									
										
										
										
											2021-07-07 01:54:03 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-07-07 07:54:02 +00:00
										 |  |  |     def __call__(self, predicts, shape_list): | 
					
						
							| 
									
										
										
										
											2021-07-07 01:54:03 +00:00
										 |  |  |         results = {} | 
					
						
							| 
									
										
										
										
											2021-07-08 14:32:44 +00:00
										 |  |  |         for k in self.model_name: | 
					
						
							|  |  |  |             results[k] = self.post_process(predicts[k], shape_list=shape_list) | 
					
						
							| 
									
										
										
										
											2021-07-07 01:54:03 +00:00
										 |  |  |         return results |