File size: 5,757 Bytes
611206a
 
 
5888da9
611206a
 
 
 
 
 
5888da9
611206a
 
 
de894d3
 
611206a
 
5888da9
611206a
 
 
 
 
 
5888da9
611206a
 
 
 
 
 
5888da9
611206a
 
 
 
 
 
 
 
 
 
 
5888da9
611206a
 
 
5888da9
611206a
 
 
 
5888da9
611206a
 
5888da9
611206a
 
 
 
 
 
 
 
 
 
 
 
 
 
5888da9
611206a
 
 
 
de894d3
 
 
 
611206a
5888da9
611206a
 
5888da9
 
 
 
 
 
 
 
 
 
 
 
 
 
611206a
5888da9
611206a
5888da9
611206a
 
 
5888da9
611206a
 
 
 
 
 
 
 
 
 
 
5888da9
611206a
 
 
 
5888da9
611206a
 
5888da9
611206a
 
 
 
 
5888da9
611206a
 
 
 
5888da9
611206a
 
 
 
 
 
5888da9
611206a
 
5888da9
611206a
 
 
 
 
5888da9
611206a
 
 
 
 
 
5888da9
611206a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
import cv2
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patheffects as path_effects
from typing import Any, List, Dict, Tuple, Optional
import io
from PIL import Image

class VisualizationHelper:
    """Helper class for visualizing detection results"""

    @staticmethod
    def visualize_detection(image: Any, result: Any, color_mapper: Optional[Any] = None,
                            figsize: Tuple[int, int] = (12, 12),
                            return_pil: bool = False,
                            filter_classes: Optional[List[int]] = None) -> Optional[Image.Image]:
        """
        Visualize detection results on a single image

        Args:
            image: Image path or numpy array
            result: Detection result object
            color_mapper: ColorMapper instance for consistent colors
            figsize: Figure size
            return_pil: If True, returns a PIL Image object

        Returns:
            PIL Image if return_pil is True, otherwise displays the plot
        """
        if result is None:
            print('No data for visualization')
            return None

        # Read image if path is provided
        if isinstance(image, str):
            img = cv2.imread(image)
            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        else:
            img = image
            if len(img.shape) == 3 and img.shape[2] == 3:
                # Check if BGR format (OpenCV) and convert to RGB if needed
                if isinstance(img, np.ndarray):
                    # Assuming BGR format from OpenCV
                    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

        # Create figure
        fig, ax = plt.subplots(figsize=figsize)
        ax.imshow(img)

        # Get bounding boxes, classes and confidences
        boxes = result.boxes.xyxy.cpu().numpy()
        classes = result.boxes.cls.cpu().numpy()
        confs = result.boxes.conf.cpu().numpy()

        # Get class names
        names = result.names

        # Create a default color mapper if none is provided
        if color_mapper is None:
            # For backward compatibility, fallback to a simple color function
            from matplotlib import colormaps
            cmap = colormaps['tab10']
            def get_color(class_id):
                return cmap(class_id % 10)
        else:
            # Use the provided color mapper
            def get_color(class_id):
                hex_color = color_mapper.get_color(class_id)
                # Convert hex to RGB float values for matplotlib
                hex_color = hex_color.lstrip('#')
                return tuple(int(hex_color[i:i+2], 16) / 255 for i in (0, 2, 4)) + (1.0,)

        # Draw detection results
        for box, cls, conf in zip(boxes, classes, confs):
            x1, y1, x2, y2 = box
            cls_id = int(cls)
            
            if filter_classes and cls_id not in filter_classes:
                continue

            cls_name = names[cls_id]

            # Get color for this class
            box_color = get_color(cls_id)

            box_width = x2 - x1
            box_height = y2 - y1
            box_area = box_width * box_height

            # 根據框大小調整字體大小,但有限制
            adaptive_fontsize = max(10, min(14, int(10 + box_area / 10000)))


            ax.text(x1, y1 - 8, f'{cls_name}: {conf:.2f}',
                    color='white', fontsize=adaptive_fontsize, fontweight="bold",
                    bbox=dict(facecolor=box_color[:3], alpha=0.85, pad=3, boxstyle="round,pad=0.3"),
                    path_effects=[path_effects.withStroke(linewidth=1.5, foreground="black")])

            # Add bounding box
            ax.add_patch(plt.Rectangle((x1, y1), x2-x1, y2-y1,
                                    fill=False, edgecolor=box_color[:3], linewidth=2))

        ax.axis('off')
        # ax.set_title('Detection Result')
        plt.tight_layout()

        if return_pil:
            # Convert plot to PIL Image
            buf = io.BytesIO()
            fig.savefig(buf, format='png', bbox_inches='tight', pad_inches=0)
            buf.seek(0)
            pil_img = Image.open(buf)
            plt.close(fig)
            return pil_img
        else:
            plt.show()
            return None

    @staticmethod
    def create_summary(result: Any) -> Dict:
        """
        Create a summary of detection results

        Args:
            result: Detection result object

        Returns:
            Dictionary with detection summary statistics
        """
        if result is None:
            return {"error": "No detection result provided"}

        # Get classes and confidences
        classes = result.boxes.cls.cpu().numpy().astype(int)
        confidences = result.boxes.conf.cpu().numpy()
        names = result.names

        # Count detections by class
        class_counts = {}
        for cls, conf in zip(classes, confidences):
            cls_name = names[int(cls)]
            if cls_name not in class_counts:
                class_counts[cls_name] = {"count": 0, "confidences": []}

            class_counts[cls_name]["count"] += 1
            class_counts[cls_name]["confidences"].append(float(conf))

        # Calculate average confidence for each class
        for cls_name, stats in class_counts.items():
            if stats["confidences"]:
                stats["average_confidence"] = float(np.mean(stats["confidences"]))
                stats.pop("confidences")  # Remove detailed confidences list to keep summary concise

        # Prepare summary
        summary = {
            "total_objects": len(classes),
            "class_counts": class_counts,
            "unique_classes": len(class_counts)
        }

        return summary