t29mato commited on
Commit
27c91ef
·
0 Parent(s):

Initial deploy: AutoLineDigitizer Gradio API for HuggingFace Spaces

Browse files

- Gradio API server for chart line extraction
- Models auto-downloaded from GitHub Releases at startup
- Supports StarryDigitizer JSON/ZIP and raw JSON output
- Includes LineFormer and ChartDete inference modules

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +6 -0
  2. LICENSE +48 -0
  3. README.md +70 -0
  4. api_server.py +446 -0
  5. config/chartdete_config.py +474 -0
  6. requirements.txt +14 -0
  7. src/app.py +843 -0
  8. src/chartdete_infer.py +711 -0
  9. submodules/chartdete/mmdet/__init__.py +29 -0
  10. submodules/chartdete/mmdet/apis/__init__.py +12 -0
  11. submodules/chartdete/mmdet/apis/inference.py +257 -0
  12. submodules/chartdete/mmdet/apis/test.py +209 -0
  13. submodules/chartdete/mmdet/apis/train.py +246 -0
  14. submodules/chartdete/mmdet/core/__init__.py +10 -0
  15. submodules/chartdete/mmdet/core/anchor/__init__.py +14 -0
  16. submodules/chartdete/mmdet/core/anchor/anchor_generator.py +866 -0
  17. submodules/chartdete/mmdet/core/anchor/builder.py +19 -0
  18. submodules/chartdete/mmdet/core/anchor/point_generator.py +263 -0
  19. submodules/chartdete/mmdet/core/anchor/utils.py +72 -0
  20. submodules/chartdete/mmdet/core/bbox/__init__.py +28 -0
  21. submodules/chartdete/mmdet/core/bbox/assigners/__init__.py +25 -0
  22. submodules/chartdete/mmdet/core/bbox/assigners/approx_max_iou_assigner.py +146 -0
  23. submodules/chartdete/mmdet/core/bbox/assigners/ascend_assign_result.py +34 -0
  24. submodules/chartdete/mmdet/core/bbox/assigners/ascend_max_iou_assigner.py +178 -0
  25. submodules/chartdete/mmdet/core/bbox/assigners/assign_result.py +206 -0
  26. submodules/chartdete/mmdet/core/bbox/assigners/atss_assigner.py +234 -0
  27. submodules/chartdete/mmdet/core/bbox/assigners/base_assigner.py +10 -0
  28. submodules/chartdete/mmdet/core/bbox/assigners/center_region_assigner.py +336 -0
  29. submodules/chartdete/mmdet/core/bbox/assigners/grid_assigner.py +156 -0
  30. submodules/chartdete/mmdet/core/bbox/assigners/hungarian_assigner.py +139 -0
  31. submodules/chartdete/mmdet/core/bbox/assigners/mask_hungarian_assigner.py +125 -0
  32. submodules/chartdete/mmdet/core/bbox/assigners/max_iou_assigner.py +218 -0
  33. submodules/chartdete/mmdet/core/bbox/assigners/point_assigner.py +134 -0
  34. submodules/chartdete/mmdet/core/bbox/assigners/region_assigner.py +222 -0
  35. submodules/chartdete/mmdet/core/bbox/assigners/sim_ota_assigner.py +257 -0
  36. submodules/chartdete/mmdet/core/bbox/assigners/task_aligned_assigner.py +151 -0
  37. submodules/chartdete/mmdet/core/bbox/assigners/uniform_assigner.py +135 -0
  38. submodules/chartdete/mmdet/core/bbox/builder.py +21 -0
  39. submodules/chartdete/mmdet/core/bbox/coder/__init__.py +15 -0
  40. submodules/chartdete/mmdet/core/bbox/coder/base_bbox_coder.py +18 -0
  41. submodules/chartdete/mmdet/core/bbox/coder/bucketing_bbox_coder.py +351 -0
  42. submodules/chartdete/mmdet/core/bbox/coder/delta_xywh_bbox_coder.py +392 -0
  43. submodules/chartdete/mmdet/core/bbox/coder/distance_point_bbox_coder.py +63 -0
  44. submodules/chartdete/mmdet/core/bbox/coder/legacy_delta_xywh_bbox_coder.py +216 -0
  45. submodules/chartdete/mmdet/core/bbox/coder/pseudo_bbox_coder.py +19 -0
  46. submodules/chartdete/mmdet/core/bbox/coder/tblr_bbox_coder.py +206 -0
  47. submodules/chartdete/mmdet/core/bbox/coder/yolo_bbox_coder.py +83 -0
  48. submodules/chartdete/mmdet/core/bbox/demodata.py +42 -0
  49. submodules/chartdete/mmdet/core/bbox/iou_calculators/__init__.py +5 -0
  50. submodules/chartdete/mmdet/core/bbox/iou_calculators/builder.py +9 -0
.gitattributes ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ *.png filter=xet diff=xet merge=xet -text
2
+ *.jpg filter=xet diff=xet merge=xet -text
3
+ *.jpeg filter=xet diff=xet merge=xet -text
4
+ *.bmp filter=xet diff=xet merge=xet -text
5
+ *.tiff filter=xet diff=xet merge=xet -text
6
+ *.pth filter=xet diff=xet merge=xet -text
LICENSE ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ ## Third-Party Licenses
26
+
27
+ ### LineFormer
28
+ This project uses LineFormer (https://github.com/TheJaeLal/LineFormer).
29
+ - Paper: "LineFormer: Rethinking Line Chart Data Extraction as Instance Segmentation" (ICDAR 2023)
30
+ - Authors: Jay Lal, Aditya Mitkari, Mahesh Bhosale, David Doermann
31
+
32
+ ### ChartDete
33
+ This project includes code from ChartDete (https://github.com/pengyu965/ChartDete).
34
+ - License: MIT
35
+ - Copyright (c) 2023 Pengyu Yan
36
+
37
+ ### MMDetection
38
+ This project uses MMDetection (https://github.com/open-mmlab/mmdetection).
39
+ - License: Apache License 2.0
40
+ - Copyright (c) 2018-2023 OpenMMLab
41
+
42
+ ## Image Attributions
43
+
44
+ ### demo/PMC5959982___3_HTML.jpg
45
+ - Source: PubMed Central (PMC5959982)
46
+ - Paper: Morrill et al., Applied Microbiology and Biotechnology, 2018
47
+ - License: CC BY 4.0
48
+ - URL: https://pmc.ncbi.nlm.nih.gov/articles/PMC5959982/
README.md ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AutoLineDigitizer API
3
+ emoji: 📈
4
+ colorFrom: blue
5
+ colorTo: green
6
+ sdk: gradio
7
+ sdk_version: 5.20.1
8
+ app_file: api_server.py
9
+ pinned: false
10
+ license: mit
11
+ ---
12
+
13
+ # AutoLineDigitizer API
14
+
15
+ Automatic chart line data extraction using deep learning (LineFormer + ChartDete + EasyOCR).
16
+
17
+ Upload a chart image to extract line data that can be imported into [StarryDigitizer](https://digitizer.starrydata.org/).
18
+
19
+ ## API Usage
20
+
21
+ ### Endpoint
22
+
23
+ ```
24
+ POST https://<your-space>.hf.space/api/predict
25
+ ```
26
+
27
+ ### Python Example
28
+
29
+ ```python
30
+ from gradio_client import Client
31
+
32
+ client = Client("https://<your-space>.hf.space/")
33
+ result = client.predict(
34
+ "chart.png", # image path
35
+ True, # auto_axis_detection
36
+ "arc_length", # downsample_mode
37
+ 20, # max_points
38
+ 10, # fixed_step
39
+ "mean_y_desc", # sort_mode
40
+ "starry_digitizer_json", # output_format
41
+ api_name="/predict"
42
+ )
43
+ ```
44
+
45
+ ### JavaScript (fetch) Example
46
+
47
+ ```javascript
48
+ const formData = new FormData();
49
+ formData.append('data', JSON.stringify([
50
+ null, // image placeholder
51
+ true, // auto_axis_detection
52
+ "arc_length",
53
+ 20,
54
+ 10,
55
+ "mean_y_desc",
56
+ "starry_digitizer_json"
57
+ ]));
58
+ formData.append('files', imageFile);
59
+
60
+ const res = await fetch('https://<your-space>.hf.space/api/predict', {
61
+ method: 'POST',
62
+ body: formData
63
+ });
64
+ ```
65
+
66
+ ## Output Formats
67
+
68
+ - **starry_digitizer_json**: StarryDigitizer project.json (for API integration)
69
+ - **starry_digitizer_zip**: ZIP file containing image.png + project.json
70
+ - **json**: Raw extraction result with line points and axis calibration
api_server.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ AutoLineDigitizer API Server for Hugging Face Spaces.
4
+ Exposes chart line extraction as a Gradio API.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+
10
+ # Setup paths (same as desktop_app.py)
11
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
12
+ CHARTDETE_DIR = os.path.join(SCRIPT_DIR, "submodules", "chartdete")
13
+ LINEFORMER_DIR = os.path.join(SCRIPT_DIR, "submodules", "lineformer")
14
+ MMDET_DIR = os.path.join(LINEFORMER_DIR, "mmdetection")
15
+ SRC_DIR = os.path.join(SCRIPT_DIR, "src")
16
+
17
+ sys.path.insert(0, SCRIPT_DIR)
18
+ sys.path.insert(0, SRC_DIR)
19
+ sys.path.insert(0, MMDET_DIR)
20
+ sys.path.insert(0, LINEFORMER_DIR)
21
+ sys.path.insert(0, CHARTDETE_DIR)
22
+
23
+ # Register ChartDete custom models
24
+ CHARTDETE_AVAILABLE = False
25
+ try:
26
+ import mmdet # noqa: F401
27
+ from mmdet.models.roi_heads.cascade_roi_head_LGF import CascadeRoIHead_LGF # noqa: F401
28
+ CHARTDETE_AVAILABLE = True
29
+ except Exception as e:
30
+ print(f"ChartDete custom models not available: {e}")
31
+
32
+ import cv2
33
+ import numpy as np
34
+ import json
35
+ import io
36
+ import zipfile
37
+ import urllib.request
38
+ import ssl
39
+ import gradio as gr
40
+ from datetime import datetime, timezone
41
+
42
+ # ============================================================
43
+ # Model download & loading
44
+ # ============================================================
45
+
46
+ GITHUB_REPO = "t29mato/AutoLineDigitizer"
47
+ GITHUB_RELEASE_TAG = "models"
48
+
49
+ # Model files: filename -> download source
50
+ MODEL_FILES = {
51
+ "iter_3000.pth": {
52
+ "source": "github",
53
+ "url": f"https://github.com/{GITHUB_REPO}/releases/download/{GITHUB_RELEASE_TAG}/iter_3000.pth",
54
+ },
55
+ "checkpoint.pth": {
56
+ "source": "github",
57
+ "url": f"https://github.com/{GITHUB_REPO}/releases/download/{GITHUB_RELEASE_TAG}/checkpoint.pth",
58
+ },
59
+ }
60
+
61
+ _infer_module = None
62
+ _chartdete_module = None
63
+
64
+
65
+ def download_file(url, dest_path):
66
+ """Download a file with progress logging and SSL fallback."""
67
+ print(f" Downloading {os.path.basename(dest_path)} from {url} ...")
68
+ tmp_path = dest_path + ".tmp"
69
+
70
+ req = urllib.request.Request(url)
71
+ try:
72
+ response_ctx = urllib.request.urlopen(req)
73
+ except Exception:
74
+ ctx = ssl._create_unverified_context()
75
+ response_ctx = urllib.request.urlopen(req, context=ctx)
76
+
77
+ with response_ctx as response:
78
+ total_size = int(response.headers.get("Content-Length", 0))
79
+ downloaded = 0
80
+ block_size = 1024 * 1024 # 1MB
81
+
82
+ with open(tmp_path, "wb") as f:
83
+ while True:
84
+ chunk = response.read(block_size)
85
+ if not chunk:
86
+ break
87
+ f.write(chunk)
88
+ downloaded += len(chunk)
89
+ if total_size > 0:
90
+ pct = downloaded * 100 // total_size
91
+ print(f" {os.path.basename(dest_path)}: {downloaded // (1024*1024)}MB / {total_size // (1024*1024)}MB ({pct}%)")
92
+
93
+ os.replace(tmp_path, dest_path)
94
+ print(f" {os.path.basename(dest_path)} downloaded successfully.")
95
+
96
+
97
+ def ensure_models():
98
+ """Download model files if they don't exist."""
99
+ models_dir = os.path.join(SCRIPT_DIR, "models")
100
+ os.makedirs(models_dir, exist_ok=True)
101
+
102
+ for filename, info in MODEL_FILES.items():
103
+ dest = os.path.join(models_dir, filename)
104
+ if not os.path.exists(dest):
105
+ download_file(info["url"], dest)
106
+ else:
107
+ print(f" {filename} already exists, skipping download.")
108
+
109
+
110
+ def load_models():
111
+ """Download (if needed) and load LineFormer and ChartDete models."""
112
+ global _infer_module, _chartdete_module
113
+
114
+ print("Checking model files...")
115
+ ensure_models()
116
+
117
+ models_dir = os.path.join(SCRIPT_DIR, "models")
118
+
119
+ # Load LineFormer
120
+ import infer
121
+ config_path = os.path.join(LINEFORMER_DIR, "lineformer_swin_t_config.py")
122
+ ckpt_path = os.path.join(models_dir, "iter_3000.pth")
123
+ infer.load_model(config_path, ckpt_path, "cpu")
124
+ _infer_module = infer
125
+ print("LineFormer model loaded.")
126
+
127
+ # Load ChartDete
128
+ if CHARTDETE_AVAILABLE:
129
+ import chartdete_infer
130
+ chartdete_config = os.path.join(SCRIPT_DIR, "config", "chartdete_config.py")
131
+ chartdete_ckpt = os.path.join(models_dir, "checkpoint.pth")
132
+ chartdete_infer.load_chartdete_model(
133
+ config_path=chartdete_config,
134
+ checkpoint_path=chartdete_ckpt,
135
+ device="cpu",
136
+ )
137
+ _chartdete_module = chartdete_infer
138
+ print("ChartDete model loaded.")
139
+ else:
140
+ print("ChartDete not available, axis detection disabled.")
141
+
142
+
143
+ def extract_lines(img):
144
+ """Run LineFormer inference and return raw centerline points."""
145
+ line_dataseries = _infer_module.get_dataseries(img, to_clean=False)
146
+ raw_lines = []
147
+ for line in line_dataseries:
148
+ if len(line) == 0:
149
+ continue
150
+ raw_lines.append([[int(pt["x"]), int(pt["y"])] for pt in line])
151
+ return raw_lines
152
+
153
+
154
+ def arc_length_resample(points, n_points):
155
+ """Resample at equidistant intervals along pixel-space arc length."""
156
+ pts = np.array(points, dtype=float)
157
+ diffs = np.diff(pts, axis=0)
158
+ seg_lengths = np.sqrt((diffs ** 2).sum(axis=1))
159
+ cum_arc = np.zeros(len(pts))
160
+ cum_arc[1:] = np.cumsum(seg_lengths)
161
+ total_length = cum_arc[-1]
162
+ if total_length == 0:
163
+ return [points[0]]
164
+
165
+ target_distances = np.linspace(0, total_length, n_points)
166
+ result = []
167
+ seg_idx = 0
168
+ for d in target_distances:
169
+ while seg_idx < len(seg_lengths) - 1 and cum_arc[seg_idx + 1] < d:
170
+ seg_idx += 1
171
+ seg_span = cum_arc[seg_idx + 1] - cum_arc[seg_idx]
172
+ t = 0.0 if seg_span == 0 else (d - cum_arc[seg_idx]) / seg_span
173
+ x = pts[seg_idx, 0] + t * (pts[seg_idx + 1, 0] - pts[seg_idx, 0])
174
+ y = pts[seg_idx, 1] + t * (pts[seg_idx + 1, 1] - pts[seg_idx, 1])
175
+ result.append([int(round(x)), int(round(y))])
176
+ return result
177
+
178
+
179
+ def downsample_points(points, mode="max_points", max_points=20, fixed_step=10):
180
+ """Downsample points based on mode."""
181
+ if len(points) <= 1:
182
+ return points
183
+ if mode == "none":
184
+ return points
185
+ elif mode == "fixed":
186
+ return points[::fixed_step]
187
+ elif mode == "max_points":
188
+ if len(points) <= max_points:
189
+ return points
190
+ step = max(1, len(points) // max_points)
191
+ return points[::step]
192
+ elif mode == "arc_length":
193
+ if len(points) <= max_points:
194
+ return points
195
+ return arc_length_resample(points, max_points)
196
+ return points
197
+
198
+
199
+ def detect_axis_calibration(img):
200
+ """Detect axis calibration using ChartDete + OCR."""
201
+ if _chartdete_module is None:
202
+ return None
203
+
204
+ detections = _chartdete_module.detect_chart_elements(img, score_thr=0.3)
205
+ axis_info = _chartdete_module.get_axis_info(detections, img=img, with_ocr=True)
206
+ calibration = axis_info.get("calibration")
207
+
208
+ if calibration is None:
209
+ return None
210
+
211
+ has_x = "x1_pixel" in calibration and "x2_pixel" in calibration
212
+ has_y = "y1_pixel" in calibration and "y2_pixel" in calibration
213
+
214
+ if not (has_x and has_y):
215
+ return None
216
+
217
+ plot_area = axis_info.get("plot_area")
218
+ if plot_area:
219
+ x_calib_y = plot_area[3]
220
+ y_calib_x = plot_area[0]
221
+ else:
222
+ x_calib_y = img.shape[0] * 0.9
223
+ y_calib_x = img.shape[1] * 0.1
224
+
225
+ return {
226
+ "x1_px": calibration["x1_pixel"],
227
+ "x1_py": x_calib_y,
228
+ "x1_val": calibration["x1_value"],
229
+ "x2_px": calibration["x2_pixel"],
230
+ "x2_py": x_calib_y,
231
+ "x2_val": calibration["x2_value"],
232
+ "y1_px": y_calib_x,
233
+ "y1_py": calibration["y2_pixel"],
234
+ "y1_val": calibration["y2_value"],
235
+ "y2_px": y_calib_x,
236
+ "y2_py": calibration["y1_pixel"],
237
+ "y2_val": calibration["y1_value"],
238
+ "xIsLogScale": False,
239
+ "yIsLogScale": False,
240
+ }
241
+
242
+
243
+ def convert_to_starry_digitizer_format(data_series, img_shape, axis_config=None):
244
+ """Convert extracted data to StarryDigitizer project.json format."""
245
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
246
+
247
+ if axis_config is None:
248
+ axis_set = {
249
+ "id": 1, "name": "XY Axes 1",
250
+ "x1": {"name": "x1", "value": 0, "coord": {"xPx": 0, "yPx": float(img_shape[0])}},
251
+ "x2": {"name": "x2", "value": 100, "coord": {"xPx": float(img_shape[1]), "yPx": float(img_shape[0])}},
252
+ "y1": {"name": "y1", "value": 0, "coord": {"xPx": 0, "yPx": float(img_shape[0])}},
253
+ "y2": {"name": "y2", "value": 100, "coord": {"xPx": 0, "yPx": 0}},
254
+ "xIsLogScale": False, "yIsLogScale": False,
255
+ "considerGraphTilt": False, "pointMode": 0, "isVisible": True,
256
+ }
257
+ else:
258
+ axis_set = {
259
+ "id": 1, "name": "XY Axes 1",
260
+ "x1": {"name": "x1", "value": axis_config["x1_val"], "coord": {"xPx": axis_config["x1_px"], "yPx": axis_config["x1_py"]}},
261
+ "x2": {"name": "x2", "value": axis_config["x2_val"], "coord": {"xPx": axis_config["x2_px"], "yPx": axis_config["x2_py"]}},
262
+ "y1": {"name": "y1", "value": axis_config["y1_val"], "coord": {"xPx": axis_config["y1_px"], "yPx": axis_config["y1_py"]}},
263
+ "y2": {"name": "y2", "value": axis_config["y2_val"], "coord": {"xPx": axis_config["y2_px"], "yPx": axis_config["y2_py"]}},
264
+ "xIsLogScale": axis_config.get("xIsLogScale", False),
265
+ "yIsLogScale": axis_config.get("yIsLogScale", False),
266
+ "considerGraphTilt": False, "pointMode": 0, "isVisible": True,
267
+ }
268
+
269
+ datasets = [{
270
+ "id": 1, "name": "dataset 1", "axisSetId": 1,
271
+ "points": [], "visiblePointIds": [], "manuallyAddedPointIds": [],
272
+ }]
273
+
274
+ for idx, series in enumerate(data_series):
275
+ points = []
276
+ visible_ids = []
277
+ for pt_idx, pt in enumerate(series["points"]):
278
+ pt_id = pt_idx + 1
279
+ points.append({"id": pt_id, "xPx": float(pt[0]), "yPx": float(pt[1])})
280
+ visible_ids.append(pt_id)
281
+ datasets.append({
282
+ "id": idx + 2, "name": f"Line {idx + 1}", "axisSetId": 1,
283
+ "points": points, "visiblePointIds": visible_ids, "manuallyAddedPointIds": [],
284
+ })
285
+
286
+ return {
287
+ "version": "1.11.2", "timestamp": timestamp,
288
+ "axisSets": [axis_set], "activeAxisSetId": 1,
289
+ "datasets": datasets, "activeDatasetId": len(datasets),
290
+ "canvasHandler": {"scale": 1.0, "manualMode": 0},
291
+ }
292
+
293
+
294
+ # ============================================================
295
+ # Gradio API endpoint
296
+ # ============================================================
297
+
298
+ def digitize_chart(
299
+ image,
300
+ auto_axis_detection: bool = True,
301
+ downsample_mode: str = "arc_length",
302
+ max_points: int = 20,
303
+ fixed_step: int = 10,
304
+ sort_mode: str = "mean_y_desc",
305
+ output_format: str = "starry_digitizer_json",
306
+ ):
307
+ """
308
+ Extract line data from a chart image.
309
+
310
+ Args:
311
+ image: Input chart image (PIL Image from Gradio)
312
+ auto_axis_detection: Enable ChartDete + OCR axis detection
313
+ downsample_mode: "none", "max_points", "fixed", or "arc_length"
314
+ max_points: Max points per line (for max_points/arc_length modes)
315
+ fixed_step: Step size (for fixed mode)
316
+ sort_mode: "original", "mean_y_desc", or "mean_y_asc"
317
+ output_format: "starry_digitizer_json", "starry_digitizer_zip", or "json"
318
+
319
+ Returns:
320
+ For starry_digitizer_json/json: JSON string
321
+ For starry_digitizer_zip: ZIP file path
322
+ """
323
+ if image is None:
324
+ return json.dumps({"error": "No image provided"})
325
+
326
+ # Convert PIL Image to BGR numpy array
327
+ img_rgb = np.array(image)
328
+ img_bgr = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2BGR)
329
+
330
+ # Extract lines
331
+ raw_lines = extract_lines(img_bgr)
332
+
333
+ # Downsample
334
+ data_series = []
335
+ for all_points in raw_lines:
336
+ points = downsample_points(all_points, downsample_mode, max_points, fixed_step)
337
+ data_series.append({"points": points})
338
+
339
+ # Sort
340
+ if sort_mode == "mean_y_desc" and len(data_series) > 0:
341
+ data_series = sorted(data_series, key=lambda s: np.mean([pt[1] for pt in s["points"]]))
342
+ elif sort_mode == "mean_y_asc" and len(data_series) > 0:
343
+ data_series = sorted(data_series, key=lambda s: np.mean([pt[1] for pt in s["points"]]), reverse=True)
344
+
345
+ # Axis detection
346
+ axis_config = None
347
+ if auto_axis_detection and _chartdete_module is not None:
348
+ axis_config = detect_axis_calibration(img_bgr)
349
+
350
+ # Build output
351
+ if output_format == "starry_digitizer_zip":
352
+ project_json = convert_to_starry_digitizer_format(data_series, img_bgr.shape, axis_config)
353
+ zip_buffer = io.BytesIO()
354
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf:
355
+ _, img_encoded = cv2.imencode(".png", img_bgr)
356
+ zf.writestr("image.png", img_encoded.tobytes())
357
+ zf.writestr("project.json", json.dumps(project_json, indent=2, ensure_ascii=False))
358
+ zip_buffer.seek(0)
359
+
360
+ import tempfile
361
+ tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
362
+ tmp.write(zip_buffer.read())
363
+ tmp.close()
364
+ return tmp.name
365
+
366
+ elif output_format == "starry_digitizer_json":
367
+ project_json = convert_to_starry_digitizer_format(data_series, img_bgr.shape, axis_config)
368
+ return json.dumps(project_json, ensure_ascii=False)
369
+
370
+ else: # "json" - raw extraction result
371
+ result = {
372
+ "num_lines": len(data_series),
373
+ "lines": [
374
+ {"line_index": i, "num_points": len(s["points"]), "points": s["points"]}
375
+ for i, s in enumerate(data_series)
376
+ ],
377
+ "axis_config": axis_config,
378
+ "image_shape": {"height": img_bgr.shape[0], "width": img_bgr.shape[1]},
379
+ }
380
+ return json.dumps(result, ensure_ascii=False)
381
+
382
+
383
+ # ============================================================
384
+ # Gradio Interface
385
+ # ============================================================
386
+
387
+ def create_app():
388
+ """Create Gradio app."""
389
+ with gr.Blocks(title="AutoLineDigitizer API") as demo:
390
+ gr.Markdown("# AutoLineDigitizer API")
391
+ gr.Markdown(
392
+ "Upload a chart image to automatically extract line data. "
393
+ "Results can be imported into [StarryDigitizer](https://digitizer.starrydata.org/)."
394
+ )
395
+
396
+ with gr.Row():
397
+ with gr.Column():
398
+ image_input = gr.Image(type="pil", label="Chart Image")
399
+ auto_axis = gr.Checkbox(value=True, label="Auto Axis Detection (ChartDete + OCR)")
400
+ downsample = gr.Dropdown(
401
+ choices=["none", "max_points", "fixed", "arc_length"],
402
+ value="arc_length",
403
+ label="Downsample Mode",
404
+ )
405
+ max_pts = gr.Slider(minimum=5, maximum=200, value=20, step=1, label="Max Points per Line")
406
+ fixed_stp = gr.Slider(minimum=1, maximum=50, value=10, step=1, label="Fixed Step")
407
+ sort = gr.Dropdown(
408
+ choices=["original", "mean_y_desc", "mean_y_asc"],
409
+ value="mean_y_desc",
410
+ label="Sort Mode",
411
+ )
412
+ out_fmt = gr.Dropdown(
413
+ choices=["starry_digitizer_json", "starry_digitizer_zip", "json"],
414
+ value="starry_digitizer_json",
415
+ label="Output Format",
416
+ )
417
+ run_btn = gr.Button("Extract Lines", variant="primary")
418
+
419
+ with gr.Column():
420
+ output = gr.Textbox(label="Result (JSON)", lines=20, max_lines=50)
421
+ file_output = gr.File(label="Download ZIP", visible=False)
422
+
423
+ def on_run(image, auto_axis, downsample, max_pts, fixed_stp, sort, out_fmt):
424
+ result = digitize_chart(image, auto_axis, downsample, max_pts, fixed_stp, sort, out_fmt)
425
+ if out_fmt == "starry_digitizer_zip":
426
+ return gr.update(value="ZIP file generated. Download below."), gr.update(value=result, visible=True)
427
+ else:
428
+ return gr.update(value=result), gr.update(visible=False)
429
+
430
+ run_btn.click(
431
+ fn=on_run,
432
+ inputs=[image_input, auto_axis, downsample, max_pts, fixed_stp, sort, out_fmt],
433
+ outputs=[output, file_output],
434
+ )
435
+
436
+ return demo
437
+
438
+
439
+ if __name__ == "__main__":
440
+ print("Loading models...")
441
+ load_models()
442
+ print("All models loaded. Starting Gradio server...")
443
+
444
+ demo = create_app()
445
+ demo.queue()
446
+ demo.launch(server_name="0.0.0.0", server_port=7860)
config/chartdete_config.py ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ model = dict(
2
+ type='CascadeRCNN',
3
+ backbone=dict(
4
+ type='SwinTransformer',
5
+ embed_dims=96,
6
+ depths=[2, 2, 6, 2],
7
+ num_heads=[3, 6, 12, 24],
8
+ window_size=7,
9
+ mlp_ratio=4,
10
+ qkv_bias=True,
11
+ qk_scale=None,
12
+ drop_rate=0.0,
13
+ attn_drop_rate=0.0,
14
+ drop_path_rate=0.2,
15
+ patch_norm=True,
16
+ out_indices=(0, 1, 2, 3),
17
+ with_cp=False,
18
+ convert_weights=True,
19
+ init_cfg=None),
20
+ neck=dict(
21
+ type='FPN',
22
+ in_channels=[96, 192, 384, 768],
23
+ out_channels=256,
24
+ num_outs=5),
25
+ rpn_head=dict(
26
+ type='RPNHead',
27
+ in_channels=256,
28
+ feat_channels=256,
29
+ anchor_generator=dict(
30
+ type='AnchorGenerator',
31
+ scales=[8],
32
+ ratios=[0.5, 1.0, 2.0],
33
+ strides=[4, 8, 16, 32, 64]),
34
+ bbox_coder=dict(
35
+ type='DeltaXYWHBBoxCoder',
36
+ target_means=[0.0, 0.0, 0.0, 0.0],
37
+ target_stds=[1.0, 1.0, 1.0, 1.0]),
38
+ loss_cls=dict(
39
+ type='CrossEntropyLoss', use_sigmoid=True, loss_weight=1.0),
40
+ loss_bbox=dict(
41
+ type='SmoothL1Loss', beta=0.1111111111111111, loss_weight=1.0)),
42
+ roi_head=dict(
43
+ type='CascadeRoIHead_LGF',
44
+ num_stages=3,
45
+ stage_loss_weights=[1, 1, 0.5],
46
+ bbox_roi_extractor=dict(
47
+ type='SingleRoIExtractor',
48
+ roi_layer=dict(type='RoIAlign', output_size=7, sampling_ratio=0),
49
+ out_channels=256,
50
+ featmap_strides=[4, 8, 16, 32]),
51
+ bbox_head=[
52
+ dict(
53
+ type='Shared3FCBBoxHead_with_BboxEncoding',
54
+ in_channels=256,
55
+ fc_out_channels=1024,
56
+ bbox_encoding_dim=512,
57
+ roi_feat_size=7,
58
+ num_classes=18,
59
+ bbox_coder=dict(
60
+ type='DeltaXYWHBBoxCoder',
61
+ target_means=[0.0, 0.0, 0.0, 0.0],
62
+ target_stds=[0.1, 0.1, 0.2, 0.2]),
63
+ reg_class_agnostic=True,
64
+ loss_cls=dict(type='FocalLoss'),
65
+ loss_bbox=dict(
66
+ type='BalancedL1Loss', beta=1.0, loss_weight=1.0)),
67
+ dict(
68
+ type='Shared3FCBBoxHead_with_BboxEncoding',
69
+ in_channels=256,
70
+ fc_out_channels=1024,
71
+ bbox_encoding_dim=512,
72
+ roi_feat_size=7,
73
+ num_classes=18,
74
+ bbox_coder=dict(
75
+ type='DeltaXYWHBBoxCoder',
76
+ target_means=[0.0, 0.0, 0.0, 0.0],
77
+ target_stds=[0.05, 0.05, 0.1, 0.1]),
78
+ reg_class_agnostic=True,
79
+ loss_cls=dict(type='FocalLoss'),
80
+ loss_bbox=dict(
81
+ type='BalancedL1Loss', beta=1.0, loss_weight=1.0)),
82
+ dict(
83
+ type='Shared3FCBBoxHead_with_BboxEncoding',
84
+ in_channels=256,
85
+ fc_out_channels=1024,
86
+ bbox_encoding_dim=512,
87
+ roi_feat_size=7,
88
+ num_classes=18,
89
+ bbox_coder=dict(
90
+ type='DeltaXYWHBBoxCoder',
91
+ target_means=[0.0, 0.0, 0.0, 0.0],
92
+ target_stds=[0.033, 0.033, 0.067, 0.067]),
93
+ reg_class_agnostic=True,
94
+ loss_cls=dict(type='FocalLoss'),
95
+ loss_bbox=dict(
96
+ type='BalancedL1Loss', beta=1.0, loss_weight=1.0))
97
+ ],
98
+ localglobal_fuser=dict(
99
+ type='LocalGlobal_Context_Fuser',
100
+ channels=256,
101
+ roi_size=7,
102
+ reduced_channels=256,
103
+ lg_merge_layer=dict(type='SELayer', channels=256)),
104
+ lgf_shared=False,
105
+ bbox_encoder=dict(
106
+ type='BboxEncoder',
107
+ n_layer=4,
108
+ n_head=4,
109
+ n_embd=512,
110
+ bbox_cord_dim=4,
111
+ bbox_max_num=1024,
112
+ embd_pdrop=0.1,
113
+ attn_pdrop=0.1),
114
+ bbox_encoder_shared=False),
115
+ train_cfg=dict(
116
+ rpn=dict(
117
+ assigner=dict(
118
+ type='MaxIoUAssigner',
119
+ pos_iou_thr=0.7,
120
+ neg_iou_thr=0.3,
121
+ min_pos_iou=0.3,
122
+ match_low_quality=True,
123
+ ignore_iof_thr=-1),
124
+ sampler=dict(
125
+ type='RandomSampler',
126
+ num=256,
127
+ pos_fraction=0.5,
128
+ neg_pos_ub=-1,
129
+ add_gt_as_proposals=False),
130
+ allowed_border=0,
131
+ pos_weight=-1,
132
+ debug=False),
133
+ rpn_proposal=dict(
134
+ nms_pre=2000,
135
+ max_per_img=2000,
136
+ nms=dict(type='nms', iou_threshold=0.7),
137
+ min_bbox_size=0),
138
+ rcnn=[
139
+ dict(
140
+ assigner=dict(
141
+ type='MaxIoUAssigner',
142
+ pos_iou_thr=0.5,
143
+ neg_iou_thr=0.5,
144
+ min_pos_iou=0.5,
145
+ match_low_quality=False,
146
+ ignore_iof_thr=-1),
147
+ sampler=dict(
148
+ type='RandomSampler',
149
+ num=512,
150
+ pos_fraction=0.25,
151
+ neg_pos_ub=-1,
152
+ add_gt_as_proposals=True),
153
+ pos_weight=-1,
154
+ debug=False),
155
+ dict(
156
+ assigner=dict(
157
+ type='MaxIoUAssigner',
158
+ pos_iou_thr=0.6,
159
+ neg_iou_thr=0.6,
160
+ min_pos_iou=0.6,
161
+ match_low_quality=False,
162
+ ignore_iof_thr=-1),
163
+ sampler=dict(
164
+ type='RandomSampler',
165
+ num=512,
166
+ pos_fraction=0.25,
167
+ neg_pos_ub=-1,
168
+ add_gt_as_proposals=True),
169
+ pos_weight=-1,
170
+ debug=False),
171
+ dict(
172
+ assigner=dict(
173
+ type='MaxIoUAssigner',
174
+ pos_iou_thr=0.7,
175
+ neg_iou_thr=0.7,
176
+ min_pos_iou=0.7,
177
+ match_low_quality=False,
178
+ ignore_iof_thr=-1),
179
+ sampler=dict(
180
+ type='RandomSampler',
181
+ num=512,
182
+ pos_fraction=0.25,
183
+ neg_pos_ub=-1,
184
+ add_gt_as_proposals=True),
185
+ pos_weight=-1,
186
+ debug=False)
187
+ ]),
188
+ test_cfg=dict(
189
+ rpn=dict(
190
+ nms_pre=1000,
191
+ max_per_img=1000,
192
+ nms=dict(type='nms', iou_threshold=0.7),
193
+ min_bbox_size=0),
194
+ rcnn=dict(
195
+ score_thr=0.,
196
+ nms=dict(type='nms', iou_threshold=0.7),
197
+ max_per_img=200)))
198
+ dataset_type = 'CocoDataset'
199
+ data_root = 'data/coco/'
200
+ img_norm_cfg = dict(
201
+ mean=[216.45, 212.36, 206.76], std=[55.82, 56.04, 55.56], to_rgb=True)
202
+ train_pipeline = [
203
+ dict(type='LoadImageFromFile'),
204
+ dict(type='LoadAnnotations', with_bbox=True),
205
+ dict(
206
+ type='AutoAugment',
207
+ policies=[[{
208
+ 'type':
209
+ 'Resize',
210
+ 'img_scale': [(480, 1333), (512, 1333), (544, 1333), (576, 1333),
211
+ (608, 1333), (640, 1333), (672, 1333), (704, 1333),
212
+ (736, 1333), (768, 1333), (800, 1333)],
213
+ 'multiscale_mode':
214
+ 'value',
215
+ 'keep_ratio':
216
+ True
217
+ }],
218
+ [{
219
+ 'type': 'Resize',
220
+ 'img_scale': [(400, 1333), (500, 1333), (600, 1333)],
221
+ 'multiscale_mode': 'value',
222
+ 'keep_ratio': True
223
+ }, {
224
+ 'type': 'RandomCrop',
225
+ 'crop_type': 'absolute_range',
226
+ 'crop_size': (384, 600),
227
+ 'allow_negative_crop': True
228
+ }, {
229
+ 'type':
230
+ 'Resize',
231
+ 'img_scale': [(480, 1333), (512, 1333), (544, 1333),
232
+ (576, 1333), (608, 1333), (640, 1333),
233
+ (672, 1333), (704, 1333), (736, 1333),
234
+ (768, 1333), (800, 1333)],
235
+ 'multiscale_mode':
236
+ 'value',
237
+ 'override':
238
+ True,
239
+ 'keep_ratio':
240
+ True
241
+ }, {
242
+ 'type': 'PhotoMetricDistortion',
243
+ 'brightness_delta': 32,
244
+ 'contrast_range': (0.5, 1.5),
245
+ 'saturation_range': (0.5, 1.5),
246
+ 'hue_delta': 18
247
+ }, {
248
+ 'type': 'MinIoURandomCrop',
249
+ 'min_ious': (0.4, 0.5, 0.6, 0.7, 0.8, 0.9),
250
+ 'min_crop_size': 0.3
251
+ }, {
252
+ 'type':
253
+ 'CutOut',
254
+ 'n_holes': (5, 10),
255
+ 'cutout_shape': [(4, 4), (4, 8), (8, 4), (8, 8),
256
+ (16, 32), (32, 16), (32, 32), (32, 48),
257
+ (48, 32), (48, 48)]
258
+ }]]),
259
+ dict(type='RandomFlip', flip_ratio=0.1),
260
+ dict(
261
+ type='Normalize',
262
+ mean=[216.45, 212.36, 206.76],
263
+ std=[55.82, 56.04, 55.56],
264
+ to_rgb=True),
265
+ dict(type='Pad', size_divisor=32),
266
+ dict(type='DefaultFormatBundle'),
267
+ dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
268
+ ]
269
+ test_pipeline = [
270
+ dict(type='LoadImageFromFile', to_float32=True),
271
+ dict(
272
+ type='MultiScaleFlipAug',
273
+ img_scale=(1333, 800),
274
+ flip=False,
275
+ transforms=[
276
+ dict(type='Resize', keep_ratio=True),
277
+ dict(type='RandomFlip', flip_ratio=0.0),
278
+ dict(
279
+ type='Normalize',
280
+ mean=[216.45, 212.36, 206.76],
281
+ std=[55.82, 56.04, 55.56],
282
+ to_rgb=True),
283
+ dict(type='Pad', size_divisor=32),
284
+ dict(type='DefaultFormatBundle'),
285
+ dict(type='Collect', keys=['img'])
286
+ ])
287
+ ]
288
+ data = dict(
289
+ samples_per_gpu=3,
290
+ workers_per_gpu=4,
291
+ train=dict(
292
+ type='CocoDataset',
293
+ ann_file=
294
+ './data/pmc_2022/pmc_coco/element_detection/train.json',
295
+ img_prefix=
296
+ './data/pmc_2022/pmc_coco/element_detection/train/',
297
+ pipeline=[
298
+ dict(type='LoadImageFromFile'),
299
+ dict(type='LoadAnnotations', with_bbox=True),
300
+ dict(
301
+ type='AutoAugment',
302
+ policies=[[{
303
+ 'type':
304
+ 'Resize',
305
+ 'img_scale': [(480, 1333), (512, 1333), (544, 1333),
306
+ (576, 1333), (608, 1333), (640, 1333),
307
+ (672, 1333), (704, 1333), (736, 1333),
308
+ (768, 1333), (800, 1333)],
309
+ 'multiscale_mode':
310
+ 'value',
311
+ 'keep_ratio':
312
+ True
313
+ }],
314
+ [{
315
+ 'type': 'Resize',
316
+ 'img_scale': [(400, 1333), (500, 1333),
317
+ (600, 1333)],
318
+ 'multiscale_mode': 'value',
319
+ 'keep_ratio': True
320
+ }, {
321
+ 'type': 'RandomCrop',
322
+ 'crop_type': 'absolute_range',
323
+ 'crop_size': (384, 600),
324
+ 'allow_negative_crop': True
325
+ }, {
326
+ 'type':
327
+ 'Resize',
328
+ 'img_scale': [(480, 1333), (512, 1333),
329
+ (544, 1333), (576, 1333),
330
+ (608, 1333), (640, 1333),
331
+ (672, 1333), (704, 1333),
332
+ (736, 1333), (768, 1333),
333
+ (800, 1333)],
334
+ 'multiscale_mode':
335
+ 'value',
336
+ 'override':
337
+ True,
338
+ 'keep_ratio':
339
+ True
340
+ }, {
341
+ 'type': 'PhotoMetricDistortion',
342
+ 'brightness_delta': 32,
343
+ 'contrast_range': (0.5, 1.5),
344
+ 'saturation_range': (0.5, 1.5),
345
+ 'hue_delta': 18
346
+ }, {
347
+ 'type': 'MinIoURandomCrop',
348
+ 'min_ious': (0.4, 0.5, 0.6, 0.7, 0.8, 0.9),
349
+ 'min_crop_size': 0.3
350
+ }, {
351
+ 'type':
352
+ 'CutOut',
353
+ 'n_holes': (5, 10),
354
+ 'cutout_shape': [(4, 4), (4, 8), (8, 4), (8, 8),
355
+ (16, 32), (32, 16), (32, 32),
356
+ (32, 48), (48, 32), (48, 48)]
357
+ }]]),
358
+ dict(type='RandomFlip', flip_ratio=0.1),
359
+ dict(
360
+ type='Normalize',
361
+ mean=[216.45, 212.36, 206.76],
362
+ std=[55.82, 56.04, 55.56],
363
+ to_rgb=True),
364
+ dict(type='Pad', size_divisor=32),
365
+ dict(type='DefaultFormatBundle'),
366
+ dict(type='Collect', keys=['img', 'gt_bboxes', 'gt_labels'])
367
+ ],
368
+ classes=[
369
+ 'x_title', 'y_title', 'plot_area', 'other', 'xlabel', 'ylabel',
370
+ 'chart_title', 'x_tick', 'y_tick', 'legend_patch', 'legend_label',
371
+ 'legend_title', 'legend_area', 'mark_label', 'value_label',
372
+ 'y_axis_area', 'x_axis_area', 'tick_grouping'
373
+ ]),
374
+ val=dict(
375
+ type='CocoDataset',
376
+ ann_file=
377
+ './data/pmc_2022/pmc_coco/element_detection/val.json',
378
+ img_prefix=
379
+ './data/pmc_2022/pmc_coco/element_detection/val/',
380
+ pipeline=[
381
+ dict(type='LoadImageFromFile'),
382
+ dict(
383
+ type='MultiScaleFlipAug',
384
+ img_scale=(1333, 800),
385
+ flip=False,
386
+ transforms=[
387
+ dict(type='Resize', keep_ratio=True),
388
+ dict(type='RandomFlip'),
389
+ dict(
390
+ type='Normalize',
391
+ mean=[123.675, 116.28, 103.53],
392
+ std=[58.395, 57.12, 57.375],
393
+ to_rgb=True),
394
+ dict(type='Pad', size_divisor=32),
395
+ dict(type='ImageToTensor', keys=['img']),
396
+ dict(type='Collect', keys=['img'])
397
+ ])
398
+ ],
399
+ classes=[
400
+ 'x_title', 'y_title', 'plot_area', 'other', 'xlabel', 'ylabel',
401
+ 'chart_title', 'x_tick', 'y_tick', 'legend_patch', 'legend_label',
402
+ 'legend_title', 'legend_area', 'mark_label', 'value_label',
403
+ 'y_axis_area', 'x_axis_area', 'tick_grouping'
404
+ ]),
405
+ test=dict(
406
+ type='CocoDataset',
407
+ ann_file=
408
+ './data/pmc_2022/pmc_coco/element_detection/split3_test.json',
409
+ img_prefix=
410
+ './data/pmc_2022/pmc_coco/element_detection/split3_test/',
411
+ pipeline=[
412
+ dict(type='LoadImageFromFile'),
413
+ dict(
414
+ type='MultiScaleFlipAug',
415
+ img_scale=(1333, 800),
416
+ flip=False,
417
+ transforms=[
418
+ dict(type='Resize', keep_ratio=True),
419
+ dict(type='RandomFlip'),
420
+ dict(
421
+ type='Normalize',
422
+ mean=[123.675, 116.28, 103.53],
423
+ std=[58.395, 57.12, 57.375],
424
+ to_rgb=True),
425
+ dict(type='Pad', size_divisor=32),
426
+ dict(type='ImageToTensor', keys=['img']),
427
+ dict(type='Collect', keys=['img'])
428
+ ])
429
+ ],
430
+ classes=[
431
+ 'x_title', 'y_title', 'plot_area', 'other', 'xlabel', 'ylabel',
432
+ 'chart_title', 'x_tick', 'y_tick', 'legend_patch', 'legend_label',
433
+ 'legend_title', 'legend_area', 'mark_label', 'value_label',
434
+ 'y_axis_area', 'x_axis_area', 'tick_grouping'
435
+ ]))
436
+ evaluation = dict(interval=1, metric=['bbox'])
437
+ optimizer = dict(
438
+ type='AdamW',
439
+ lr=0.0002,
440
+ betas=(0.9, 0.999),
441
+ weight_decay=0.05,
442
+ paramwise_cfg=dict(
443
+ custom_keys=dict(
444
+ absolute_pos_embed=dict(decay_mult=0.0),
445
+ relative_position_bias_table=dict(decay_mult=0.0),
446
+ norm=dict(decay_mult=0.0))))
447
+ optimizer_config = dict(grad_clip=None)
448
+ lr_config = dict(
449
+ policy='step',
450
+ warmup='linear',
451
+ warmup_iters=500,
452
+ warmup_ratio=0.001,
453
+ step=[8, 11])
454
+ runner = dict(type='EpochBasedRunner', max_epochs=150)
455
+ checkpoint_config = dict(interval=1)
456
+ log_config = dict(interval=50, hooks=[dict(type='TextLoggerHook')])
457
+ custom_hooks = [dict(type='NumClassCheckHook')]
458
+ dist_params = dict(backend='nccl')
459
+ log_level = 'INFO'
460
+ load_from = None
461
+ resume_from = None
462
+ workflow = [('train', 1)]
463
+ opencv_num_threads = 0
464
+ mp_start_method = 'fork'
465
+ auto_scale_lr = dict(enable=False, base_batch_size=16)
466
+ pretrained = 'https://github.com/SwinTransformer/storage/releases/download/v1.0.0/swin_tiny_patch4_window7_224.pth'
467
+ classes = [
468
+ 'x_title', 'y_title', 'plot_area', 'other', 'xlabel', 'ylabel',
469
+ 'chart_title', 'x_tick', 'y_tick', 'legend_patch', 'legend_label',
470
+ 'legend_title', 'legend_area', 'mark_label', 'value_label', 'y_axis_area',
471
+ 'x_axis_area', 'tick_grouping'
472
+ ]
473
+ auto_resume = False
474
+ gpu_ids = range(0, 4)
requirements.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HuggingFace Spaces requirements
2
+ gradio>=5.0.0
3
+ opencv-python-headless>=4.8.0
4
+ numpy>=1.24.0
5
+ torch>=1.13.0
6
+ torchvision>=0.14.0
7
+ mmcv-full==1.7.2
8
+ easyocr>=1.7.0
9
+ scikit-image>=0.21.0
10
+ scipy>=1.9.0
11
+ bresenham
12
+ terminaltables
13
+ matplotlib
14
+ pycocotools
src/app.py ADDED
@@ -0,0 +1,843 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ LineFormer Streamlit App
4
+ Chart line data extraction using LineFormer.
5
+ Automatic axis detection using ChartDete + OCR.
6
+ Output compatible with StarryDigitizer and WebPlotDigitizer formats.
7
+ """
8
+
9
+ import sys
10
+ import os
11
+
12
+ # Project root is parent of src/
13
+ _src_dir = os.path.dirname(os.path.abspath(__file__))
14
+ _project_root = os.path.dirname(_src_dir)
15
+
16
+ # Add ChartDete submodule to path FIRST (highest priority for custom mmdet models)
17
+ sys.path.insert(0, os.path.join(_project_root, 'submodules', 'chartdete'))
18
+
19
+ # Add lineformer submodule to path
20
+ sys.path.insert(0, os.path.join(_project_root, 'submodules', 'lineformer'))
21
+
22
+ # Import mmdet (uses ChartDete's mmdet which includes custom models like CascadeRoIHead_LGF)
23
+ import mmdet # noqa: F401
24
+ from mmdet.models.roi_heads.cascade_roi_head_LGF import CascadeRoIHead_LGF # noqa: F401
25
+
26
+ # Add src to path for local imports
27
+ sys.path.insert(0, _src_dir)
28
+
29
+ import streamlit as st
30
+ import cv2
31
+ import numpy as np
32
+ import json
33
+ import io
34
+ import zipfile
35
+ import tarfile
36
+ from datetime import datetime
37
+
38
+ # Page config
39
+ st.set_page_config(
40
+ page_title="AutoLineDigitizer",
41
+ page_icon="📈",
42
+ layout="wide"
43
+ )
44
+
45
+
46
+ @st.cache_resource
47
+ def load_lineformer_model():
48
+ """Load LineFormer model (cached)."""
49
+ import infer
50
+
51
+ # Model weights in models/, config in submodules/lineformer/
52
+ CKPT = os.path.join(_project_root, "models", "iter_3000.pth")
53
+ CONFIG = os.path.join(_project_root, "submodules", "lineformer", "lineformer_swin_t_config.py")
54
+ DEVICE = "cpu"
55
+
56
+ infer.load_model(CONFIG, CKPT, DEVICE)
57
+ return infer
58
+
59
+
60
+ @st.cache_resource
61
+ def load_chartdete_model():
62
+ """Load ChartDete model for axis detection (cached)."""
63
+ import chartdete_infer
64
+ chartdete_infer.load_chartdete_model(device='cpu')
65
+ return chartdete_infer
66
+
67
+
68
+ def detect_axis_calibration(chartdete_module, img):
69
+ """
70
+ Detect chart elements and extract axis calibration using ChartDete + OCR.
71
+
72
+ Returns:
73
+ axis_config: dict with calibration data or None
74
+ detections: raw detection results
75
+ ocr_results: OCR results for labels
76
+ """
77
+ # Run ChartDete detection
78
+ detections = chartdete_module.detect_chart_elements(img, score_thr=0.3)
79
+
80
+ # Get axis info with OCR
81
+ axis_info = chartdete_module.get_axis_info(detections, img=img, with_ocr=True)
82
+
83
+ calibration = axis_info.get('calibration')
84
+ ocr_results = axis_info.get('ocr_results', {})
85
+
86
+ if calibration is None:
87
+ return None, detections, ocr_results
88
+
89
+ # Convert calibration to axis_config format
90
+ # For x-axis: use xlabel center positions (x pixel, fixed y at label position)
91
+ # For y-axis: use ylabel center positions (fixed x at label position, y pixel)
92
+ axis_config = None
93
+
94
+ has_x = 'x1_pixel' in calibration and 'x2_pixel' in calibration
95
+ has_y = 'y1_pixel' in calibration and 'y2_pixel' in calibration
96
+
97
+ if has_x and has_y:
98
+ # Get plot_area to determine y position for x-axis calibration points
99
+ plot_area = axis_info.get('plot_area')
100
+
101
+ if plot_area:
102
+ # x calibration: use bottom of plot area for y
103
+ x_calib_y = plot_area[3] # y2 of plot_area (bottom)
104
+ # y calibration: use left of plot area for x
105
+ y_calib_x = plot_area[0] # x1 of plot_area (left)
106
+ else:
107
+ # Fallback: use image dimensions
108
+ x_calib_y = img.shape[0] * 0.9
109
+ y_calib_x = img.shape[1] * 0.1
110
+
111
+ # Note: In calibration from OCR:
112
+ # y1_pixel/y1_value = top label (higher Y pixel, but could be higher or lower value)
113
+ # y2_pixel/y2_value = bottom label (lower Y pixel)
114
+ # In StarryDigitizer/WPD format:
115
+ # y1 = bottom point (lower Y pixel = higher on screen)
116
+ # y2 = top point (higher Y pixel = lower on screen)
117
+ # So we swap y1 and y2 from OCR calibration
118
+
119
+ axis_config = {
120
+ # X axis calibration points (at bottom of chart)
121
+ "x1_px": calibration['x1_pixel'],
122
+ "x1_py": x_calib_y,
123
+ "x1_val": calibration['x1_value'],
124
+ "x2_px": calibration['x2_pixel'],
125
+ "x2_py": x_calib_y,
126
+ "x2_val": calibration['x2_value'],
127
+ # Y axis calibration points (at left of chart)
128
+ # y1 = bottom (higher pixel Y), y2 = top (lower pixel Y)
129
+ "y1_px": y_calib_x,
130
+ "y1_py": calibration['y2_pixel'], # bottom label
131
+ "y1_val": calibration['y2_value'],
132
+ "y2_px": y_calib_x,
133
+ "y2_py": calibration['y1_pixel'], # top label
134
+ "y2_val": calibration['y1_value'],
135
+ "xIsLogScale": False,
136
+ "yIsLogScale": False,
137
+ }
138
+
139
+ return axis_config, detections, ocr_results
140
+
141
+
142
+ def downsample_points(points, mode, fixed_step, max_points):
143
+ """Downsample points based on mode."""
144
+ if len(points) <= 1:
145
+ return points
146
+
147
+ if mode == "none":
148
+ return points
149
+ elif mode == "fixed":
150
+ return points[::fixed_step]
151
+ elif mode == "max_points":
152
+ if len(points) <= max_points:
153
+ return points
154
+ step = max(1, len(points) // max_points)
155
+ return points[::step]
156
+
157
+ return points
158
+
159
+
160
+ def sort_data_series(data_series, sort_mode):
161
+ """Sort data series based on the specified mode."""
162
+ if sort_mode == "original" or len(data_series) == 0:
163
+ return data_series
164
+
165
+ if sort_mode == "mean_y_desc":
166
+ # Sort by mean Y descending (higher Y value = lower on screen in image coords)
167
+ # For chart interpretation: lower Y pixel = higher value, so desc means high→low value
168
+ return sorted(data_series, key=lambda s: np.mean([pt[1] for pt in s["points"]]))
169
+ elif sort_mode == "mean_y_asc":
170
+ # Sort by mean Y ascending
171
+ return sorted(data_series, key=lambda s: np.mean([pt[1] for pt in s["points"]]), reverse=True)
172
+
173
+ return data_series
174
+
175
+
176
+ def extract_lines(infer_module, img, downsample_mode, fixed_step, max_points):
177
+ """Extract line data from image."""
178
+ line_dataseries = infer_module.get_dataseries(img, to_clean=False)
179
+
180
+ data_series = []
181
+ for line in line_dataseries:
182
+ if len(line) == 0:
183
+ continue
184
+
185
+ # Extract all points
186
+ all_points = [[int(pt['x']), int(pt['y'])] for pt in line]
187
+
188
+ # Downsample
189
+ points = downsample_points(all_points, downsample_mode, fixed_step, max_points)
190
+
191
+ data_series.append({"points": points})
192
+
193
+ return data_series, line_dataseries
194
+
195
+
196
+ def draw_points_on_image(img, data_series, axis_config=None):
197
+ """Draw extracted points as symbols on image, with optional axis calibration markers."""
198
+ import line_utils
199
+
200
+ result_img = img.copy()
201
+ num_lines = len(data_series)
202
+ colors = list(line_utils.get_distinct_colors(num_lines))
203
+
204
+ # Marker symbols (using different shapes)
205
+ markers = [
206
+ cv2.MARKER_CROSS,
207
+ cv2.MARKER_DIAMOND,
208
+ cv2.MARKER_SQUARE,
209
+ cv2.MARKER_TRIANGLE_UP,
210
+ cv2.MARKER_TRIANGLE_DOWN,
211
+ cv2.MARKER_STAR,
212
+ ]
213
+
214
+ for line_idx, series in enumerate(data_series):
215
+ color = colors[line_idx]
216
+ marker = markers[line_idx % len(markers)]
217
+
218
+ for pt in series["points"]:
219
+ x, y = int(pt[0]), int(pt[1])
220
+ cv2.drawMarker(result_img, (x, y), color, marker, markerSize=8, thickness=2)
221
+
222
+ # Draw axis calibration points if available
223
+ if axis_config is not None:
224
+ # Single color for all calibration points (Magenta - visible on white backgrounds)
225
+ calib_color = (255, 0, 255) # Magenta (BGR)
226
+ outline_color = (0, 0, 0) # Black outline for contrast
227
+ font = cv2.FONT_HERSHEY_SIMPLEX
228
+ font_scale = 0.8
229
+ thickness = 2
230
+
231
+ # Helper function to draw text with background
232
+ def draw_text_with_bg(img, text, pos, color):
233
+ (text_w, text_h), baseline = cv2.getTextSize(text, font, font_scale, thickness)
234
+ x, y = pos
235
+ # Draw background rectangle
236
+ padding = 3
237
+ cv2.rectangle(img, (x - padding, y - text_h - padding),
238
+ (x + text_w + padding, y + baseline + padding),
239
+ (255, 255, 255), -1) # White background
240
+ cv2.rectangle(img, (x - padding, y - text_h - padding),
241
+ (x + text_w + padding, y + baseline + padding),
242
+ outline_color, 1) # Black border
243
+ # Draw text
244
+ cv2.putText(img, text, (x, y), font, font_scale, color, thickness)
245
+
246
+ # Helper function to draw calibration point with marker
247
+ def draw_calib_point(img, x, y, color, label, label_offset):
248
+ # Draw filled circle with outline
249
+ cv2.circle(img, (x, y), 12, outline_color, 3) # Black outline
250
+ cv2.circle(img, (x, y), 10, color, -1) # Filled circle
251
+ cv2.circle(img, (x, y), 10, outline_color, 2) # Inner outline
252
+ # Draw crosshair inside circle
253
+ cv2.line(img, (x - 6, y), (x + 6, y), outline_color, 2)
254
+ cv2.line(img, (x, y - 6), (x, y + 6), outline_color, 2)
255
+ # Draw label with background
256
+ label_x = x + label_offset[0]
257
+ label_y = y + label_offset[1]
258
+ draw_text_with_bg(img, label, (label_x, label_y), color)
259
+
260
+ # Get calibration points
261
+ x1_x, x1_y = int(axis_config['x1_px']), int(axis_config['x1_py'])
262
+ x2_x, x2_y = int(axis_config['x2_px']), int(axis_config['x2_py'])
263
+ y1_x, y1_y = int(axis_config['y1_px']), int(axis_config['y1_py'])
264
+ y2_x, y2_y = int(axis_config['y2_px']), int(axis_config['y2_py'])
265
+
266
+ # Draw dashed lines connecting calibration points
267
+ # X-axis line (X1 to X2)
268
+ dash_length = 10
269
+ gap_length = 5
270
+ # Draw dashed line for X-axis
271
+ dx = x2_x - x1_x
272
+ dy = x2_y - x1_y
273
+ dist = max(1, int(np.sqrt(dx*dx + dy*dy)))
274
+ for i in range(0, dist, dash_length + gap_length):
275
+ start_x = int(x1_x + dx * i / dist)
276
+ start_y = int(x1_y + dy * i / dist)
277
+ end_i = min(i + dash_length, dist)
278
+ end_x = int(x1_x + dx * end_i / dist)
279
+ end_y = int(x1_y + dy * end_i / dist)
280
+ cv2.line(result_img, (start_x, start_y), (end_x, end_y), calib_color, 2)
281
+
282
+ # Draw dashed line for Y-axis
283
+ dx = y2_x - y1_x
284
+ dy = y2_y - y1_y
285
+ dist = max(1, int(np.sqrt(dx*dx + dy*dy)))
286
+ for i in range(0, dist, dash_length + gap_length):
287
+ start_x = int(y1_x + dx * i / dist)
288
+ start_y = int(y1_y + dy * i / dist)
289
+ end_i = min(i + dash_length, dist)
290
+ end_x = int(y1_x + dx * end_i / dist)
291
+ end_y = int(y1_y + dy * end_i / dist)
292
+ cv2.line(result_img, (start_x, start_y), (end_x, end_y), calib_color, 2)
293
+
294
+ # Draw calibration points with labels
295
+ # X1 point (left on X axis)
296
+ draw_calib_point(result_img, x1_x, x1_y, calib_color,
297
+ f"X1={axis_config['x1_val']}", (15, -5))
298
+
299
+ # X2 point (right on X axis) - label on left side to avoid edge
300
+ draw_calib_point(result_img, x2_x, x2_y, calib_color,
301
+ f"X2={axis_config['x2_val']}", (-100, -5))
302
+
303
+ # Y1 point (bottom on Y axis)
304
+ draw_calib_point(result_img, y1_x, y1_y, calib_color,
305
+ f"Y1={axis_config['y1_val']}", (15, 20))
306
+
307
+ # Y2 point (top on Y axis)
308
+ draw_calib_point(result_img, y2_x, y2_y, calib_color,
309
+ f"Y2={axis_config['y2_val']}", (15, -5))
310
+
311
+ return result_img
312
+
313
+
314
+ def convert_to_starry_digitizer_format(data_series, img_shape, axis_config=None):
315
+ """
316
+ Convert LineFormer output to StarryDigitizer project.json format.
317
+
318
+ axis_config: dict with x1, x2, y1, y2 pixel coordinates and values
319
+ """
320
+ timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
321
+
322
+ # Default axis set (user needs to calibrate in StarryDigitizer)
323
+ if axis_config is None:
324
+ # Use image corners as default
325
+ axis_set = {
326
+ "id": 1,
327
+ "name": "XY Axes 1",
328
+ "x1": {
329
+ "name": "x1",
330
+ "value": 0,
331
+ "coord": {"xPx": 0, "yPx": float(img_shape[0])}
332
+ },
333
+ "x2": {
334
+ "name": "x2",
335
+ "value": 100,
336
+ "coord": {"xPx": float(img_shape[1]), "yPx": float(img_shape[0])}
337
+ },
338
+ "y1": {
339
+ "name": "y1",
340
+ "value": 0,
341
+ "coord": {"xPx": 0, "yPx": float(img_shape[0])}
342
+ },
343
+ "y2": {
344
+ "name": "y2",
345
+ "value": 100,
346
+ "coord": {"xPx": 0, "yPx": 0}
347
+ },
348
+ "xIsLogScale": False,
349
+ "yIsLogScale": False,
350
+ "considerGraphTilt": False,
351
+ "pointMode": 0,
352
+ "isVisible": True
353
+ }
354
+ else:
355
+ axis_set = {
356
+ "id": 1,
357
+ "name": "XY Axes 1",
358
+ "x1": {
359
+ "name": "x1",
360
+ "value": axis_config["x1_val"],
361
+ "coord": {"xPx": axis_config["x1_px"], "yPx": axis_config["x1_py"]}
362
+ },
363
+ "x2": {
364
+ "name": "x2",
365
+ "value": axis_config["x2_val"],
366
+ "coord": {"xPx": axis_config["x2_px"], "yPx": axis_config["x2_py"]}
367
+ },
368
+ "y1": {
369
+ "name": "y1",
370
+ "value": axis_config["y1_val"],
371
+ "coord": {"xPx": axis_config["y1_px"], "yPx": axis_config["y1_py"]}
372
+ },
373
+ "y2": {
374
+ "name": "y2",
375
+ "value": axis_config["y2_val"],
376
+ "coord": {"xPx": axis_config["y2_px"], "yPx": axis_config["y2_py"]}
377
+ },
378
+ "xIsLogScale": axis_config.get("xIsLogScale", False),
379
+ "yIsLogScale": axis_config.get("yIsLogScale", False),
380
+ "considerGraphTilt": False,
381
+ "pointMode": 0,
382
+ "isVisible": True
383
+ }
384
+
385
+ # Convert datasets
386
+ datasets = []
387
+
388
+ # Add empty dataset 1 (StarryDigitizer convention)
389
+ datasets.append({
390
+ "id": 1,
391
+ "name": "dataset 1",
392
+ "axisSetId": 1,
393
+ "points": [],
394
+ "visiblePointIds": [],
395
+ "manuallyAddedPointIds": []
396
+ })
397
+
398
+ # Add extracted lines
399
+ for idx, series in enumerate(data_series):
400
+ points = []
401
+ visible_ids = []
402
+ for pt_idx, pt in enumerate(series["points"]):
403
+ pt_id = pt_idx + 1
404
+ points.append({
405
+ "id": pt_id,
406
+ "xPx": float(pt[0]),
407
+ "yPx": float(pt[1])
408
+ })
409
+ visible_ids.append(pt_id)
410
+
411
+ datasets.append({
412
+ "id": idx + 2, # Start from 2 (1 is empty dataset)
413
+ "name": f"Line {idx + 1}",
414
+ "axisSetId": 1,
415
+ "points": points,
416
+ "visiblePointIds": visible_ids,
417
+ "manuallyAddedPointIds": []
418
+ })
419
+
420
+ project = {
421
+ "version": "1.11.2",
422
+ "timestamp": timestamp,
423
+ "axisSets": [axis_set],
424
+ "activeAxisSetId": 1,
425
+ "datasets": datasets,
426
+ "activeDatasetId": len(datasets),
427
+ "canvasHandler": {
428
+ "scale": 1.0,
429
+ "manualMode": 0
430
+ }
431
+ }
432
+
433
+ return project
434
+
435
+
436
+ def create_starry_digitizer_zip(img, project_json):
437
+ """
438
+ Create a ZIP file containing image.png and project.json
439
+ for StarryDigitizer import.
440
+ """
441
+ zip_buffer = io.BytesIO()
442
+
443
+ with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
444
+ # Add image.png
445
+ _, img_encoded = cv2.imencode('.png', img)
446
+ zf.writestr('image.png', img_encoded.tobytes())
447
+
448
+ # Add project.json
449
+ json_str = json.dumps(project_json, indent=2, ensure_ascii=False)
450
+ zf.writestr('project.json', json_str.encode('utf-8'))
451
+
452
+ zip_buffer.seek(0)
453
+ return zip_buffer
454
+
455
+
456
+ def convert_to_wpd_format(data_series, img_shape, axis_config=None):
457
+ """
458
+ Convert LineFormer output to WebPlotDigitizer JSON format.
459
+
460
+ This format can be loaded in WebPlotDigitizer after loading the image.
461
+ """
462
+ # Default calibration points (user needs to recalibrate in WPD)
463
+ if axis_config is None:
464
+ calibration_points = [
465
+ {"px": 0.0, "py": float(img_shape[0]), "dx": "0", "dy": "0", "dz": None},
466
+ {"px": float(img_shape[1]), "py": float(img_shape[0]), "dx": "100", "dy": "0", "dz": None},
467
+ {"px": 0.0, "py": float(img_shape[0]), "dx": "0", "dy": "0", "dz": None},
468
+ {"px": 0.0, "py": 0.0, "dx": "0", "dy": "100", "dz": None}
469
+ ]
470
+ is_log_x = False
471
+ is_log_y = False
472
+ else:
473
+ calibration_points = [
474
+ {"px": axis_config["x1_px"], "py": axis_config["x1_py"],
475
+ "dx": str(axis_config["x1_val"]), "dy": str(axis_config["y1_val"]), "dz": None},
476
+ {"px": axis_config["x2_px"], "py": axis_config["x2_py"],
477
+ "dx": str(axis_config["x2_val"]), "dy": str(axis_config["y1_val"]), "dz": None},
478
+ {"px": axis_config["y1_px"], "py": axis_config["y1_py"],
479
+ "dx": str(axis_config["x1_val"]), "dy": str(axis_config["y1_val"]), "dz": None},
480
+ {"px": axis_config["y2_px"], "py": axis_config["y2_py"],
481
+ "dx": str(axis_config["x1_val"]), "dy": str(axis_config["y2_val"]), "dz": None}
482
+ ]
483
+ is_log_x = axis_config.get("xIsLogScale", False)
484
+ is_log_y = axis_config.get("yIsLogScale", False)
485
+
486
+ axes_coll = [{
487
+ "name": "XY",
488
+ "type": "XYAxes",
489
+ "isLogX": is_log_x,
490
+ "isLogY": is_log_y,
491
+ "noRotation": False,
492
+ "calibrationPoints": calibration_points
493
+ }]
494
+
495
+ # Convert datasets
496
+ dataset_coll = []
497
+
498
+ # Add empty default dataset (WPD convention)
499
+ dataset_coll.append({
500
+ "name": "Default Dataset",
501
+ "axesName": "XY",
502
+ "colorRGB": [200, 0, 0, 255],
503
+ "metadataKeys": [],
504
+ "data": [],
505
+ "autoDetectionData": None
506
+ })
507
+
508
+ # Add extracted lines
509
+ for idx, series in enumerate(data_series):
510
+ data_points = []
511
+ for pt in series["points"]:
512
+ # WPD format: x, y are pixel coords, value is [realX, realY] (null if not calibrated)
513
+ data_points.append({
514
+ "x": float(pt[0]),
515
+ "y": float(pt[1]),
516
+ "value": None # Will be calculated by WPD after calibration
517
+ })
518
+
519
+ dataset_coll.append({
520
+ "name": f"Dataset {idx + 1}",
521
+ "axesName": "XY",
522
+ "colorRGB": [200, 0, 0, 255],
523
+ "metadataKeys": [],
524
+ "data": data_points,
525
+ "autoDetectionData": None
526
+ })
527
+
528
+ wpd_json = {
529
+ "version": [4, 2],
530
+ "axesColl": axes_coll,
531
+ "datasetColl": dataset_coll,
532
+ "measurementColl": []
533
+ }
534
+
535
+ return wpd_json
536
+
537
+
538
+ def create_wpd_tar(img, wpd_json, project_name="project"):
539
+ """
540
+ Create a TAR file for WebPlotDigitizer import.
541
+
542
+ WPD expects TAR structure:
543
+ projectName/
544
+ projectName/info.json
545
+ projectName/wpd.json
546
+ projectName/image.png
547
+ """
548
+ import time
549
+ tar_buffer = io.BytesIO()
550
+ mtime = time.time()
551
+
552
+ with tarfile.open(fileobj=tar_buffer, mode='w') as tf:
553
+ # Add project folder
554
+ folder_info = tarfile.TarInfo(name=f'{project_name}/')
555
+ folder_info.type = tarfile.DIRTYPE
556
+ folder_info.mtime = mtime
557
+ tf.addfile(folder_info)
558
+
559
+ # Add info.json
560
+ info_json = {
561
+ "version": [4, 0],
562
+ "json": "wpd.json",
563
+ "images": ["image.png"]
564
+ }
565
+ info_bytes = json.dumps(info_json, ensure_ascii=False).encode('utf-8')
566
+ info_tarinfo = tarfile.TarInfo(name=f'{project_name}/info.json')
567
+ info_tarinfo.size = len(info_bytes)
568
+ info_tarinfo.mtime = mtime
569
+ tf.addfile(info_tarinfo, io.BytesIO(info_bytes))
570
+
571
+ # Add wpd.json
572
+ wpd_bytes = json.dumps(wpd_json, ensure_ascii=False).encode('utf-8')
573
+ wpd_tarinfo = tarfile.TarInfo(name=f'{project_name}/wpd.json')
574
+ wpd_tarinfo.size = len(wpd_bytes)
575
+ wpd_tarinfo.mtime = mtime
576
+ tf.addfile(wpd_tarinfo, io.BytesIO(wpd_bytes))
577
+
578
+ # Add image.png
579
+ _, img_encoded = cv2.imencode('.png', img)
580
+ img_bytes = img_encoded.tobytes()
581
+ img_tarinfo = tarfile.TarInfo(name=f'{project_name}/image.png')
582
+ img_tarinfo.size = len(img_bytes)
583
+ img_tarinfo.mtime = mtime
584
+ tf.addfile(img_tarinfo, io.BytesIO(img_bytes))
585
+
586
+ tar_buffer.seek(0)
587
+ return tar_buffer
588
+
589
+
590
+ def main():
591
+ st.title("📈 AutoLineDigitizer")
592
+ st.markdown("""
593
+ Upload a chart image to extract line data automatically.
594
+ Output is compatible with **[StarryDigitizer](https://starrydigitizer.vercel.app/)** and **[WebPlotDigitizer](https://apps.automeris.io/wpd4/)**.
595
+
596
+ **[LineFormer Paper (ICDAR 2023)](https://arxiv.org/abs/2305.01837)** |
597
+ **[ChartDete Paper (ICDAR 2023)](https://arxiv.org/abs/2305.04151)**
598
+ """)
599
+
600
+ # Sidebar settings
601
+ st.sidebar.header("Settings")
602
+
603
+ show_visualization = st.sidebar.checkbox("Show visualization", value=True)
604
+
605
+ st.sidebar.subheader("Axis Detection")
606
+ auto_axis = st.sidebar.checkbox("Auto-detect axis (ChartDete + OCR)", value=True,
607
+ help="Automatically detect axis labels and calibration")
608
+
609
+ st.sidebar.subheader("Line Sorting")
610
+ sort_mode = st.sidebar.selectbox(
611
+ "Sort by",
612
+ options=["original", "mean_y_desc", "mean_y_asc"],
613
+ format_func=lambda x: {
614
+ "original": "Original (Detection Order)",
615
+ "mean_y_desc": "Mean Y (High → Low)",
616
+ "mean_y_asc": "Mean Y (Low → High)",
617
+ }[x]
618
+ )
619
+
620
+ st.sidebar.subheader("Downsampling")
621
+ downsample_mode = st.sidebar.selectbox(
622
+ "Mode",
623
+ options=["max_points", "fixed", "none"],
624
+ index=0,
625
+ help="max_points: Limit points per line, fixed: Every N points, none: All points"
626
+ )
627
+
628
+ if downsample_mode == "fixed":
629
+ fixed_step = st.sidebar.slider("Fixed step (every N points)", 1, 50, 10)
630
+ max_points = 50
631
+ elif downsample_mode == "max_points":
632
+ max_points = st.sidebar.slider("Max points per line", 10, 200, 50)
633
+ fixed_step = 10
634
+ else:
635
+ fixed_step = 10
636
+ max_points = 50
637
+
638
+ # Load LineFormer model
639
+ with st.spinner("Loading LineFormer model..."):
640
+ try:
641
+ infer_module = load_lineformer_model()
642
+ st.sidebar.success("LineFormer loaded!")
643
+ except Exception as e:
644
+ st.error(f"Failed to load LineFormer: {e}")
645
+ st.stop()
646
+
647
+ # Load ChartDete model if needed
648
+ chartdete_module = None
649
+ if auto_axis:
650
+ with st.spinner("Loading ChartDete model..."):
651
+ try:
652
+ chartdete_module = load_chartdete_model()
653
+ st.sidebar.success("ChartDete loaded!")
654
+ except Exception as e:
655
+ st.warning(f"ChartDete not available: {e}")
656
+ auto_axis = False
657
+
658
+ # File upload
659
+ uploaded_file = st.file_uploader(
660
+ "Upload a chart image",
661
+ type=["png", "jpg", "jpeg", "bmp", "tiff"]
662
+ )
663
+
664
+ # Status placeholder right after file uploader (below Drag and drop)
665
+ status_placeholder = st.empty()
666
+
667
+ if uploaded_file is not None:
668
+ # Read image
669
+ file_bytes = np.asarray(bytearray(uploaded_file.read()), dtype=np.uint8)
670
+ img = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
671
+
672
+ if img is None:
673
+ st.error("Failed to read image")
674
+ st.stop()
675
+
676
+ # Initialize axis detection variables
677
+ axis_config = None
678
+ detections = None
679
+ ocr_results = None
680
+
681
+ # Display columns - show input image immediately
682
+ col1, col2 = st.columns(2)
683
+
684
+ with col1:
685
+ st.subheader("Input Image")
686
+ st.image(cv2.cvtColor(img, cv2.COLOR_BGR2RGB), use_container_width=True)
687
+ st.caption(f"Size: {img.shape[1]} x {img.shape[0]} pixels")
688
+
689
+ with col2:
690
+ # Create placeholders for dynamic updates
691
+ st.subheader("Extraction Result")
692
+ viz_placeholder = st.empty()
693
+ summary_placeholder = st.empty()
694
+ caption_placeholder = st.empty()
695
+
696
+ # Placeholder for axis calibration results (outside columns)
697
+ axis_placeholder = st.empty()
698
+
699
+ # Step 1: Extract lines (faster) - with spinner
700
+ with status_placeholder.container():
701
+ with st.spinner("⏳ Extracting lines (LineFormer)..."):
702
+ data_series, raw_lines = extract_lines(
703
+ infer_module, img, downsample_mode, fixed_step, max_points
704
+ )
705
+
706
+ # Sort lines
707
+ data_series = sort_data_series(data_series, sort_mode)
708
+
709
+ # Show initial result (without axis calibration) immediately
710
+ if show_visualization:
711
+ result_img = draw_points_on_image(img, data_series, None)
712
+ viz_placeholder.image(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB), use_container_width=True)
713
+
714
+ # Show line summary
715
+ total_points = sum(len(s['points']) for s in data_series)
716
+ line_pts = [len(s['points']) for s in data_series]
717
+ summary_placeholder.success(f"**{len(data_series)} lines** detected ({total_points} points total)")
718
+ caption_placeholder.caption(f"Points per line: {', '.join(map(str, line_pts))}")
719
+
720
+ # Step 2: Run axis detection (slower, OCR-heavy) - with spinner
721
+ if auto_axis and chartdete_module is not None:
722
+ with status_placeholder.container():
723
+ with st.spinner("🔍 Detecting axis labels (ChartDete + OCR)..."):
724
+ axis_config, detections, ocr_results = detect_axis_calibration(
725
+ chartdete_module, img
726
+ )
727
+
728
+ # Update visualization with axis calibration
729
+ if show_visualization:
730
+ result_img = draw_points_on_image(img, data_series, axis_config)
731
+ viz_placeholder.image(cv2.cvtColor(result_img, cv2.COLOR_BGR2RGB), use_container_width=True)
732
+
733
+ # Clear the status
734
+ status_placeholder.empty()
735
+ else:
736
+ # Clear status if no axis detection
737
+ status_placeholder.empty()
738
+
739
+ # Show axis calibration results
740
+ if axis_config is not None:
741
+ with axis_placeholder.container():
742
+ with st.expander("Axis Calibration (Auto-detected)", expanded=True):
743
+ col_x, col_y = st.columns(2)
744
+ with col_x:
745
+ st.markdown("**X-axis:**")
746
+ st.write(f" {axis_config['x1_val']} → {axis_config['x2_val']}")
747
+ with col_y:
748
+ st.markdown("**Y-axis:**")
749
+ st.write(f" {axis_config['y1_val']} → {axis_config['y2_val']}")
750
+
751
+ # Show OCR details
752
+ if ocr_results:
753
+ st.markdown("**Detected Labels:**")
754
+ ocr_text = []
755
+ if 'xlabels' in ocr_results:
756
+ x_vals = [f"{l['value']}" for l in ocr_results['xlabels'] if l['value'] is not None]
757
+ ocr_text.append(f"X: [{', '.join(x_vals)}]")
758
+ if 'ylabels' in ocr_results:
759
+ y_vals = [f"{l['value']}" for l in ocr_results['ylabels'] if l['value'] is not None]
760
+ ocr_text.append(f"Y: [{', '.join(y_vals)}]")
761
+ st.caption(' | '.join(ocr_text))
762
+ elif auto_axis:
763
+ axis_placeholder.warning("Could not auto-detect axis calibration. Manual calibration needed in WPD/StarryDigitizer.")
764
+
765
+ # Build StarryDigitizer project
766
+ project_json = convert_to_starry_digitizer_format(
767
+ data_series, img.shape, axis_config
768
+ )
769
+
770
+ # Build WebPlotDigitizer project
771
+ wpd_json = convert_to_wpd_format(
772
+ data_series, img.shape, axis_config
773
+ )
774
+
775
+ # Create ZIP file for StarryDigitizer
776
+ zip_buffer = create_starry_digitizer_zip(img, project_json)
777
+
778
+ # Create TAR file for WebPlotDigitizer
779
+ base_name = os.path.splitext(uploaded_file.name)[0]
780
+ tar_buffer = create_wpd_tar(img, wpd_json, project_name=base_name)
781
+
782
+ # Generate filenames
783
+ timestamp_str = datetime.now().strftime('%Y%m%d-%H%M%S')
784
+ zip_filename = f"sd-{timestamp_str}.zip"
785
+ tar_filename = f"wpd-{timestamp_str}.tar"
786
+
787
+ # Download buttons
788
+ st.subheader("Download")
789
+ col_dl1, col_dl2, col_dl3 = st.columns(3)
790
+
791
+ with col_dl1:
792
+ st.download_button(
793
+ label="📦 StarryDigitizer (.zip)",
794
+ data=zip_buffer.getvalue(),
795
+ file_name=zip_filename,
796
+ mime="application/zip",
797
+ help="ZIP containing image.png + project.json"
798
+ )
799
+
800
+ with col_dl2:
801
+ st.download_button(
802
+ label="📊 WebPlotDigitizer (.tar)",
803
+ data=tar_buffer.getvalue(),
804
+ file_name=tar_filename,
805
+ mime="application/x-tar",
806
+ help="TAR with project folder structure"
807
+ )
808
+
809
+ with col_dl3:
810
+ if show_visualization:
811
+ _, buffer = cv2.imencode('.png', result_img)
812
+ st.download_button(
813
+ label="🖼️ Visualization (.png)",
814
+ data=buffer.tobytes(),
815
+ file_name=f"{base_name}_result.png",
816
+ mime="image/png"
817
+ )
818
+
819
+ # Show JSON previews
820
+ with st.expander("Preview StarryDigitizer project.json"):
821
+ json_str = json.dumps(project_json, indent=2, ensure_ascii=False)
822
+ if len(json_str) > 5000:
823
+ st.code(json_str[:5000] + "\n... (truncated)", language="json")
824
+ else:
825
+ st.code(json_str, language="json")
826
+
827
+ with st.expander("Preview WebPlotDigitizer wpd.json"):
828
+ wpd_preview = json.dumps(wpd_json, indent=2, ensure_ascii=False)
829
+ if len(wpd_preview) > 5000:
830
+ st.code(wpd_preview[:5000] + "\n... (truncated)", language="json")
831
+ else:
832
+ st.code(wpd_preview, language="json")
833
+
834
+ # Instructions
835
+ st.info("""
836
+ **StarryDigitizer:** Download ZIP → Open [StarryDigitizer](https://starrydigitizer.vercel.app/) → Load Project
837
+
838
+ **WebPlotDigitizer:** Download TAR → Open [WPD](https://apps.automeris.io/wpd4/) → File → Load Project (.tar)
839
+ """)
840
+
841
+
842
+ if __name__ == "__main__":
843
+ main()
src/chartdete_infer.py ADDED
@@ -0,0 +1,711 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ChartDete inference module with CPU support.
4
+ Uses torchvision NMS fallback for CPU inference.
5
+ Includes EasyOCR for reading axis label text.
6
+ """
7
+ import sys
8
+ import os
9
+ import re
10
+
11
+ # Project paths
12
+ _src_dir = os.path.dirname(os.path.abspath(__file__))
13
+ _project_root = os.path.dirname(_src_dir)
14
+
15
+ # ChartDete submodule path
16
+ _chartdete_path = os.path.join(_project_root, 'submodules', 'chartdete')
17
+
18
+ import torch
19
+ import numpy as np
20
+ import cv2
21
+ import torchvision.ops as tv_ops
22
+
23
+ # EasyOCR (lazy load)
24
+ _ocr_reader = None
25
+
26
+
27
+ def _patch_mmcv_ops():
28
+ """Patch mmcv ops to use torchvision for CPU.
29
+
30
+ Works with both mmcv-full (has C++ extensions) and mmcv lite (pure Python).
31
+ When C++ extensions are not available, directly replaces ops with torchvision.
32
+ """
33
+ import mmcv.ops
34
+ import mmcv.ops.nms as mmcv_nms_module
35
+
36
+ # ========== Patch NMSop class ==========
37
+ class TorchvisionNMSop(torch.autograd.Function):
38
+ @staticmethod
39
+ def forward(ctx, bboxes, scores, iou_threshold, offset, score_threshold, max_num):
40
+ is_filtering_by_score = score_threshold > 0
41
+ if is_filtering_by_score:
42
+ valid_mask = scores > score_threshold
43
+ bboxes_f, scores_f = bboxes[valid_mask], scores[valid_mask]
44
+ valid_inds = torch.nonzero(valid_mask, as_tuple=False).squeeze(dim=1)
45
+ else:
46
+ bboxes_f, scores_f = bboxes, scores
47
+ valid_inds = None
48
+
49
+ if bboxes_f.numel() == 0:
50
+ return torch.zeros(0, dtype=torch.long, device=bboxes.device)
51
+
52
+ # Use torchvision NMS
53
+ inds = tv_ops.nms(bboxes_f, scores_f, iou_threshold)
54
+
55
+ if max_num > 0:
56
+ inds = inds[:max_num]
57
+ if is_filtering_by_score and valid_inds is not None:
58
+ inds = valid_inds[inds]
59
+ return inds
60
+
61
+ @staticmethod
62
+ def backward(ctx, grad_output):
63
+ return None, None, None, None, None, None
64
+
65
+ # Replace NMSop in the module
66
+ mmcv_nms_module.NMSop = TorchvisionNMSop
67
+
68
+ # ========== Patch batched_nms ==========
69
+ def torchvision_batched_nms(boxes, scores, idxs, nms_cfg, class_agnostic=False):
70
+ """Batched NMS using torchvision."""
71
+ nms_cfg_ = nms_cfg.copy()
72
+ class_agnostic = nms_cfg_.pop('class_agnostic', class_agnostic)
73
+ iou_thr = nms_cfg_.get('iou_threshold', nms_cfg_.get('iou_thr', 0.5))
74
+
75
+ if class_agnostic:
76
+ boxes_for_nms = boxes
77
+ else:
78
+ if boxes.numel() == 0:
79
+ return boxes.new_zeros((0, 5)), torch.zeros(0, dtype=torch.long, device=boxes.device)
80
+ max_coordinate = boxes.max()
81
+ offsets = idxs.to(boxes) * (max_coordinate + 1)
82
+ boxes_for_nms = boxes + offsets[:, None]
83
+
84
+ if boxes_for_nms.numel() == 0:
85
+ return boxes.new_zeros((0, 5)), torch.zeros(0, dtype=torch.long, device=boxes.device)
86
+
87
+ keep = tv_ops.nms(boxes_for_nms, scores, iou_thr)
88
+
89
+ max_num = nms_cfg_.get('max_num', -1)
90
+ if max_num > 0 and len(keep) > max_num:
91
+ keep = keep[:max_num]
92
+
93
+ dets = torch.cat([boxes[keep], scores[keep].unsqueeze(1)], dim=1)
94
+ return dets, keep
95
+
96
+ # Replace batched_nms in both places
97
+ mmcv.ops.batched_nms = torchvision_batched_nms
98
+ mmcv_nms_module.batched_nms = torchvision_batched_nms
99
+
100
+ # ========== RoIAlign ==========
101
+ def patched_roi_align(input, rois, output_size, spatial_scale=1.0,
102
+ sampling_ratio=-1, pool_mode='avg', aligned=True):
103
+ """Use torchvision roi_align as fallback."""
104
+ return tv_ops.roi_align(
105
+ input, rois, output_size,
106
+ spatial_scale=spatial_scale,
107
+ sampling_ratio=sampling_ratio if sampling_ratio > 0 else 2,
108
+ aligned=aligned
109
+ )
110
+
111
+ # Directly replace roi_align with torchvision version
112
+ mmcv.ops.roi_align = patched_roi_align
113
+
114
+ # Patch RoIAlign class forward method
115
+ from mmcv.ops import RoIAlign
116
+
117
+ def patched_roialign_forward(self, input, rois):
118
+ return tv_ops.roi_align(
119
+ input, rois, self.output_size,
120
+ spatial_scale=self.spatial_scale,
121
+ sampling_ratio=self.sampling_ratio if self.sampling_ratio > 0 else 2,
122
+ aligned=self.aligned
123
+ )
124
+
125
+ RoIAlign.forward = patched_roialign_forward
126
+
127
+
128
+ # Apply patch before importing mmdet
129
+ _patch_mmcv_ops()
130
+
131
+ from mmdet.apis import init_detector, inference_detector
132
+
133
+ # ChartDete classes
134
+ CHARTDETE_CLASSES = [
135
+ 'x_title', 'y_title', 'plot_area', 'other', 'xlabel', 'ylabel',
136
+ 'chart_title', 'x_tick', 'y_tick', 'legend_patch', 'legend_label',
137
+ 'legend_title', 'legend_area', 'mark_label', 'value_label',
138
+ 'y_axis_area', 'x_axis_area', 'tick_grouping'
139
+ ]
140
+
141
+ # Indices for axis-related classes
142
+ AXIS_CLASSES = {
143
+ 'x_title': 0,
144
+ 'y_title': 1,
145
+ 'plot_area': 2,
146
+ 'xlabel': 4,
147
+ 'ylabel': 5,
148
+ 'x_tick': 7,
149
+ 'y_tick': 8,
150
+ 'y_axis_area': 15,
151
+ 'x_axis_area': 16,
152
+ }
153
+
154
+
155
+ _model = None
156
+
157
+
158
+ def load_chartdete_model(config_path=None, checkpoint_path=None, device='cpu'):
159
+ """Load ChartDete model."""
160
+ global _model
161
+
162
+ if config_path is None:
163
+ config_path = os.path.join(_project_root, 'config', 'chartdete_config.py')
164
+
165
+ if checkpoint_path is None:
166
+ checkpoint_path = os.path.join(_project_root, 'models', 'checkpoint.pth')
167
+
168
+ _model = init_detector(config_path, checkpoint_path, device=device)
169
+ return _model
170
+
171
+
172
+ def detect_chart_elements(img, score_thr=0.5, model=None):
173
+ """
174
+ Detect chart elements in an image.
175
+
176
+ Args:
177
+ img: Image path or numpy array (BGR)
178
+ score_thr: Score threshold for detection
179
+ model: Optional model instance
180
+
181
+ Returns:
182
+ dict: Detection results keyed by class name
183
+ Each value is a list of [x1, y1, x2, y2, score]
184
+ """
185
+ global _model
186
+
187
+ if model is None:
188
+ model = _model
189
+
190
+ if model is None:
191
+ raise ValueError("Model not loaded. Call load_chartdete_model() first.")
192
+
193
+ result = inference_detector(model, img)
194
+
195
+ detections = {}
196
+ for i, class_result in enumerate(result):
197
+ class_name = CHARTDETE_CLASSES[i]
198
+ if len(class_result) > 0:
199
+ high_conf = class_result[class_result[:, 4] > score_thr]
200
+ if len(high_conf) > 0:
201
+ detections[class_name] = high_conf.tolist()
202
+
203
+ return detections
204
+
205
+
206
+ def get_ocr_reader():
207
+ """Get or initialize EasyOCR reader."""
208
+ global _ocr_reader
209
+ if _ocr_reader is None:
210
+ import easyocr
211
+ # Use bundled models for PyInstaller builds
212
+ model_dir = None
213
+ if getattr(sys, 'frozen', False):
214
+ model_dir = os.path.join(sys._MEIPASS, 'easyocr_models')
215
+ if model_dir and os.path.isdir(model_dir):
216
+ _ocr_reader = easyocr.Reader(['en'], gpu=False, model_storage_directory=model_dir, download_enabled=False)
217
+ else:
218
+ _ocr_reader = easyocr.Reader(['en'], gpu=False)
219
+ return _ocr_reader
220
+
221
+
222
+ def parse_numeric_value(text):
223
+ """
224
+ Parse numeric value from OCR text.
225
+ Handles scientific notation, negative numbers, decimals.
226
+
227
+ Returns:
228
+ float or None if parsing fails
229
+ """
230
+ if not text:
231
+ return None
232
+
233
+ # Clean up text
234
+ text = text.strip()
235
+
236
+ # Handle common OCR errors
237
+ text = text.replace('O', '0').replace('o', '0')
238
+ text = text.replace('l', '1').replace('I', '1')
239
+ text = text.replace(',', '.') # European decimal
240
+ text = text.replace(' ', '')
241
+
242
+ # Try to extract number with regex
243
+ # Matches: -123, 12.34, 1.23e-4, 1.23E+4, etc.
244
+ pattern = r'[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?'
245
+ match = re.search(pattern, text)
246
+
247
+ if match:
248
+ try:
249
+ return float(match.group())
250
+ except ValueError:
251
+ return None
252
+ return None
253
+
254
+
255
+ def infer_axis_scale(values, positions):
256
+ """
257
+ Infer the correct axis scale from a list of values and positions.
258
+ Helps correct OCR errors by detecting linear patterns.
259
+
260
+ Args:
261
+ values: List of numeric values (may contain None or errors)
262
+ positions: List of pixel positions
263
+
264
+ Returns:
265
+ Corrected list of values
266
+ """
267
+ # Filter valid values
268
+ valid_pairs = [(v, p) for v, p in zip(values, positions) if v is not None]
269
+
270
+ if len(valid_pairs) < 2:
271
+ return values
272
+
273
+ # Try to find linear relationship
274
+ vs = [v for v, _ in valid_pairs]
275
+ ps = [p for _, p in valid_pairs]
276
+
277
+ # Check if values form a linear sequence
278
+ diffs = [vs[i+1] - vs[i] for i in range(len(vs)-1)]
279
+ pos_diffs = [ps[i+1] - ps[i] for i in range(len(ps)-1)]
280
+
281
+ # If roughly uniform spacing in both, infer scale
282
+ if len(diffs) >= 2:
283
+ # Calculate expected step size
284
+ avg_val_step = sum(diffs) / len(diffs)
285
+ avg_pos_step = sum(pos_diffs) / len(pos_diffs)
286
+
287
+ # Check consistency
288
+ if avg_pos_step != 0:
289
+ scale = avg_val_step / avg_pos_step
290
+
291
+ # Correct any outliers
292
+ corrected = list(values)
293
+ for i, (v, p) in enumerate(zip(values, positions)):
294
+ if v is None:
295
+ # Interpolate from neighbors
296
+ if i > 0 and values[i-1] is not None:
297
+ expected = values[i-1] + scale * (positions[i] - positions[i-1])
298
+ corrected[i] = round(expected)
299
+ return corrected
300
+
301
+ return values
302
+
303
+
304
+ def ocr_region(img, bbox, padding=10):
305
+ """
306
+ Run OCR on a specific region of the image.
307
+
308
+ Args:
309
+ img: Image (BGR numpy array)
310
+ bbox: [x1, y1, x2, y2] bounding box
311
+ padding: Extra pixels to add around bbox
312
+
313
+ Returns:
314
+ str: OCR result text
315
+ """
316
+ x1, y1, x2, y2 = [int(v) for v in bbox[:4]]
317
+ h, w = img.shape[:2]
318
+
319
+ # Add padding
320
+ x1 = max(0, x1 - padding)
321
+ y1 = max(0, y1 - padding)
322
+ x2 = min(w, x2 + padding)
323
+ y2 = min(h, y2 + padding)
324
+
325
+ # Crop region
326
+ crop = img[y1:y2, x1:x2]
327
+
328
+ if crop.size == 0:
329
+ return ""
330
+
331
+ # Preprocessing for better OCR
332
+ # Convert to grayscale
333
+ if len(crop.shape) == 3:
334
+ gray = cv2.cvtColor(crop, cv2.COLOR_BGR2GRAY)
335
+ else:
336
+ gray = crop
337
+
338
+ # Scale up significantly for small text (decimal points are tiny)
339
+ target_height = 128 # Larger for better decimal point detection
340
+ if gray.shape[0] < target_height:
341
+ scale = target_height / gray.shape[0]
342
+ gray = cv2.resize(gray, None, fx=scale, fy=scale, interpolation=cv2.INTER_CUBIC)
343
+
344
+ # Sharpening to make decimal points more visible
345
+ kernel = np.array([[-1, -1, -1],
346
+ [-1, 9, -1],
347
+ [-1, -1, -1]])
348
+ gray_sharp = cv2.filter2D(gray, -1, kernel)
349
+ gray_sharp = np.clip(gray_sharp, 0, 255).astype(np.uint8)
350
+
351
+ # Increase contrast using CLAHE
352
+ clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8, 8))
353
+ gray_contrast = clahe.apply(gray_sharp)
354
+
355
+ # Try multiple OCR approaches and pick best result
356
+ reader = get_ocr_reader()
357
+
358
+ all_results = []
359
+
360
+ # Approach 1: Sharpened + contrast enhanced
361
+ results1 = reader.readtext(gray_contrast, allowlist='0123456789.-eE+')
362
+ for r in results1:
363
+ all_results.append((r[1], r[2], 'sharp'))
364
+
365
+ # Approach 2: Binarized with adaptive threshold (better for small dots)
366
+ binary_adapt = cv2.adaptiveThreshold(gray_sharp, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
367
+ cv2.THRESH_BINARY, 11, 2)
368
+ results2 = reader.readtext(binary_adapt, allowlist='0123456789.-eE+')
369
+ for r in results2:
370
+ all_results.append((r[1], r[2], 'adapt'))
371
+
372
+ # Approach 3: Inverted binary
373
+ binary_inv = 255 - binary_adapt
374
+ results3 = reader.readtext(binary_inv, allowlist='0123456789.-eE+')
375
+ for r in results3:
376
+ all_results.append((r[1], r[2], 'inv'))
377
+
378
+ # Approach 4: Morphological enhancement (dilate to make dots larger)
379
+ kernel_dilate = np.ones((2, 2), np.uint8)
380
+ gray_dilated = cv2.dilate(gray_sharp, kernel_dilate, iterations=1)
381
+ results4 = reader.readtext(gray_dilated, allowlist='0123456789.-eE+')
382
+ for r in results4:
383
+ all_results.append((r[1], r[2], 'dilate'))
384
+
385
+ if not all_results:
386
+ return ""
387
+
388
+ # Prefer results that contain a decimal point (more likely to be correct)
389
+ results_with_dot = [(t, c, m) for t, c, m in all_results if '.' in t]
390
+ if results_with_dot:
391
+ results_with_dot.sort(key=lambda x: x[1], reverse=True)
392
+ return results_with_dot[0][0]
393
+
394
+ # Otherwise return highest confidence
395
+ all_results.sort(key=lambda x: x[1], reverse=True)
396
+ return all_results[0][0]
397
+
398
+
399
+ def validate_and_correct_axis_values(labels, is_y_axis=False):
400
+ """
401
+ Validate axis values and correct obvious OCR errors using linear interpolation.
402
+
403
+ Common OCR errors:
404
+ - Missing decimal point: 45 instead of 4.5
405
+ - Wrong digit: 20 instead of 2.0
406
+
407
+ Strategy: If values form a roughly linear sequence, detect outliers.
408
+ """
409
+ if len(labels) < 3:
410
+ return labels
411
+
412
+ # Extract values and positions
413
+ values = [l['value'] for l in labels]
414
+ if is_y_axis:
415
+ positions = [(l['bbox'][1] + l['bbox'][3]) / 2 for l in labels] # y center
416
+ else:
417
+ positions = [(l['bbox'][0] + l['bbox'][2]) / 2 for l in labels] # x center
418
+
419
+ # Filter valid values for analysis
420
+ valid_indices = [i for i, v in enumerate(values) if v is not None]
421
+ if len(valid_indices) < 3:
422
+ return labels
423
+
424
+ valid_values = [values[i] for i in valid_indices]
425
+ valid_positions = [positions[i] for i in valid_indices]
426
+
427
+ # Check if values form a linear sequence
428
+ # Calculate expected step sizes
429
+ diffs = [valid_values[i+1] - valid_values[i] for i in range(len(valid_values)-1)]
430
+ pos_diffs = [valid_positions[i+1] - valid_positions[i] for i in range(len(valid_positions)-1)]
431
+
432
+ # Check for consistent step (allowing some tolerance)
433
+ if len(diffs) >= 2:
434
+ median_diff = sorted(diffs)[len(diffs)//2]
435
+
436
+ # If most diffs are similar, we have a linear scale
437
+ consistent_count = sum(1 for d in diffs if abs(d - median_diff) < abs(median_diff) * 0.3)
438
+
439
+ if consistent_count >= len(diffs) * 0.6:
440
+ # Linear scale detected - check for outliers
441
+ scale = median_diff / (sum(pos_diffs) / len(pos_diffs)) if sum(pos_diffs) != 0 else 0
442
+
443
+ # Detect values that are likely wrong (off by factor of 10)
444
+ for i in range(1, len(valid_indices) - 1):
445
+ idx = valid_indices[i]
446
+ prev_idx = valid_indices[i-1]
447
+ next_idx = valid_indices[i+1]
448
+
449
+ expected = (values[prev_idx] + values[next_idx]) / 2
450
+ actual = values[idx]
451
+
452
+ if actual is not None and expected != 0:
453
+ ratio = actual / expected
454
+ # Check if off by factor of 10
455
+ if 8 < ratio < 12: # ~10x too high
456
+ labels[idx]['value'] = actual / 10
457
+ labels[idx]['corrected'] = True
458
+ elif 0.08 < ratio < 0.12: # ~10x too low
459
+ labels[idx]['value'] = actual * 10
460
+ labels[idx]['corrected'] = True
461
+
462
+ return labels
463
+
464
+
465
+ def ocr_labels(img, detections, label_type='both'):
466
+ """
467
+ Run OCR on detected xlabel and ylabel regions.
468
+
469
+ Args:
470
+ img: Image path or BGR numpy array
471
+ detections: Detection results from detect_chart_elements()
472
+ label_type: 'xlabel', 'ylabel', or 'both'
473
+
474
+ Returns:
475
+ dict with 'xlabels' and/or 'ylabels', each containing
476
+ list of {'bbox': [x1,y1,x2,y2], 'text': str, 'value': float}
477
+ """
478
+ if isinstance(img, str):
479
+ img = cv2.imread(img)
480
+
481
+ results = {}
482
+
483
+ if label_type in ['xlabel', 'both'] and 'xlabel' in detections:
484
+ xlabels = []
485
+ for det in detections['xlabel']:
486
+ bbox = det[:4]
487
+ text = ocr_region(img, bbox)
488
+ value = parse_numeric_value(text)
489
+ xlabels.append({
490
+ 'bbox': bbox,
491
+ 'text': text,
492
+ 'value': value,
493
+ 'confidence': det[4]
494
+ })
495
+ # Sort by x position (left to right)
496
+ xlabels.sort(key=lambda x: x['bbox'][0])
497
+ # Validate and correct
498
+ xlabels = validate_and_correct_axis_values(xlabels, is_y_axis=False)
499
+ results['xlabels'] = xlabels
500
+
501
+ if label_type in ['ylabel', 'both'] and 'ylabel' in detections:
502
+ ylabels = []
503
+ for det in detections['ylabel']:
504
+ bbox = det[:4]
505
+ text = ocr_region(img, bbox)
506
+ value = parse_numeric_value(text)
507
+ ylabels.append({
508
+ 'bbox': bbox,
509
+ 'text': text,
510
+ 'value': value,
511
+ 'confidence': det[4]
512
+ })
513
+ # Sort by y position (top to bottom)
514
+ ylabels.sort(key=lambda x: x['bbox'][1])
515
+ # Validate and correct
516
+ ylabels = validate_and_correct_axis_values(ylabels, is_y_axis=True)
517
+ results['ylabels'] = ylabels
518
+
519
+ return results
520
+
521
+
522
+ def get_axis_calibration(img, detections):
523
+ """
524
+ Extract axis calibration data for WebPlotDigitizer/starry-digitizer format.
525
+
526
+ Returns:
527
+ dict with:
528
+ - x1_pixel, x1_value: First x calibration point
529
+ - x2_pixel, x2_value: Second x calibration point
530
+ - y1_pixel, y1_value: First y calibration point
531
+ - y2_pixel, y2_value: Second y calibration point
532
+ Or None if calibration cannot be determined
533
+ """
534
+ # Get OCR results for labels
535
+ ocr_results = ocr_labels(img, detections, label_type='both')
536
+
537
+ calibration = {}
538
+
539
+ # Process x-axis labels
540
+ if 'xlabels' in ocr_results:
541
+ xlabels = [l for l in ocr_results['xlabels'] if l['value'] is not None]
542
+ if len(xlabels) >= 2:
543
+ # Use first and last labels with valid values
544
+ x1_label = xlabels[0]
545
+ x2_label = xlabels[-1]
546
+
547
+ # Use center of bbox for pixel position
548
+ calibration['x1_pixel'] = (x1_label['bbox'][0] + x1_label['bbox'][2]) / 2
549
+ calibration['x1_value'] = x1_label['value']
550
+ calibration['x2_pixel'] = (x2_label['bbox'][0] + x2_label['bbox'][2]) / 2
551
+ calibration['x2_value'] = x2_label['value']
552
+
553
+ # Process y-axis labels
554
+ if 'ylabels' in ocr_results:
555
+ ylabels = [l for l in ocr_results['ylabels'] if l['value'] is not None]
556
+ if len(ylabels) >= 2:
557
+ # Use first (top) and last (bottom) labels
558
+ y1_label = ylabels[0] # Top (usually higher value)
559
+ y2_label = ylabels[-1] # Bottom (usually lower value)
560
+
561
+ # Use center of bbox for pixel position
562
+ calibration['y1_pixel'] = (y1_label['bbox'][1] + y1_label['bbox'][3]) / 2
563
+ calibration['y1_value'] = y1_label['value']
564
+ calibration['y2_pixel'] = (y2_label['bbox'][1] + y2_label['bbox'][3]) / 2
565
+ calibration['y2_value'] = y2_label['value']
566
+
567
+ return calibration if calibration else None
568
+
569
+
570
+ def get_axis_info(detections, img=None, with_ocr=False):
571
+ """
572
+ Extract axis information from detections.
573
+
574
+ Args:
575
+ detections: Detection results from detect_chart_elements()
576
+ img: Image (required if with_ocr=True)
577
+ with_ocr: If True, run OCR on labels to get text values
578
+
579
+ Returns:
580
+ dict with plot_area, x_axis, y_axis bounding boxes
581
+ If with_ocr=True, also includes 'ocr_results' and 'calibration'
582
+ """
583
+ info = {}
584
+
585
+ if 'plot_area' in detections and len(detections['plot_area']) > 0:
586
+ # Get highest confidence plot area
587
+ plot_areas = sorted(detections['plot_area'], key=lambda x: x[4], reverse=True)
588
+ info['plot_area'] = plot_areas[0][:4] # x1, y1, x2, y2
589
+
590
+ if 'x_axis_area' in detections and len(detections['x_axis_area']) > 0:
591
+ x_axes = sorted(detections['x_axis_area'], key=lambda x: x[4], reverse=True)
592
+ info['x_axis_area'] = x_axes[0][:4]
593
+
594
+ if 'y_axis_area' in detections and len(detections['y_axis_area']) > 0:
595
+ y_axes = sorted(detections['y_axis_area'], key=lambda x: x[4], reverse=True)
596
+ info['y_axis_area'] = y_axes[0][:4]
597
+
598
+ # Get tick positions
599
+ if 'x_tick' in detections:
600
+ info['x_ticks'] = [d[:4] for d in detections['x_tick']]
601
+
602
+ if 'y_tick' in detections:
603
+ info['y_ticks'] = [d[:4] for d in detections['y_tick']]
604
+
605
+ # Get labels (bbox only)
606
+ if 'xlabel' in detections:
607
+ info['xlabels'] = detections['xlabel']
608
+
609
+ if 'ylabel' in detections:
610
+ info['ylabels'] = detections['ylabel']
611
+
612
+ # OCR if requested
613
+ if with_ocr and img is not None:
614
+ info['ocr_results'] = ocr_labels(img, detections)
615
+ info['calibration'] = get_axis_calibration(img, detections)
616
+
617
+ return info
618
+
619
+
620
+ def visualize_detections(img, detections, output_path=None):
621
+ """
622
+ Visualize detected chart elements.
623
+
624
+ Args:
625
+ img: Image path or numpy array (BGR)
626
+ detections: Detection results from detect_chart_elements()
627
+ output_path: Optional path to save visualization
628
+
629
+ Returns:
630
+ Annotated image (BGR numpy array)
631
+ """
632
+ if isinstance(img, str):
633
+ img = cv2.imread(img)
634
+ else:
635
+ img = img.copy()
636
+
637
+ # Color map for different classes
638
+ colors = {
639
+ 'plot_area': (0, 255, 0), # Green
640
+ 'x_axis_area': (255, 0, 0), # Blue
641
+ 'y_axis_area': (0, 0, 255), # Red
642
+ 'x_tick': (255, 255, 0), # Cyan
643
+ 'y_tick': (0, 255, 255), # Yellow
644
+ 'xlabel': (255, 0, 255), # Magenta
645
+ 'ylabel': (128, 0, 255), # Purple
646
+ 'x_title': (0, 128, 255), # Orange
647
+ 'y_title': (255, 128, 0), # Light blue
648
+ 'chart_title': (128, 255, 0), # Lime
649
+ 'legend_area': (128, 128, 128),# Gray
650
+ }
651
+
652
+ for class_name, boxes in detections.items():
653
+ color = colors.get(class_name, (200, 200, 200))
654
+ for box in boxes:
655
+ x1, y1, x2, y2, score = box[:5]
656
+ cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), color, 2)
657
+ label = f"{class_name}: {score:.2f}"
658
+ cv2.putText(img, label, (int(x1), int(y1) - 5),
659
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1)
660
+
661
+ if output_path:
662
+ cv2.imwrite(output_path, img)
663
+
664
+ return img
665
+
666
+
667
+ if __name__ == '__main__':
668
+ # Test
669
+ import argparse
670
+ parser = argparse.ArgumentParser()
671
+ parser.add_argument('--image', type=str, required=True)
672
+ parser.add_argument('--output', type=str, default='chartdete_result.png')
673
+ parser.add_argument('--ocr', action='store_true', help='Run OCR on labels')
674
+ args = parser.parse_args()
675
+
676
+ print("Loading model...")
677
+ load_chartdete_model(device='cpu')
678
+ print("Model loaded!")
679
+
680
+ print(f"Processing {args.image}...")
681
+ detections = detect_chart_elements(args.image, score_thr=0.3)
682
+
683
+ print("Detected elements:")
684
+ for class_name, boxes in detections.items():
685
+ print(f" {class_name}: {len(boxes)}")
686
+
687
+ # Load image for OCR
688
+ img = cv2.imread(args.image)
689
+
690
+ axis_info = get_axis_info(detections, img=img, with_ocr=args.ocr)
691
+ print("\nAxis info:")
692
+ for key, value in axis_info.items():
693
+ if key == 'ocr_results':
694
+ print(f" OCR Results:")
695
+ for label_type, labels in value.items():
696
+ print(f" {label_type}:")
697
+ for label in labels:
698
+ print(f" text='{label['text']}' value={label['value']} bbox={label['bbox'][:2]}")
699
+ elif key == 'calibration':
700
+ print(f" Calibration:")
701
+ if value:
702
+ for k, v in value.items():
703
+ print(f" {k}: {v}")
704
+ else:
705
+ print(" Could not determine calibration")
706
+ else:
707
+ print(f" {key}: {value}")
708
+
709
+ print(f"\nSaving visualization to {args.output}...")
710
+ visualize_detections(args.image, detections, args.output)
711
+ print("Done!")
submodules/chartdete/mmdet/__init__.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import mmcv
3
+
4
+ from .version import __version__, short_version
5
+
6
+
7
+ def digit_version(version_str):
8
+ digit_version = []
9
+ for x in version_str.split('.'):
10
+ if x.isdigit():
11
+ digit_version.append(int(x))
12
+ elif x.find('rc') != -1:
13
+ patch_version = x.split('rc')
14
+ digit_version.append(int(patch_version[0]) - 1)
15
+ digit_version.append(int(patch_version[1]))
16
+ return digit_version
17
+
18
+
19
+ mmcv_minimum_version = '1.3.17'
20
+ mmcv_maximum_version = '1.8.0'
21
+ mmcv_version = digit_version(mmcv.__version__)
22
+
23
+
24
+ assert (mmcv_version >= digit_version(mmcv_minimum_version)
25
+ and mmcv_version <= digit_version(mmcv_maximum_version)), \
26
+ f'MMCV=={mmcv.__version__} is used but incompatible. ' \
27
+ f'Please install mmcv>={mmcv_minimum_version}, <={mmcv_maximum_version}.'
28
+
29
+ __all__ = ['__version__', 'short_version']
submodules/chartdete/mmdet/apis/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from .inference import (async_inference_detector, inference_detector,
3
+ init_detector, show_result_pyplot)
4
+ from .test import multi_gpu_test, single_gpu_test
5
+ from .train import (get_root_logger, init_random_seed, set_random_seed,
6
+ train_detector)
7
+
8
+ __all__ = [
9
+ 'get_root_logger', 'set_random_seed', 'train_detector', 'init_detector',
10
+ 'async_inference_detector', 'inference_detector', 'show_result_pyplot',
11
+ 'multi_gpu_test', 'single_gpu_test', 'init_random_seed'
12
+ ]
submodules/chartdete/mmdet/apis/inference.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import warnings
3
+ from pathlib import Path
4
+
5
+ import mmcv
6
+ import numpy as np
7
+ import torch
8
+ from mmcv.ops import RoIPool
9
+ from mmcv.parallel import collate, scatter
10
+ from mmcv.runner import load_checkpoint
11
+
12
+ from mmdet.core import get_classes
13
+ from mmdet.datasets import replace_ImageToTensor
14
+ from mmdet.datasets.pipelines import Compose
15
+ from mmdet.models import build_detector
16
+
17
+
18
+ def init_detector(config, checkpoint=None, device='cuda:0', cfg_options=None):
19
+ """Initialize a detector from config file.
20
+
21
+ Args:
22
+ config (str, :obj:`Path`, or :obj:`mmcv.Config`): Config file path,
23
+ :obj:`Path`, or the config object.
24
+ checkpoint (str, optional): Checkpoint path. If left as None, the model
25
+ will not load any weights.
26
+ cfg_options (dict): Options to override some settings in the used
27
+ config.
28
+
29
+ Returns:
30
+ nn.Module: The constructed detector.
31
+ """
32
+ if isinstance(config, (str, Path)):
33
+ config = mmcv.Config.fromfile(config)
34
+ elif not isinstance(config, mmcv.Config):
35
+ raise TypeError('config must be a filename or Config object, '
36
+ f'but got {type(config)}')
37
+ if cfg_options is not None:
38
+ config.merge_from_dict(cfg_options)
39
+ if 'pretrained' in config.model:
40
+ config.model.pretrained = None
41
+ elif 'init_cfg' in config.model.backbone:
42
+ config.model.backbone.init_cfg = None
43
+ config.model.train_cfg = None
44
+ model = build_detector(config.model, test_cfg=config.get('test_cfg'))
45
+ if checkpoint is not None:
46
+ checkpoint = load_checkpoint(model, checkpoint, map_location='cpu')
47
+ if 'CLASSES' in checkpoint.get('meta', {}):
48
+ model.CLASSES = checkpoint['meta']['CLASSES']
49
+ else:
50
+ warnings.simplefilter('once')
51
+ warnings.warn('Class names are not saved in the checkpoint\'s '
52
+ 'meta data, use COCO classes by default.')
53
+ model.CLASSES = get_classes('coco')
54
+ model.cfg = config # save the config in the model for convenience
55
+ model.to(device)
56
+ model.eval()
57
+
58
+ if device == 'npu':
59
+ from mmcv.device.npu import NPUDataParallel
60
+ model = NPUDataParallel(model)
61
+ model.cfg = config
62
+
63
+ return model
64
+
65
+
66
+ class LoadImage:
67
+ """Deprecated.
68
+
69
+ A simple pipeline to load image.
70
+ """
71
+
72
+ def __call__(self, results):
73
+ """Call function to load images into results.
74
+
75
+ Args:
76
+ results (dict): A result dict contains the file name
77
+ of the image to be read.
78
+ Returns:
79
+ dict: ``results`` will be returned containing loaded image.
80
+ """
81
+ warnings.simplefilter('once')
82
+ warnings.warn('`LoadImage` is deprecated and will be removed in '
83
+ 'future releases. You may use `LoadImageFromWebcam` '
84
+ 'from `mmdet.datasets.pipelines.` instead.')
85
+ if isinstance(results['img'], str):
86
+ results['filename'] = results['img']
87
+ results['ori_filename'] = results['img']
88
+ else:
89
+ results['filename'] = None
90
+ results['ori_filename'] = None
91
+ img = mmcv.imread(results['img'])
92
+ results['img'] = img
93
+ results['img_fields'] = ['img']
94
+ results['img_shape'] = img.shape
95
+ results['ori_shape'] = img.shape
96
+ return results
97
+
98
+
99
+ def inference_detector(model, imgs):
100
+ """Inference image(s) with the detector.
101
+
102
+ Args:
103
+ model (nn.Module): The loaded detector.
104
+ imgs (str/ndarray or list[str/ndarray] or tuple[str/ndarray]):
105
+ Either image files or loaded images.
106
+
107
+ Returns:
108
+ If imgs is a list or tuple, the same length list type results
109
+ will be returned, otherwise return the detection results directly.
110
+ """
111
+
112
+ if isinstance(imgs, (list, tuple)):
113
+ is_batch = True
114
+ else:
115
+ imgs = [imgs]
116
+ is_batch = False
117
+
118
+ cfg = model.cfg
119
+ device = next(model.parameters()).device # model device
120
+
121
+ if isinstance(imgs[0], np.ndarray):
122
+ cfg = cfg.copy()
123
+ # set loading pipeline type
124
+ cfg.data.test.pipeline[0].type = 'LoadImageFromWebcam'
125
+
126
+ cfg.data.test.pipeline = replace_ImageToTensor(cfg.data.test.pipeline)
127
+ test_pipeline = Compose(cfg.data.test.pipeline)
128
+
129
+ datas = []
130
+ for img in imgs:
131
+ # prepare data
132
+ if isinstance(img, np.ndarray):
133
+ # directly add img
134
+ data = dict(img=img)
135
+ else:
136
+ # add information into dict
137
+ data = dict(img_info=dict(filename=img), img_prefix=None)
138
+ # build the data pipeline
139
+ data = test_pipeline(data)
140
+ datas.append(data)
141
+
142
+ data = collate(datas, samples_per_gpu=len(imgs))
143
+ # just get the actual data from DataContainer
144
+ data['img_metas'] = [img_metas.data[0] for img_metas in data['img_metas']]
145
+ data['img'] = [img.data[0] for img in data['img']]
146
+ if next(model.parameters()).is_cuda:
147
+ # scatter to specified GPU
148
+ data = scatter(data, [device])[0]
149
+ else:
150
+ for m in model.modules():
151
+ assert not isinstance(
152
+ m, RoIPool
153
+ ), 'CPU inference with RoIPool is not supported currently.'
154
+
155
+ # forward the model
156
+ with torch.no_grad():
157
+ results = model(return_loss=False, rescale=True, **data)
158
+
159
+ if not is_batch:
160
+ return results[0]
161
+ else:
162
+ return results
163
+
164
+
165
+ async def async_inference_detector(model, imgs):
166
+ """Async inference image(s) with the detector.
167
+
168
+ Args:
169
+ model (nn.Module): The loaded detector.
170
+ img (str | ndarray): Either image files or loaded images.
171
+
172
+ Returns:
173
+ Awaitable detection results.
174
+ """
175
+ if not isinstance(imgs, (list, tuple)):
176
+ imgs = [imgs]
177
+
178
+ cfg = model.cfg
179
+ device = next(model.parameters()).device # model device
180
+
181
+ if isinstance(imgs[0], np.ndarray):
182
+ cfg = cfg.copy()
183
+ # set loading pipeline type
184
+ cfg.data.test.pipeline[0].type = 'LoadImageFromWebcam'
185
+
186
+ cfg.data.test.pipeline = replace_ImageToTensor(cfg.data.test.pipeline)
187
+ test_pipeline = Compose(cfg.data.test.pipeline)
188
+
189
+ datas = []
190
+ for img in imgs:
191
+ # prepare data
192
+ if isinstance(img, np.ndarray):
193
+ # directly add img
194
+ data = dict(img=img)
195
+ else:
196
+ # add information into dict
197
+ data = dict(img_info=dict(filename=img), img_prefix=None)
198
+ # build the data pipeline
199
+ data = test_pipeline(data)
200
+ datas.append(data)
201
+
202
+ data = collate(datas, samples_per_gpu=len(imgs))
203
+ # just get the actual data from DataContainer
204
+ data['img_metas'] = [img_metas.data[0] for img_metas in data['img_metas']]
205
+ data['img'] = [img.data[0] for img in data['img']]
206
+ if next(model.parameters()).is_cuda:
207
+ # scatter to specified GPU
208
+ data = scatter(data, [device])[0]
209
+ else:
210
+ for m in model.modules():
211
+ assert not isinstance(
212
+ m, RoIPool
213
+ ), 'CPU inference with RoIPool is not supported currently.'
214
+
215
+ # We don't restore `torch.is_grad_enabled()` value during concurrent
216
+ # inference since execution can overlap
217
+ torch.set_grad_enabled(False)
218
+ results = await model.aforward_test(rescale=True, **data)
219
+ return results
220
+
221
+
222
+ def show_result_pyplot(model,
223
+ img,
224
+ result,
225
+ score_thr=0.3,
226
+ title='result',
227
+ wait_time=0,
228
+ palette=None,
229
+ out_file=None):
230
+ """Visualize the detection results on the image.
231
+
232
+ Args:
233
+ model (nn.Module): The loaded detector.
234
+ img (str or np.ndarray): Image filename or loaded image.
235
+ result (tuple[list] or list): The detection result, can be either
236
+ (bbox, segm) or just bbox.
237
+ score_thr (float): The threshold to visualize the bboxes and masks.
238
+ title (str): Title of the pyplot figure.
239
+ wait_time (float): Value of waitKey param. Default: 0.
240
+ palette (str or tuple(int) or :obj:`Color`): Color.
241
+ The tuple of color should be in BGR order.
242
+ out_file (str or None): The path to write the image.
243
+ Default: None.
244
+ """
245
+ if hasattr(model, 'module'):
246
+ model = model.module
247
+ model.show_result(
248
+ img,
249
+ result,
250
+ score_thr=score_thr,
251
+ show=True,
252
+ wait_time=wait_time,
253
+ win_name=title,
254
+ bbox_color=palette,
255
+ text_color=(200, 200, 200),
256
+ mask_color=palette,
257
+ out_file=out_file)
submodules/chartdete/mmdet/apis/test.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import os.path as osp
3
+ import pickle
4
+ import shutil
5
+ import tempfile
6
+ import time
7
+
8
+ import mmcv
9
+ import torch
10
+ import torch.distributed as dist
11
+ from mmcv.image import tensor2imgs
12
+ from mmcv.runner import get_dist_info
13
+
14
+ from mmdet.core import encode_mask_results
15
+
16
+
17
+ def single_gpu_test(model,
18
+ data_loader,
19
+ show=False,
20
+ out_dir=None,
21
+ show_score_thr=0.3):
22
+ model.eval()
23
+ results = []
24
+ dataset = data_loader.dataset
25
+ PALETTE = getattr(dataset, 'PALETTE', None)
26
+ prog_bar = mmcv.ProgressBar(len(dataset))
27
+ for i, data in enumerate(data_loader):
28
+ with torch.no_grad():
29
+ result = model(return_loss=False, rescale=True, **data)
30
+
31
+ batch_size = len(result)
32
+ if show or out_dir:
33
+ if batch_size == 1 and isinstance(data['img'][0], torch.Tensor):
34
+ img_tensor = data['img'][0]
35
+ else:
36
+ img_tensor = data['img'][0].data[0]
37
+ img_metas = data['img_metas'][0].data[0]
38
+ imgs = tensor2imgs(img_tensor, **img_metas[0]['img_norm_cfg'])
39
+ assert len(imgs) == len(img_metas)
40
+
41
+ for i, (img, img_meta) in enumerate(zip(imgs, img_metas)):
42
+ h, w, _ = img_meta['img_shape']
43
+ img_show = img[:h, :w, :]
44
+
45
+ ori_h, ori_w = img_meta['ori_shape'][:-1]
46
+ img_show = mmcv.imresize(img_show, (ori_w, ori_h))
47
+
48
+ if out_dir:
49
+ out_file = osp.join(out_dir, img_meta['ori_filename'])
50
+ else:
51
+ out_file = None
52
+
53
+ model.module.show_result(
54
+ img_show,
55
+ result[i],
56
+ bbox_color=PALETTE,
57
+ text_color=PALETTE,
58
+ mask_color=PALETTE,
59
+ show=show,
60
+ out_file=out_file,
61
+ score_thr=show_score_thr)
62
+
63
+ # encode mask results
64
+ if isinstance(result[0], tuple):
65
+ result = [(bbox_results, encode_mask_results(mask_results))
66
+ for bbox_results, mask_results in result]
67
+ # This logic is only used in panoptic segmentation test.
68
+ elif isinstance(result[0], dict) and 'ins_results' in result[0]:
69
+ for j in range(len(result)):
70
+ bbox_results, mask_results = result[j]['ins_results']
71
+ result[j]['ins_results'] = (bbox_results,
72
+ encode_mask_results(mask_results))
73
+
74
+ results.extend(result)
75
+
76
+ for _ in range(batch_size):
77
+ prog_bar.update()
78
+ return results
79
+
80
+
81
+ def multi_gpu_test(model, data_loader, tmpdir=None, gpu_collect=False):
82
+ """Test model with multiple gpus.
83
+
84
+ This method tests model with multiple gpus and collects the results
85
+ under two different modes: gpu and cpu modes. By setting 'gpu_collect=True'
86
+ it encodes results to gpu tensors and use gpu communication for results
87
+ collection. On cpu mode it saves the results on different gpus to 'tmpdir'
88
+ and collects them by the rank 0 worker.
89
+
90
+ Args:
91
+ model (nn.Module): Model to be tested.
92
+ data_loader (nn.Dataloader): Pytorch data loader.
93
+ tmpdir (str): Path of directory to save the temporary results from
94
+ different gpus under cpu mode.
95
+ gpu_collect (bool): Option to use either gpu or cpu to collect results.
96
+
97
+ Returns:
98
+ list: The prediction results.
99
+ """
100
+ model.eval()
101
+ results = []
102
+ dataset = data_loader.dataset
103
+ rank, world_size = get_dist_info()
104
+ if rank == 0:
105
+ prog_bar = mmcv.ProgressBar(len(dataset))
106
+ time.sleep(2) # This line can prevent deadlock problem in some cases.
107
+ for i, data in enumerate(data_loader):
108
+ with torch.no_grad():
109
+ result = model(return_loss=False, rescale=True, **data)
110
+ # encode mask results
111
+ if isinstance(result[0], tuple):
112
+ result = [(bbox_results, encode_mask_results(mask_results))
113
+ for bbox_results, mask_results in result]
114
+ # This logic is only used in panoptic segmentation test.
115
+ elif isinstance(result[0], dict) and 'ins_results' in result[0]:
116
+ for j in range(len(result)):
117
+ bbox_results, mask_results = result[j]['ins_results']
118
+ result[j]['ins_results'] = (
119
+ bbox_results, encode_mask_results(mask_results))
120
+
121
+ results.extend(result)
122
+
123
+ if rank == 0:
124
+ batch_size = len(result)
125
+ for _ in range(batch_size * world_size):
126
+ prog_bar.update()
127
+
128
+ # collect results from all ranks
129
+ if gpu_collect:
130
+ results = collect_results_gpu(results, len(dataset))
131
+ else:
132
+ results = collect_results_cpu(results, len(dataset), tmpdir)
133
+ return results
134
+
135
+
136
+ def collect_results_cpu(result_part, size, tmpdir=None):
137
+ rank, world_size = get_dist_info()
138
+ # create a tmp dir if it is not specified
139
+ if tmpdir is None:
140
+ MAX_LEN = 512
141
+ # 32 is whitespace
142
+ dir_tensor = torch.full((MAX_LEN, ),
143
+ 32,
144
+ dtype=torch.uint8,
145
+ device='cuda')
146
+ if rank == 0:
147
+ mmcv.mkdir_or_exist('.dist_test')
148
+ tmpdir = tempfile.mkdtemp(dir='.dist_test')
149
+ tmpdir = torch.tensor(
150
+ bytearray(tmpdir.encode()), dtype=torch.uint8, device='cuda')
151
+ dir_tensor[:len(tmpdir)] = tmpdir
152
+ dist.broadcast(dir_tensor, 0)
153
+ tmpdir = dir_tensor.cpu().numpy().tobytes().decode().rstrip()
154
+ else:
155
+ mmcv.mkdir_or_exist(tmpdir)
156
+ # dump the part result to the dir
157
+ mmcv.dump(result_part, osp.join(tmpdir, f'part_{rank}.pkl'))
158
+ dist.barrier()
159
+ # collect all parts
160
+ if rank != 0:
161
+ return None
162
+ else:
163
+ # load results of all parts from tmp dir
164
+ part_list = []
165
+ for i in range(world_size):
166
+ part_file = osp.join(tmpdir, f'part_{i}.pkl')
167
+ part_list.append(mmcv.load(part_file))
168
+ # sort the results
169
+ ordered_results = []
170
+ for res in zip(*part_list):
171
+ ordered_results.extend(list(res))
172
+ # the dataloader may pad some samples
173
+ ordered_results = ordered_results[:size]
174
+ # remove tmp dir
175
+ shutil.rmtree(tmpdir)
176
+ return ordered_results
177
+
178
+
179
+ def collect_results_gpu(result_part, size):
180
+ rank, world_size = get_dist_info()
181
+ # dump result part to tensor with pickle
182
+ part_tensor = torch.tensor(
183
+ bytearray(pickle.dumps(result_part)), dtype=torch.uint8, device='cuda')
184
+ # gather all result part tensor shape
185
+ shape_tensor = torch.tensor(part_tensor.shape, device='cuda')
186
+ shape_list = [shape_tensor.clone() for _ in range(world_size)]
187
+ dist.all_gather(shape_list, shape_tensor)
188
+ # padding result part tensor to max length
189
+ shape_max = torch.tensor(shape_list).max()
190
+ part_send = torch.zeros(shape_max, dtype=torch.uint8, device='cuda')
191
+ part_send[:shape_tensor[0]] = part_tensor
192
+ part_recv_list = [
193
+ part_tensor.new_zeros(shape_max) for _ in range(world_size)
194
+ ]
195
+ # gather all result part
196
+ dist.all_gather(part_recv_list, part_send)
197
+
198
+ if rank == 0:
199
+ part_list = []
200
+ for recv, shape in zip(part_recv_list, shape_list):
201
+ part_list.append(
202
+ pickle.loads(recv[:shape[0]].cpu().numpy().tobytes()))
203
+ # sort the results
204
+ ordered_results = []
205
+ for res in zip(*part_list):
206
+ ordered_results.extend(list(res))
207
+ # the dataloader may pad some samples
208
+ ordered_results = ordered_results[:size]
209
+ return ordered_results
submodules/chartdete/mmdet/apis/train.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import os
3
+ import random
4
+
5
+ import numpy as np
6
+ import torch
7
+ import torch.distributed as dist
8
+ from mmcv.runner import (DistSamplerSeedHook, EpochBasedRunner,
9
+ Fp16OptimizerHook, OptimizerHook, build_runner,
10
+ get_dist_info)
11
+
12
+ from mmdet.core import DistEvalHook, EvalHook, build_optimizer
13
+ from mmdet.datasets import (build_dataloader, build_dataset,
14
+ replace_ImageToTensor)
15
+ from mmdet.utils import (build_ddp, build_dp, compat_cfg,
16
+ find_latest_checkpoint, get_root_logger)
17
+
18
+
19
+ def init_random_seed(seed=None, device='cuda'):
20
+ """Initialize random seed.
21
+
22
+ If the seed is not set, the seed will be automatically randomized,
23
+ and then broadcast to all processes to prevent some potential bugs.
24
+
25
+ Args:
26
+ seed (int, Optional): The seed. Default to None.
27
+ device (str): The device where the seed will be put on.
28
+ Default to 'cuda'.
29
+
30
+ Returns:
31
+ int: Seed to be used.
32
+ """
33
+ if seed is not None:
34
+ return seed
35
+
36
+ # Make sure all ranks share the same random seed to prevent
37
+ # some potential bugs. Please refer to
38
+ # https://github.com/open-mmlab/mmdetection/issues/6339
39
+ rank, world_size = get_dist_info()
40
+ seed = np.random.randint(2**31)
41
+ if world_size == 1:
42
+ return seed
43
+
44
+ if rank == 0:
45
+ random_num = torch.tensor(seed, dtype=torch.int32, device=device)
46
+ else:
47
+ random_num = torch.tensor(0, dtype=torch.int32, device=device)
48
+ dist.broadcast(random_num, src=0)
49
+ return random_num.item()
50
+
51
+
52
+ def set_random_seed(seed, deterministic=False):
53
+ """Set random seed.
54
+
55
+ Args:
56
+ seed (int): Seed to be used.
57
+ deterministic (bool): Whether to set the deterministic option for
58
+ CUDNN backend, i.e., set `torch.backends.cudnn.deterministic`
59
+ to True and `torch.backends.cudnn.benchmark` to False.
60
+ Default: False.
61
+ """
62
+ random.seed(seed)
63
+ np.random.seed(seed)
64
+ torch.manual_seed(seed)
65
+ torch.cuda.manual_seed_all(seed)
66
+ if deterministic:
67
+ torch.backends.cudnn.deterministic = True
68
+ torch.backends.cudnn.benchmark = False
69
+
70
+
71
+ def auto_scale_lr(cfg, distributed, logger):
72
+ """Automatically scaling LR according to GPU number and sample per GPU.
73
+
74
+ Args:
75
+ cfg (config): Training config.
76
+ distributed (bool): Using distributed or not.
77
+ logger (logging.Logger): Logger.
78
+ """
79
+ # Get flag from config
80
+ if ('auto_scale_lr' not in cfg) or \
81
+ (not cfg.auto_scale_lr.get('enable', False)):
82
+ logger.info('Automatic scaling of learning rate (LR)'
83
+ ' has been disabled.')
84
+ return
85
+
86
+ # Get base batch size from config
87
+ base_batch_size = cfg.auto_scale_lr.get('base_batch_size', None)
88
+ if base_batch_size is None:
89
+ return
90
+
91
+ # Get gpu number
92
+ if distributed:
93
+ _, world_size = get_dist_info()
94
+ num_gpus = len(range(world_size))
95
+ else:
96
+ num_gpus = len(cfg.gpu_ids)
97
+
98
+ # calculate the batch size
99
+ samples_per_gpu = cfg.data.train_dataloader.samples_per_gpu
100
+ batch_size = num_gpus * samples_per_gpu
101
+ logger.info(f'Training with {num_gpus} GPU(s) with {samples_per_gpu} '
102
+ f'samples per GPU. The total batch size is {batch_size}.')
103
+
104
+ if batch_size != base_batch_size:
105
+ # scale LR with
106
+ # [linear scaling rule](https://arxiv.org/abs/1706.02677)
107
+ scaled_lr = (batch_size / base_batch_size) * cfg.optimizer.lr
108
+ logger.info('LR has been automatically scaled '
109
+ f'from {cfg.optimizer.lr} to {scaled_lr}')
110
+ cfg.optimizer.lr = scaled_lr
111
+ else:
112
+ logger.info('The batch size match the '
113
+ f'base batch size: {base_batch_size}, '
114
+ f'will not scaling the LR ({cfg.optimizer.lr}).')
115
+
116
+
117
+ def train_detector(model,
118
+ dataset,
119
+ cfg,
120
+ distributed=False,
121
+ validate=False,
122
+ timestamp=None,
123
+ meta=None):
124
+
125
+ cfg = compat_cfg(cfg)
126
+ logger = get_root_logger(log_level=cfg.log_level)
127
+
128
+ # prepare data loaders
129
+ dataset = dataset if isinstance(dataset, (list, tuple)) else [dataset]
130
+
131
+ runner_type = 'EpochBasedRunner' if 'runner' not in cfg else cfg.runner[
132
+ 'type']
133
+
134
+ train_dataloader_default_args = dict(
135
+ samples_per_gpu=2,
136
+ workers_per_gpu=2,
137
+ # `num_gpus` will be ignored if distributed
138
+ num_gpus=len(cfg.gpu_ids),
139
+ dist=distributed,
140
+ seed=cfg.seed,
141
+ runner_type=runner_type,
142
+ persistent_workers=False)
143
+
144
+ train_loader_cfg = {
145
+ **train_dataloader_default_args,
146
+ **cfg.data.get('train_dataloader', {})
147
+ }
148
+
149
+ data_loaders = [build_dataloader(ds, **train_loader_cfg) for ds in dataset]
150
+
151
+ # put model on gpus
152
+ if distributed:
153
+ find_unused_parameters = cfg.get('find_unused_parameters', False)
154
+ # Sets the `find_unused_parameters` parameter in
155
+ # torch.nn.parallel.DistributedDataParallel
156
+ model = build_ddp(
157
+ model,
158
+ cfg.device,
159
+ device_ids=[int(os.environ['LOCAL_RANK'])],
160
+ broadcast_buffers=False,
161
+ find_unused_parameters=find_unused_parameters)
162
+ else:
163
+ model = build_dp(model, cfg.device, device_ids=cfg.gpu_ids)
164
+
165
+ # build optimizer
166
+ auto_scale_lr(cfg, distributed, logger)
167
+ optimizer = build_optimizer(model, cfg.optimizer)
168
+
169
+ runner = build_runner(
170
+ cfg.runner,
171
+ default_args=dict(
172
+ model=model,
173
+ optimizer=optimizer,
174
+ work_dir=cfg.work_dir,
175
+ logger=logger,
176
+ meta=meta))
177
+
178
+ # an ugly workaround to make .log and .log.json filenames the same
179
+ runner.timestamp = timestamp
180
+
181
+ # fp16 setting
182
+ fp16_cfg = cfg.get('fp16', None)
183
+ if fp16_cfg is None and cfg.get('device', None) == 'npu':
184
+ fp16_cfg = dict(loss_scale='dynamic')
185
+ if fp16_cfg is not None:
186
+ optimizer_config = Fp16OptimizerHook(
187
+ **cfg.optimizer_config, **fp16_cfg, distributed=distributed)
188
+ elif distributed and 'type' not in cfg.optimizer_config:
189
+ optimizer_config = OptimizerHook(**cfg.optimizer_config)
190
+ else:
191
+ optimizer_config = cfg.optimizer_config
192
+
193
+ # register hooks
194
+ runner.register_training_hooks(
195
+ cfg.lr_config,
196
+ optimizer_config,
197
+ cfg.checkpoint_config,
198
+ cfg.log_config,
199
+ cfg.get('momentum_config', None),
200
+ custom_hooks_config=cfg.get('custom_hooks', None))
201
+
202
+ if distributed:
203
+ if isinstance(runner, EpochBasedRunner):
204
+ runner.register_hook(DistSamplerSeedHook())
205
+
206
+ # register eval hooks
207
+ if validate:
208
+ val_dataloader_default_args = dict(
209
+ samples_per_gpu=1,
210
+ workers_per_gpu=2,
211
+ dist=distributed,
212
+ shuffle=False,
213
+ persistent_workers=False)
214
+
215
+ val_dataloader_args = {
216
+ **val_dataloader_default_args,
217
+ **cfg.data.get('val_dataloader', {})
218
+ }
219
+ # Support batch_size > 1 in validation
220
+
221
+ if val_dataloader_args['samples_per_gpu'] > 1:
222
+ # Replace 'ImageToTensor' to 'DefaultFormatBundle'
223
+ cfg.data.val.pipeline = replace_ImageToTensor(
224
+ cfg.data.val.pipeline)
225
+ val_dataset = build_dataset(cfg.data.val, dict(test_mode=True))
226
+
227
+ val_dataloader = build_dataloader(val_dataset, **val_dataloader_args)
228
+ eval_cfg = cfg.get('evaluation', {})
229
+ eval_cfg['by_epoch'] = cfg.runner['type'] != 'IterBasedRunner'
230
+ eval_hook = DistEvalHook if distributed else EvalHook
231
+ # In this PR (https://github.com/open-mmlab/mmcv/pull/1193), the
232
+ # priority of IterTimerHook has been modified from 'NORMAL' to 'LOW'.
233
+ runner.register_hook(
234
+ eval_hook(val_dataloader, **eval_cfg), priority='LOW')
235
+
236
+ resume_from = None
237
+ if cfg.resume_from is None and cfg.get('auto_resume'):
238
+ resume_from = find_latest_checkpoint(cfg.work_dir)
239
+ if resume_from is not None:
240
+ cfg.resume_from = resume_from
241
+
242
+ if cfg.resume_from:
243
+ runner.resume(cfg.resume_from)
244
+ elif cfg.load_from:
245
+ runner.load_checkpoint(cfg.load_from)
246
+ runner.run(data_loaders, cfg.workflow)
submodules/chartdete/mmdet/core/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from .anchor import * # noqa: F401, F403
3
+ from .bbox import * # noqa: F401, F403
4
+ from .data_structures import * # noqa: F401, F403
5
+ from .evaluation import * # noqa: F401, F403
6
+ from .hook import * # noqa: F401, F403
7
+ from .mask import * # noqa: F401, F403
8
+ from .optimizers import * # noqa: F401, F403
9
+ from .post_processing import * # noqa: F401, F403
10
+ from .utils import * # noqa: F401, F403
submodules/chartdete/mmdet/core/anchor/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from .anchor_generator import (AnchorGenerator, LegacyAnchorGenerator,
3
+ YOLOAnchorGenerator)
4
+ from .builder import (ANCHOR_GENERATORS, PRIOR_GENERATORS,
5
+ build_anchor_generator, build_prior_generator)
6
+ from .point_generator import MlvlPointGenerator, PointGenerator
7
+ from .utils import anchor_inside_flags, calc_region, images_to_levels
8
+
9
+ __all__ = [
10
+ 'AnchorGenerator', 'LegacyAnchorGenerator', 'anchor_inside_flags',
11
+ 'PointGenerator', 'images_to_levels', 'calc_region',
12
+ 'build_anchor_generator', 'ANCHOR_GENERATORS', 'YOLOAnchorGenerator',
13
+ 'build_prior_generator', 'PRIOR_GENERATORS', 'MlvlPointGenerator'
14
+ ]
submodules/chartdete/mmdet/core/anchor/anchor_generator.py ADDED
@@ -0,0 +1,866 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import warnings
3
+
4
+ import mmcv
5
+ import numpy as np
6
+ import torch
7
+ from torch.nn.modules.utils import _pair
8
+
9
+ from .builder import PRIOR_GENERATORS
10
+
11
+
12
+ @PRIOR_GENERATORS.register_module()
13
+ class AnchorGenerator:
14
+ """Standard anchor generator for 2D anchor-based detectors.
15
+
16
+ Args:
17
+ strides (list[int] | list[tuple[int, int]]): Strides of anchors
18
+ in multiple feature levels in order (w, h).
19
+ ratios (list[float]): The list of ratios between the height and width
20
+ of anchors in a single level.
21
+ scales (list[int] | None): Anchor scales for anchors in a single level.
22
+ It cannot be set at the same time if `octave_base_scale` and
23
+ `scales_per_octave` are set.
24
+ base_sizes (list[int] | None): The basic sizes
25
+ of anchors in multiple levels.
26
+ If None is given, strides will be used as base_sizes.
27
+ (If strides are non square, the shortest stride is taken.)
28
+ scale_major (bool): Whether to multiply scales first when generating
29
+ base anchors. If true, the anchors in the same row will have the
30
+ same scales. By default it is True in V2.0
31
+ octave_base_scale (int): The base scale of octave.
32
+ scales_per_octave (int): Number of scales for each octave.
33
+ `octave_base_scale` and `scales_per_octave` are usually used in
34
+ retinanet and the `scales` should be None when they are set.
35
+ centers (list[tuple[float, float]] | None): The centers of the anchor
36
+ relative to the feature grid center in multiple feature levels.
37
+ By default it is set to be None and not used. If a list of tuple of
38
+ float is given, they will be used to shift the centers of anchors.
39
+ center_offset (float): The offset of center in proportion to anchors'
40
+ width and height. By default it is 0 in V2.0.
41
+
42
+ Examples:
43
+ >>> from mmdet.core import AnchorGenerator
44
+ >>> self = AnchorGenerator([16], [1.], [1.], [9])
45
+ >>> all_anchors = self.grid_priors([(2, 2)], device='cpu')
46
+ >>> print(all_anchors)
47
+ [tensor([[-4.5000, -4.5000, 4.5000, 4.5000],
48
+ [11.5000, -4.5000, 20.5000, 4.5000],
49
+ [-4.5000, 11.5000, 4.5000, 20.5000],
50
+ [11.5000, 11.5000, 20.5000, 20.5000]])]
51
+ >>> self = AnchorGenerator([16, 32], [1.], [1.], [9, 18])
52
+ >>> all_anchors = self.grid_priors([(2, 2), (1, 1)], device='cpu')
53
+ >>> print(all_anchors)
54
+ [tensor([[-4.5000, -4.5000, 4.5000, 4.5000],
55
+ [11.5000, -4.5000, 20.5000, 4.5000],
56
+ [-4.5000, 11.5000, 4.5000, 20.5000],
57
+ [11.5000, 11.5000, 20.5000, 20.5000]]), \
58
+ tensor([[-9., -9., 9., 9.]])]
59
+ """
60
+
61
+ def __init__(self,
62
+ strides,
63
+ ratios,
64
+ scales=None,
65
+ base_sizes=None,
66
+ scale_major=True,
67
+ octave_base_scale=None,
68
+ scales_per_octave=None,
69
+ centers=None,
70
+ center_offset=0.):
71
+ # check center and center_offset
72
+ if center_offset != 0:
73
+ assert centers is None, 'center cannot be set when center_offset' \
74
+ f'!=0, {centers} is given.'
75
+ if not (0 <= center_offset <= 1):
76
+ raise ValueError('center_offset should be in range [0, 1], '
77
+ f'{center_offset} is given.')
78
+ if centers is not None:
79
+ assert len(centers) == len(strides), \
80
+ 'The number of strides should be the same as centers, got ' \
81
+ f'{strides} and {centers}'
82
+
83
+ # calculate base sizes of anchors
84
+ self.strides = [_pair(stride) for stride in strides]
85
+ self.base_sizes = [min(stride) for stride in self.strides
86
+ ] if base_sizes is None else base_sizes
87
+ assert len(self.base_sizes) == len(self.strides), \
88
+ 'The number of strides should be the same as base sizes, got ' \
89
+ f'{self.strides} and {self.base_sizes}'
90
+
91
+ # calculate scales of anchors
92
+ assert ((octave_base_scale is not None
93
+ and scales_per_octave is not None) ^ (scales is not None)), \
94
+ 'scales and octave_base_scale with scales_per_octave cannot' \
95
+ ' be set at the same time'
96
+ if scales is not None:
97
+ self.scales = torch.Tensor(scales)
98
+ elif octave_base_scale is not None and scales_per_octave is not None:
99
+ octave_scales = np.array(
100
+ [2**(i / scales_per_octave) for i in range(scales_per_octave)])
101
+ scales = octave_scales * octave_base_scale
102
+ self.scales = torch.Tensor(scales)
103
+ else:
104
+ raise ValueError('Either scales or octave_base_scale with '
105
+ 'scales_per_octave should be set')
106
+
107
+ self.octave_base_scale = octave_base_scale
108
+ self.scales_per_octave = scales_per_octave
109
+ self.ratios = torch.Tensor(ratios)
110
+ self.scale_major = scale_major
111
+ self.centers = centers
112
+ self.center_offset = center_offset
113
+ self.base_anchors = self.gen_base_anchors()
114
+
115
+ @property
116
+ def num_base_anchors(self):
117
+ """list[int]: total number of base anchors in a feature grid"""
118
+ return self.num_base_priors
119
+
120
+ @property
121
+ def num_base_priors(self):
122
+ """list[int]: The number of priors (anchors) at a point
123
+ on the feature grid"""
124
+ return [base_anchors.size(0) for base_anchors in self.base_anchors]
125
+
126
+ @property
127
+ def num_levels(self):
128
+ """int: number of feature levels that the generator will be applied"""
129
+ return len(self.strides)
130
+
131
+ def gen_base_anchors(self):
132
+ """Generate base anchors.
133
+
134
+ Returns:
135
+ list(torch.Tensor): Base anchors of a feature grid in multiple \
136
+ feature levels.
137
+ """
138
+ multi_level_base_anchors = []
139
+ for i, base_size in enumerate(self.base_sizes):
140
+ center = None
141
+ if self.centers is not None:
142
+ center = self.centers[i]
143
+ multi_level_base_anchors.append(
144
+ self.gen_single_level_base_anchors(
145
+ base_size,
146
+ scales=self.scales,
147
+ ratios=self.ratios,
148
+ center=center))
149
+ return multi_level_base_anchors
150
+
151
+ def gen_single_level_base_anchors(self,
152
+ base_size,
153
+ scales,
154
+ ratios,
155
+ center=None):
156
+ """Generate base anchors of a single level.
157
+
158
+ Args:
159
+ base_size (int | float): Basic size of an anchor.
160
+ scales (torch.Tensor): Scales of the anchor.
161
+ ratios (torch.Tensor): The ratio between between the height
162
+ and width of anchors in a single level.
163
+ center (tuple[float], optional): The center of the base anchor
164
+ related to a single feature grid. Defaults to None.
165
+
166
+ Returns:
167
+ torch.Tensor: Anchors in a single-level feature maps.
168
+ """
169
+ w = base_size
170
+ h = base_size
171
+ if center is None:
172
+ x_center = self.center_offset * w
173
+ y_center = self.center_offset * h
174
+ else:
175
+ x_center, y_center = center
176
+
177
+ h_ratios = torch.sqrt(ratios)
178
+ w_ratios = 1 / h_ratios
179
+ if self.scale_major:
180
+ ws = (w * w_ratios[:, None] * scales[None, :]).view(-1)
181
+ hs = (h * h_ratios[:, None] * scales[None, :]).view(-1)
182
+ else:
183
+ ws = (w * scales[:, None] * w_ratios[None, :]).view(-1)
184
+ hs = (h * scales[:, None] * h_ratios[None, :]).view(-1)
185
+
186
+ # use float anchor and the anchor's center is aligned with the
187
+ # pixel center
188
+ base_anchors = [
189
+ x_center - 0.5 * ws, y_center - 0.5 * hs, x_center + 0.5 * ws,
190
+ y_center + 0.5 * hs
191
+ ]
192
+ base_anchors = torch.stack(base_anchors, dim=-1)
193
+
194
+ return base_anchors
195
+
196
+ def _meshgrid(self, x, y, row_major=True):
197
+ """Generate mesh grid of x and y.
198
+
199
+ Args:
200
+ x (torch.Tensor): Grids of x dimension.
201
+ y (torch.Tensor): Grids of y dimension.
202
+ row_major (bool, optional): Whether to return y grids first.
203
+ Defaults to True.
204
+
205
+ Returns:
206
+ tuple[torch.Tensor]: The mesh grids of x and y.
207
+ """
208
+ # use shape instead of len to keep tracing while exporting to onnx
209
+ xx = x.repeat(y.shape[0])
210
+ yy = y.view(-1, 1).repeat(1, x.shape[0]).view(-1)
211
+ if row_major:
212
+ return xx, yy
213
+ else:
214
+ return yy, xx
215
+
216
+ def grid_priors(self, featmap_sizes, dtype=torch.float32, device='cuda'):
217
+ """Generate grid anchors in multiple feature levels.
218
+
219
+ Args:
220
+ featmap_sizes (list[tuple]): List of feature map sizes in
221
+ multiple feature levels.
222
+ dtype (:obj:`torch.dtype`): Dtype of priors.
223
+ Default: torch.float32.
224
+ device (str): The device where the anchors will be put on.
225
+
226
+ Return:
227
+ list[torch.Tensor]: Anchors in multiple feature levels. \
228
+ The sizes of each tensor should be [N, 4], where \
229
+ N = width * height * num_base_anchors, width and height \
230
+ are the sizes of the corresponding feature level, \
231
+ num_base_anchors is the number of anchors for that level.
232
+ """
233
+ assert self.num_levels == len(featmap_sizes)
234
+ multi_level_anchors = []
235
+ for i in range(self.num_levels):
236
+ anchors = self.single_level_grid_priors(
237
+ featmap_sizes[i], level_idx=i, dtype=dtype, device=device)
238
+ multi_level_anchors.append(anchors)
239
+ return multi_level_anchors
240
+
241
+ def single_level_grid_priors(self,
242
+ featmap_size,
243
+ level_idx,
244
+ dtype=torch.float32,
245
+ device='cuda'):
246
+ """Generate grid anchors of a single level.
247
+
248
+ Note:
249
+ This function is usually called by method ``self.grid_priors``.
250
+
251
+ Args:
252
+ featmap_size (tuple[int]): Size of the feature maps.
253
+ level_idx (int): The index of corresponding feature map level.
254
+ dtype (obj:`torch.dtype`): Date type of points.Defaults to
255
+ ``torch.float32``.
256
+ device (str, optional): The device the tensor will be put on.
257
+ Defaults to 'cuda'.
258
+
259
+ Returns:
260
+ torch.Tensor: Anchors in the overall feature maps.
261
+ """
262
+
263
+ base_anchors = self.base_anchors[level_idx].to(device).to(dtype)
264
+ feat_h, feat_w = featmap_size
265
+ stride_w, stride_h = self.strides[level_idx]
266
+ # First create Range with the default dtype, than convert to
267
+ # target `dtype` for onnx exporting.
268
+ shift_x = torch.arange(0, feat_w, device=device).to(dtype) * stride_w
269
+ shift_y = torch.arange(0, feat_h, device=device).to(dtype) * stride_h
270
+
271
+ shift_xx, shift_yy = self._meshgrid(shift_x, shift_y)
272
+ shifts = torch.stack([shift_xx, shift_yy, shift_xx, shift_yy], dim=-1)
273
+ # first feat_w elements correspond to the first row of shifts
274
+ # add A anchors (1, A, 4) to K shifts (K, 1, 4) to get
275
+ # shifted anchors (K, A, 4), reshape to (K*A, 4)
276
+
277
+ all_anchors = base_anchors[None, :, :] + shifts[:, None, :]
278
+ all_anchors = all_anchors.view(-1, 4)
279
+ # first A rows correspond to A anchors of (0, 0) in feature map,
280
+ # then (0, 1), (0, 2), ...
281
+ return all_anchors
282
+
283
+ def sparse_priors(self,
284
+ prior_idxs,
285
+ featmap_size,
286
+ level_idx,
287
+ dtype=torch.float32,
288
+ device='cuda'):
289
+ """Generate sparse anchors according to the ``prior_idxs``.
290
+
291
+ Args:
292
+ prior_idxs (Tensor): The index of corresponding anchors
293
+ in the feature map.
294
+ featmap_size (tuple[int]): feature map size arrange as (h, w).
295
+ level_idx (int): The level index of corresponding feature
296
+ map.
297
+ dtype (obj:`torch.dtype`): Date type of points.Defaults to
298
+ ``torch.float32``.
299
+ device (obj:`torch.device`): The device where the points is
300
+ located.
301
+ Returns:
302
+ Tensor: Anchor with shape (N, 4), N should be equal to
303
+ the length of ``prior_idxs``.
304
+ """
305
+
306
+ height, width = featmap_size
307
+ num_base_anchors = self.num_base_anchors[level_idx]
308
+ base_anchor_id = prior_idxs % num_base_anchors
309
+ x = (prior_idxs //
310
+ num_base_anchors) % width * self.strides[level_idx][0]
311
+ y = (prior_idxs // width //
312
+ num_base_anchors) % height * self.strides[level_idx][1]
313
+ priors = torch.stack([x, y, x, y], 1).to(dtype).to(device) + \
314
+ self.base_anchors[level_idx][base_anchor_id, :].to(device)
315
+
316
+ return priors
317
+
318
+ def grid_anchors(self, featmap_sizes, device='cuda'):
319
+ """Generate grid anchors in multiple feature levels.
320
+
321
+ Args:
322
+ featmap_sizes (list[tuple]): List of feature map sizes in
323
+ multiple feature levels.
324
+ device (str): Device where the anchors will be put on.
325
+
326
+ Return:
327
+ list[torch.Tensor]: Anchors in multiple feature levels. \
328
+ The sizes of each tensor should be [N, 4], where \
329
+ N = width * height * num_base_anchors, width and height \
330
+ are the sizes of the corresponding feature level, \
331
+ num_base_anchors is the number of anchors for that level.
332
+ """
333
+ warnings.warn('``grid_anchors`` would be deprecated soon. '
334
+ 'Please use ``grid_priors`` ')
335
+
336
+ assert self.num_levels == len(featmap_sizes)
337
+ multi_level_anchors = []
338
+ for i in range(self.num_levels):
339
+ anchors = self.single_level_grid_anchors(
340
+ self.base_anchors[i].to(device),
341
+ featmap_sizes[i],
342
+ self.strides[i],
343
+ device=device)
344
+ multi_level_anchors.append(anchors)
345
+ return multi_level_anchors
346
+
347
+ def single_level_grid_anchors(self,
348
+ base_anchors,
349
+ featmap_size,
350
+ stride=(16, 16),
351
+ device='cuda'):
352
+ """Generate grid anchors of a single level.
353
+
354
+ Note:
355
+ This function is usually called by method ``self.grid_anchors``.
356
+
357
+ Args:
358
+ base_anchors (torch.Tensor): The base anchors of a feature grid.
359
+ featmap_size (tuple[int]): Size of the feature maps.
360
+ stride (tuple[int], optional): Stride of the feature map in order
361
+ (w, h). Defaults to (16, 16).
362
+ device (str, optional): Device the tensor will be put on.
363
+ Defaults to 'cuda'.
364
+
365
+ Returns:
366
+ torch.Tensor: Anchors in the overall feature maps.
367
+ """
368
+
369
+ warnings.warn(
370
+ '``single_level_grid_anchors`` would be deprecated soon. '
371
+ 'Please use ``single_level_grid_priors`` ')
372
+
373
+ # keep featmap_size as Tensor instead of int, so that we
374
+ # can convert to ONNX correctly
375
+ feat_h, feat_w = featmap_size
376
+ shift_x = torch.arange(0, feat_w, device=device) * stride[0]
377
+ shift_y = torch.arange(0, feat_h, device=device) * stride[1]
378
+
379
+ shift_xx, shift_yy = self._meshgrid(shift_x, shift_y)
380
+ shifts = torch.stack([shift_xx, shift_yy, shift_xx, shift_yy], dim=-1)
381
+ shifts = shifts.type_as(base_anchors)
382
+ # first feat_w elements correspond to the first row of shifts
383
+ # add A anchors (1, A, 4) to K shifts (K, 1, 4) to get
384
+ # shifted anchors (K, A, 4), reshape to (K*A, 4)
385
+
386
+ all_anchors = base_anchors[None, :, :] + shifts[:, None, :]
387
+ all_anchors = all_anchors.view(-1, 4)
388
+ # first A rows correspond to A anchors of (0, 0) in feature map,
389
+ # then (0, 1), (0, 2), ...
390
+ return all_anchors
391
+
392
+ def valid_flags(self, featmap_sizes, pad_shape, device='cuda'):
393
+ """Generate valid flags of anchors in multiple feature levels.
394
+
395
+ Args:
396
+ featmap_sizes (list(tuple)): List of feature map sizes in
397
+ multiple feature levels.
398
+ pad_shape (tuple): The padded shape of the image.
399
+ device (str): Device where the anchors will be put on.
400
+
401
+ Return:
402
+ list(torch.Tensor): Valid flags of anchors in multiple levels.
403
+ """
404
+ assert self.num_levels == len(featmap_sizes)
405
+ multi_level_flags = []
406
+ for i in range(self.num_levels):
407
+ anchor_stride = self.strides[i]
408
+ feat_h, feat_w = featmap_sizes[i]
409
+ h, w = pad_shape[:2]
410
+ valid_feat_h = min(int(np.ceil(h / anchor_stride[1])), feat_h)
411
+ valid_feat_w = min(int(np.ceil(w / anchor_stride[0])), feat_w)
412
+ flags = self.single_level_valid_flags((feat_h, feat_w),
413
+ (valid_feat_h, valid_feat_w),
414
+ self.num_base_anchors[i],
415
+ device=device)
416
+ multi_level_flags.append(flags)
417
+ return multi_level_flags
418
+
419
+ def single_level_valid_flags(self,
420
+ featmap_size,
421
+ valid_size,
422
+ num_base_anchors,
423
+ device='cuda'):
424
+ """Generate the valid flags of anchor in a single feature map.
425
+
426
+ Args:
427
+ featmap_size (tuple[int]): The size of feature maps, arrange
428
+ as (h, w).
429
+ valid_size (tuple[int]): The valid size of the feature maps.
430
+ num_base_anchors (int): The number of base anchors.
431
+ device (str, optional): Device where the flags will be put on.
432
+ Defaults to 'cuda'.
433
+
434
+ Returns:
435
+ torch.Tensor: The valid flags of each anchor in a single level \
436
+ feature map.
437
+ """
438
+ feat_h, feat_w = featmap_size
439
+ valid_h, valid_w = valid_size
440
+ assert valid_h <= feat_h and valid_w <= feat_w
441
+ valid_x = torch.zeros(feat_w, dtype=torch.bool, device=device)
442
+ valid_y = torch.zeros(feat_h, dtype=torch.bool, device=device)
443
+ valid_x[:valid_w] = 1
444
+ valid_y[:valid_h] = 1
445
+ valid_xx, valid_yy = self._meshgrid(valid_x, valid_y)
446
+ valid = valid_xx & valid_yy
447
+ valid = valid[:, None].expand(valid.size(0),
448
+ num_base_anchors).contiguous().view(-1)
449
+ return valid
450
+
451
+ def __repr__(self):
452
+ """str: a string that describes the module"""
453
+ indent_str = ' '
454
+ repr_str = self.__class__.__name__ + '(\n'
455
+ repr_str += f'{indent_str}strides={self.strides},\n'
456
+ repr_str += f'{indent_str}ratios={self.ratios},\n'
457
+ repr_str += f'{indent_str}scales={self.scales},\n'
458
+ repr_str += f'{indent_str}base_sizes={self.base_sizes},\n'
459
+ repr_str += f'{indent_str}scale_major={self.scale_major},\n'
460
+ repr_str += f'{indent_str}octave_base_scale='
461
+ repr_str += f'{self.octave_base_scale},\n'
462
+ repr_str += f'{indent_str}scales_per_octave='
463
+ repr_str += f'{self.scales_per_octave},\n'
464
+ repr_str += f'{indent_str}num_levels={self.num_levels}\n'
465
+ repr_str += f'{indent_str}centers={self.centers},\n'
466
+ repr_str += f'{indent_str}center_offset={self.center_offset})'
467
+ return repr_str
468
+
469
+
470
+ @PRIOR_GENERATORS.register_module()
471
+ class SSDAnchorGenerator(AnchorGenerator):
472
+ """Anchor generator for SSD.
473
+
474
+ Args:
475
+ strides (list[int] | list[tuple[int, int]]): Strides of anchors
476
+ in multiple feature levels.
477
+ ratios (list[float]): The list of ratios between the height and width
478
+ of anchors in a single level.
479
+ min_sizes (list[float]): The list of minimum anchor sizes on each
480
+ level.
481
+ max_sizes (list[float]): The list of maximum anchor sizes on each
482
+ level.
483
+ basesize_ratio_range (tuple(float)): Ratio range of anchors. Being
484
+ used when not setting min_sizes and max_sizes.
485
+ input_size (int): Size of feature map, 300 for SSD300, 512 for
486
+ SSD512. Being used when not setting min_sizes and max_sizes.
487
+ scale_major (bool): Whether to multiply scales first when generating
488
+ base anchors. If true, the anchors in the same row will have the
489
+ same scales. It is always set to be False in SSD.
490
+ """
491
+
492
+ def __init__(self,
493
+ strides,
494
+ ratios,
495
+ min_sizes=None,
496
+ max_sizes=None,
497
+ basesize_ratio_range=(0.15, 0.9),
498
+ input_size=300,
499
+ scale_major=True):
500
+ assert len(strides) == len(ratios)
501
+ assert not (min_sizes is None) ^ (max_sizes is None)
502
+ self.strides = [_pair(stride) for stride in strides]
503
+ self.centers = [(stride[0] / 2., stride[1] / 2.)
504
+ for stride in self.strides]
505
+
506
+ if min_sizes is None and max_sizes is None:
507
+ # use hard code to generate SSD anchors
508
+ self.input_size = input_size
509
+ assert mmcv.is_tuple_of(basesize_ratio_range, float)
510
+ self.basesize_ratio_range = basesize_ratio_range
511
+ # calculate anchor ratios and sizes
512
+ min_ratio, max_ratio = basesize_ratio_range
513
+ min_ratio = int(min_ratio * 100)
514
+ max_ratio = int(max_ratio * 100)
515
+ step = int(np.floor(max_ratio - min_ratio) / (self.num_levels - 2))
516
+ min_sizes = []
517
+ max_sizes = []
518
+ for ratio in range(int(min_ratio), int(max_ratio) + 1, step):
519
+ min_sizes.append(int(self.input_size * ratio / 100))
520
+ max_sizes.append(int(self.input_size * (ratio + step) / 100))
521
+ if self.input_size == 300:
522
+ if basesize_ratio_range[0] == 0.15: # SSD300 COCO
523
+ min_sizes.insert(0, int(self.input_size * 7 / 100))
524
+ max_sizes.insert(0, int(self.input_size * 15 / 100))
525
+ elif basesize_ratio_range[0] == 0.2: # SSD300 VOC
526
+ min_sizes.insert(0, int(self.input_size * 10 / 100))
527
+ max_sizes.insert(0, int(self.input_size * 20 / 100))
528
+ else:
529
+ raise ValueError(
530
+ 'basesize_ratio_range[0] should be either 0.15'
531
+ 'or 0.2 when input_size is 300, got '
532
+ f'{basesize_ratio_range[0]}.')
533
+ elif self.input_size == 512:
534
+ if basesize_ratio_range[0] == 0.1: # SSD512 COCO
535
+ min_sizes.insert(0, int(self.input_size * 4 / 100))
536
+ max_sizes.insert(0, int(self.input_size * 10 / 100))
537
+ elif basesize_ratio_range[0] == 0.15: # SSD512 VOC
538
+ min_sizes.insert(0, int(self.input_size * 7 / 100))
539
+ max_sizes.insert(0, int(self.input_size * 15 / 100))
540
+ else:
541
+ raise ValueError(
542
+ 'When not setting min_sizes and max_sizes,'
543
+ 'basesize_ratio_range[0] should be either 0.1'
544
+ 'or 0.15 when input_size is 512, got'
545
+ f' {basesize_ratio_range[0]}.')
546
+ else:
547
+ raise ValueError(
548
+ 'Only support 300 or 512 in SSDAnchorGenerator when '
549
+ 'not setting min_sizes and max_sizes, '
550
+ f'got {self.input_size}.')
551
+
552
+ assert len(min_sizes) == len(max_sizes) == len(strides)
553
+
554
+ anchor_ratios = []
555
+ anchor_scales = []
556
+ for k in range(len(self.strides)):
557
+ scales = [1., np.sqrt(max_sizes[k] / min_sizes[k])]
558
+ anchor_ratio = [1.]
559
+ for r in ratios[k]:
560
+ anchor_ratio += [1 / r, r] # 4 or 6 ratio
561
+ anchor_ratios.append(torch.Tensor(anchor_ratio))
562
+ anchor_scales.append(torch.Tensor(scales))
563
+
564
+ self.base_sizes = min_sizes
565
+ self.scales = anchor_scales
566
+ self.ratios = anchor_ratios
567
+ self.scale_major = scale_major
568
+ self.center_offset = 0
569
+ self.base_anchors = self.gen_base_anchors()
570
+
571
+ def gen_base_anchors(self):
572
+ """Generate base anchors.
573
+
574
+ Returns:
575
+ list(torch.Tensor): Base anchors of a feature grid in multiple \
576
+ feature levels.
577
+ """
578
+ multi_level_base_anchors = []
579
+ for i, base_size in enumerate(self.base_sizes):
580
+ base_anchors = self.gen_single_level_base_anchors(
581
+ base_size,
582
+ scales=self.scales[i],
583
+ ratios=self.ratios[i],
584
+ center=self.centers[i])
585
+ indices = list(range(len(self.ratios[i])))
586
+ indices.insert(1, len(indices))
587
+ base_anchors = torch.index_select(base_anchors, 0,
588
+ torch.LongTensor(indices))
589
+ multi_level_base_anchors.append(base_anchors)
590
+ return multi_level_base_anchors
591
+
592
+ def __repr__(self):
593
+ """str: a string that describes the module"""
594
+ indent_str = ' '
595
+ repr_str = self.__class__.__name__ + '(\n'
596
+ repr_str += f'{indent_str}strides={self.strides},\n'
597
+ repr_str += f'{indent_str}scales={self.scales},\n'
598
+ repr_str += f'{indent_str}scale_major={self.scale_major},\n'
599
+ repr_str += f'{indent_str}input_size={self.input_size},\n'
600
+ repr_str += f'{indent_str}scales={self.scales},\n'
601
+ repr_str += f'{indent_str}ratios={self.ratios},\n'
602
+ repr_str += f'{indent_str}num_levels={self.num_levels},\n'
603
+ repr_str += f'{indent_str}base_sizes={self.base_sizes},\n'
604
+ repr_str += f'{indent_str}basesize_ratio_range='
605
+ repr_str += f'{self.basesize_ratio_range})'
606
+ return repr_str
607
+
608
+
609
+ @PRIOR_GENERATORS.register_module()
610
+ class LegacyAnchorGenerator(AnchorGenerator):
611
+ """Legacy anchor generator used in MMDetection V1.x.
612
+
613
+ Note:
614
+ Difference to the V2.0 anchor generator:
615
+
616
+ 1. The center offset of V1.x anchors are set to be 0.5 rather than 0.
617
+ 2. The width/height are minused by 1 when calculating the anchors' \
618
+ centers and corners to meet the V1.x coordinate system.
619
+ 3. The anchors' corners are quantized.
620
+
621
+ Args:
622
+ strides (list[int] | list[tuple[int]]): Strides of anchors
623
+ in multiple feature levels.
624
+ ratios (list[float]): The list of ratios between the height and width
625
+ of anchors in a single level.
626
+ scales (list[int] | None): Anchor scales for anchors in a single level.
627
+ It cannot be set at the same time if `octave_base_scale` and
628
+ `scales_per_octave` are set.
629
+ base_sizes (list[int]): The basic sizes of anchors in multiple levels.
630
+ If None is given, strides will be used to generate base_sizes.
631
+ scale_major (bool): Whether to multiply scales first when generating
632
+ base anchors. If true, the anchors in the same row will have the
633
+ same scales. By default it is True in V2.0
634
+ octave_base_scale (int): The base scale of octave.
635
+ scales_per_octave (int): Number of scales for each octave.
636
+ `octave_base_scale` and `scales_per_octave` are usually used in
637
+ retinanet and the `scales` should be None when they are set.
638
+ centers (list[tuple[float, float]] | None): The centers of the anchor
639
+ relative to the feature grid center in multiple feature levels.
640
+ By default it is set to be None and not used. It a list of float
641
+ is given, this list will be used to shift the centers of anchors.
642
+ center_offset (float): The offset of center in proportion to anchors'
643
+ width and height. By default it is 0.5 in V2.0 but it should be 0.5
644
+ in v1.x models.
645
+
646
+ Examples:
647
+ >>> from mmdet.core import LegacyAnchorGenerator
648
+ >>> self = LegacyAnchorGenerator(
649
+ >>> [16], [1.], [1.], [9], center_offset=0.5)
650
+ >>> all_anchors = self.grid_anchors(((2, 2),), device='cpu')
651
+ >>> print(all_anchors)
652
+ [tensor([[ 0., 0., 8., 8.],
653
+ [16., 0., 24., 8.],
654
+ [ 0., 16., 8., 24.],
655
+ [16., 16., 24., 24.]])]
656
+ """
657
+
658
+ def gen_single_level_base_anchors(self,
659
+ base_size,
660
+ scales,
661
+ ratios,
662
+ center=None):
663
+ """Generate base anchors of a single level.
664
+
665
+ Note:
666
+ The width/height of anchors are minused by 1 when calculating \
667
+ the centers and corners to meet the V1.x coordinate system.
668
+
669
+ Args:
670
+ base_size (int | float): Basic size of an anchor.
671
+ scales (torch.Tensor): Scales of the anchor.
672
+ ratios (torch.Tensor): The ratio between between the height.
673
+ and width of anchors in a single level.
674
+ center (tuple[float], optional): The center of the base anchor
675
+ related to a single feature grid. Defaults to None.
676
+
677
+ Returns:
678
+ torch.Tensor: Anchors in a single-level feature map.
679
+ """
680
+ w = base_size
681
+ h = base_size
682
+ if center is None:
683
+ x_center = self.center_offset * (w - 1)
684
+ y_center = self.center_offset * (h - 1)
685
+ else:
686
+ x_center, y_center = center
687
+
688
+ h_ratios = torch.sqrt(ratios)
689
+ w_ratios = 1 / h_ratios
690
+ if self.scale_major:
691
+ ws = (w * w_ratios[:, None] * scales[None, :]).view(-1)
692
+ hs = (h * h_ratios[:, None] * scales[None, :]).view(-1)
693
+ else:
694
+ ws = (w * scales[:, None] * w_ratios[None, :]).view(-1)
695
+ hs = (h * scales[:, None] * h_ratios[None, :]).view(-1)
696
+
697
+ # use float anchor and the anchor's center is aligned with the
698
+ # pixel center
699
+ base_anchors = [
700
+ x_center - 0.5 * (ws - 1), y_center - 0.5 * (hs - 1),
701
+ x_center + 0.5 * (ws - 1), y_center + 0.5 * (hs - 1)
702
+ ]
703
+ base_anchors = torch.stack(base_anchors, dim=-1).round()
704
+
705
+ return base_anchors
706
+
707
+
708
+ @PRIOR_GENERATORS.register_module()
709
+ class LegacySSDAnchorGenerator(SSDAnchorGenerator, LegacyAnchorGenerator):
710
+ """Legacy anchor generator used in MMDetection V1.x.
711
+
712
+ The difference between `LegacySSDAnchorGenerator` and `SSDAnchorGenerator`
713
+ can be found in `LegacyAnchorGenerator`.
714
+ """
715
+
716
+ def __init__(self,
717
+ strides,
718
+ ratios,
719
+ basesize_ratio_range,
720
+ input_size=300,
721
+ scale_major=True):
722
+ super(LegacySSDAnchorGenerator, self).__init__(
723
+ strides=strides,
724
+ ratios=ratios,
725
+ basesize_ratio_range=basesize_ratio_range,
726
+ input_size=input_size,
727
+ scale_major=scale_major)
728
+ self.centers = [((stride - 1) / 2., (stride - 1) / 2.)
729
+ for stride in strides]
730
+ self.base_anchors = self.gen_base_anchors()
731
+
732
+
733
+ @PRIOR_GENERATORS.register_module()
734
+ class YOLOAnchorGenerator(AnchorGenerator):
735
+ """Anchor generator for YOLO.
736
+
737
+ Args:
738
+ strides (list[int] | list[tuple[int, int]]): Strides of anchors
739
+ in multiple feature levels.
740
+ base_sizes (list[list[tuple[int, int]]]): The basic sizes
741
+ of anchors in multiple levels.
742
+ """
743
+
744
+ def __init__(self, strides, base_sizes):
745
+ self.strides = [_pair(stride) for stride in strides]
746
+ self.centers = [(stride[0] / 2., stride[1] / 2.)
747
+ for stride in self.strides]
748
+ self.base_sizes = []
749
+ num_anchor_per_level = len(base_sizes[0])
750
+ for base_sizes_per_level in base_sizes:
751
+ assert num_anchor_per_level == len(base_sizes_per_level)
752
+ self.base_sizes.append(
753
+ [_pair(base_size) for base_size in base_sizes_per_level])
754
+ self.base_anchors = self.gen_base_anchors()
755
+
756
+ @property
757
+ def num_levels(self):
758
+ """int: number of feature levels that the generator will be applied"""
759
+ return len(self.base_sizes)
760
+
761
+ def gen_base_anchors(self):
762
+ """Generate base anchors.
763
+
764
+ Returns:
765
+ list(torch.Tensor): Base anchors of a feature grid in multiple \
766
+ feature levels.
767
+ """
768
+ multi_level_base_anchors = []
769
+ for i, base_sizes_per_level in enumerate(self.base_sizes):
770
+ center = None
771
+ if self.centers is not None:
772
+ center = self.centers[i]
773
+ multi_level_base_anchors.append(
774
+ self.gen_single_level_base_anchors(base_sizes_per_level,
775
+ center))
776
+ return multi_level_base_anchors
777
+
778
+ def gen_single_level_base_anchors(self, base_sizes_per_level, center=None):
779
+ """Generate base anchors of a single level.
780
+
781
+ Args:
782
+ base_sizes_per_level (list[tuple[int, int]]): Basic sizes of
783
+ anchors.
784
+ center (tuple[float], optional): The center of the base anchor
785
+ related to a single feature grid. Defaults to None.
786
+
787
+ Returns:
788
+ torch.Tensor: Anchors in a single-level feature maps.
789
+ """
790
+ x_center, y_center = center
791
+ base_anchors = []
792
+ for base_size in base_sizes_per_level:
793
+ w, h = base_size
794
+
795
+ # use float anchor and the anchor's center is aligned with the
796
+ # pixel center
797
+ base_anchor = torch.Tensor([
798
+ x_center - 0.5 * w, y_center - 0.5 * h, x_center + 0.5 * w,
799
+ y_center + 0.5 * h
800
+ ])
801
+ base_anchors.append(base_anchor)
802
+ base_anchors = torch.stack(base_anchors, dim=0)
803
+
804
+ return base_anchors
805
+
806
+ def responsible_flags(self, featmap_sizes, gt_bboxes, device='cuda'):
807
+ """Generate responsible anchor flags of grid cells in multiple scales.
808
+
809
+ Args:
810
+ featmap_sizes (list(tuple)): List of feature map sizes in multiple
811
+ feature levels.
812
+ gt_bboxes (Tensor): Ground truth boxes, shape (n, 4).
813
+ device (str): Device where the anchors will be put on.
814
+
815
+ Return:
816
+ list(torch.Tensor): responsible flags of anchors in multiple level
817
+ """
818
+ assert self.num_levels == len(featmap_sizes)
819
+ multi_level_responsible_flags = []
820
+ for i in range(self.num_levels):
821
+ anchor_stride = self.strides[i]
822
+ flags = self.single_level_responsible_flags(
823
+ featmap_sizes[i],
824
+ gt_bboxes,
825
+ anchor_stride,
826
+ self.num_base_anchors[i],
827
+ device=device)
828
+ multi_level_responsible_flags.append(flags)
829
+ return multi_level_responsible_flags
830
+
831
+ def single_level_responsible_flags(self,
832
+ featmap_size,
833
+ gt_bboxes,
834
+ stride,
835
+ num_base_anchors,
836
+ device='cuda'):
837
+ """Generate the responsible flags of anchor in a single feature map.
838
+
839
+ Args:
840
+ featmap_size (tuple[int]): The size of feature maps.
841
+ gt_bboxes (Tensor): Ground truth boxes, shape (n, 4).
842
+ stride (tuple(int)): stride of current level
843
+ num_base_anchors (int): The number of base anchors.
844
+ device (str, optional): Device where the flags will be put on.
845
+ Defaults to 'cuda'.
846
+
847
+ Returns:
848
+ torch.Tensor: The valid flags of each anchor in a single level \
849
+ feature map.
850
+ """
851
+ feat_h, feat_w = featmap_size
852
+ gt_bboxes_cx = ((gt_bboxes[:, 0] + gt_bboxes[:, 2]) * 0.5).to(device)
853
+ gt_bboxes_cy = ((gt_bboxes[:, 1] + gt_bboxes[:, 3]) * 0.5).to(device)
854
+ gt_bboxes_grid_x = torch.floor(gt_bboxes_cx / stride[0]).long()
855
+ gt_bboxes_grid_y = torch.floor(gt_bboxes_cy / stride[1]).long()
856
+
857
+ # row major indexing
858
+ gt_bboxes_grid_idx = gt_bboxes_grid_y * feat_w + gt_bboxes_grid_x
859
+
860
+ responsible_grid = torch.zeros(
861
+ feat_h * feat_w, dtype=torch.uint8, device=device)
862
+ responsible_grid[gt_bboxes_grid_idx] = 1
863
+
864
+ responsible_grid = responsible_grid[:, None].expand(
865
+ responsible_grid.size(0), num_base_anchors).contiguous().view(-1)
866
+ return responsible_grid
submodules/chartdete/mmdet/core/anchor/builder.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import warnings
3
+
4
+ from mmcv.utils import Registry, build_from_cfg
5
+
6
+ PRIOR_GENERATORS = Registry('Generator for anchors and points')
7
+
8
+ ANCHOR_GENERATORS = PRIOR_GENERATORS
9
+
10
+
11
+ def build_prior_generator(cfg, default_args=None):
12
+ return build_from_cfg(cfg, PRIOR_GENERATORS, default_args)
13
+
14
+
15
+ def build_anchor_generator(cfg, default_args=None):
16
+ warnings.warn(
17
+ '``build_anchor_generator`` would be deprecated soon, please use '
18
+ '``build_prior_generator`` ')
19
+ return build_prior_generator(cfg, default_args=default_args)
submodules/chartdete/mmdet/core/anchor/point_generator.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import numpy as np
3
+ import torch
4
+ from torch.nn.modules.utils import _pair
5
+
6
+ from .builder import PRIOR_GENERATORS
7
+
8
+
9
+ @PRIOR_GENERATORS.register_module()
10
+ class PointGenerator:
11
+
12
+ def _meshgrid(self, x, y, row_major=True):
13
+ xx = x.repeat(len(y))
14
+ yy = y.view(-1, 1).repeat(1, len(x)).view(-1)
15
+ if row_major:
16
+ return xx, yy
17
+ else:
18
+ return yy, xx
19
+
20
+ def grid_points(self, featmap_size, stride=16, device='cuda'):
21
+ feat_h, feat_w = featmap_size
22
+ shift_x = torch.arange(0., feat_w, device=device) * stride
23
+ shift_y = torch.arange(0., feat_h, device=device) * stride
24
+ shift_xx, shift_yy = self._meshgrid(shift_x, shift_y)
25
+ stride = shift_x.new_full((shift_xx.shape[0], ), stride)
26
+ shifts = torch.stack([shift_xx, shift_yy, stride], dim=-1)
27
+ all_points = shifts.to(device)
28
+ return all_points
29
+
30
+ def valid_flags(self, featmap_size, valid_size, device='cuda'):
31
+ feat_h, feat_w = featmap_size
32
+ valid_h, valid_w = valid_size
33
+ assert valid_h <= feat_h and valid_w <= feat_w
34
+ valid_x = torch.zeros(feat_w, dtype=torch.bool, device=device)
35
+ valid_y = torch.zeros(feat_h, dtype=torch.bool, device=device)
36
+ valid_x[:valid_w] = 1
37
+ valid_y[:valid_h] = 1
38
+ valid_xx, valid_yy = self._meshgrid(valid_x, valid_y)
39
+ valid = valid_xx & valid_yy
40
+ return valid
41
+
42
+
43
+ @PRIOR_GENERATORS.register_module()
44
+ class MlvlPointGenerator:
45
+ """Standard points generator for multi-level (Mlvl) feature maps in 2D
46
+ points-based detectors.
47
+
48
+ Args:
49
+ strides (list[int] | list[tuple[int, int]]): Strides of anchors
50
+ in multiple feature levels in order (w, h).
51
+ offset (float): The offset of points, the value is normalized with
52
+ corresponding stride. Defaults to 0.5.
53
+ """
54
+
55
+ def __init__(self, strides, offset=0.5):
56
+ self.strides = [_pair(stride) for stride in strides]
57
+ self.offset = offset
58
+
59
+ @property
60
+ def num_levels(self):
61
+ """int: number of feature levels that the generator will be applied"""
62
+ return len(self.strides)
63
+
64
+ @property
65
+ def num_base_priors(self):
66
+ """list[int]: The number of priors (points) at a point
67
+ on the feature grid"""
68
+ return [1 for _ in range(len(self.strides))]
69
+
70
+ def _meshgrid(self, x, y, row_major=True):
71
+ yy, xx = torch.meshgrid(y, x)
72
+ if row_major:
73
+ # warning .flatten() would cause error in ONNX exporting
74
+ # have to use reshape here
75
+ return xx.reshape(-1), yy.reshape(-1)
76
+
77
+ else:
78
+ return yy.reshape(-1), xx.reshape(-1)
79
+
80
+ def grid_priors(self,
81
+ featmap_sizes,
82
+ dtype=torch.float32,
83
+ device='cuda',
84
+ with_stride=False):
85
+ """Generate grid points of multiple feature levels.
86
+
87
+ Args:
88
+ featmap_sizes (list[tuple]): List of feature map sizes in
89
+ multiple feature levels, each size arrange as
90
+ as (h, w).
91
+ dtype (:obj:`dtype`): Dtype of priors. Default: torch.float32.
92
+ device (str): The device where the anchors will be put on.
93
+ with_stride (bool): Whether to concatenate the stride to
94
+ the last dimension of points.
95
+
96
+ Return:
97
+ list[torch.Tensor]: Points of multiple feature levels.
98
+ The sizes of each tensor should be (N, 2) when with stride is
99
+ ``False``, where N = width * height, width and height
100
+ are the sizes of the corresponding feature level,
101
+ and the last dimension 2 represent (coord_x, coord_y),
102
+ otherwise the shape should be (N, 4),
103
+ and the last dimension 4 represent
104
+ (coord_x, coord_y, stride_w, stride_h).
105
+ """
106
+
107
+ assert self.num_levels == len(featmap_sizes)
108
+ multi_level_priors = []
109
+ for i in range(self.num_levels):
110
+ priors = self.single_level_grid_priors(
111
+ featmap_sizes[i],
112
+ level_idx=i,
113
+ dtype=dtype,
114
+ device=device,
115
+ with_stride=with_stride)
116
+ multi_level_priors.append(priors)
117
+ return multi_level_priors
118
+
119
+ def single_level_grid_priors(self,
120
+ featmap_size,
121
+ level_idx,
122
+ dtype=torch.float32,
123
+ device='cuda',
124
+ with_stride=False):
125
+ """Generate grid Points of a single level.
126
+
127
+ Note:
128
+ This function is usually called by method ``self.grid_priors``.
129
+
130
+ Args:
131
+ featmap_size (tuple[int]): Size of the feature maps, arrange as
132
+ (h, w).
133
+ level_idx (int): The index of corresponding feature map level.
134
+ dtype (:obj:`dtype`): Dtype of priors. Default: torch.float32.
135
+ device (str, optional): The device the tensor will be put on.
136
+ Defaults to 'cuda'.
137
+ with_stride (bool): Concatenate the stride to the last dimension
138
+ of points.
139
+
140
+ Return:
141
+ Tensor: Points of single feature levels.
142
+ The shape of tensor should be (N, 2) when with stride is
143
+ ``False``, where N = width * height, width and height
144
+ are the sizes of the corresponding feature level,
145
+ and the last dimension 2 represent (coord_x, coord_y),
146
+ otherwise the shape should be (N, 4),
147
+ and the last dimension 4 represent
148
+ (coord_x, coord_y, stride_w, stride_h).
149
+ """
150
+ feat_h, feat_w = featmap_size
151
+ stride_w, stride_h = self.strides[level_idx]
152
+ shift_x = (torch.arange(0, feat_w, device=device) +
153
+ self.offset) * stride_w
154
+ # keep featmap_size as Tensor instead of int, so that we
155
+ # can convert to ONNX correctly
156
+ shift_x = shift_x.to(dtype)
157
+
158
+ shift_y = (torch.arange(0, feat_h, device=device) +
159
+ self.offset) * stride_h
160
+ # keep featmap_size as Tensor instead of int, so that we
161
+ # can convert to ONNX correctly
162
+ shift_y = shift_y.to(dtype)
163
+ shift_xx, shift_yy = self._meshgrid(shift_x, shift_y)
164
+ if not with_stride:
165
+ shifts = torch.stack([shift_xx, shift_yy], dim=-1)
166
+ else:
167
+ # use `shape[0]` instead of `len(shift_xx)` for ONNX export
168
+ stride_w = shift_xx.new_full((shift_xx.shape[0], ),
169
+ stride_w).to(dtype)
170
+ stride_h = shift_xx.new_full((shift_yy.shape[0], ),
171
+ stride_h).to(dtype)
172
+ shifts = torch.stack([shift_xx, shift_yy, stride_w, stride_h],
173
+ dim=-1)
174
+ all_points = shifts.to(device)
175
+ return all_points
176
+
177
+ def valid_flags(self, featmap_sizes, pad_shape, device='cuda'):
178
+ """Generate valid flags of points of multiple feature levels.
179
+
180
+ Args:
181
+ featmap_sizes (list(tuple)): List of feature map sizes in
182
+ multiple feature levels, each size arrange as
183
+ as (h, w).
184
+ pad_shape (tuple(int)): The padded shape of the image,
185
+ arrange as (h, w).
186
+ device (str): The device where the anchors will be put on.
187
+
188
+ Return:
189
+ list(torch.Tensor): Valid flags of points of multiple levels.
190
+ """
191
+ assert self.num_levels == len(featmap_sizes)
192
+ multi_level_flags = []
193
+ for i in range(self.num_levels):
194
+ point_stride = self.strides[i]
195
+ feat_h, feat_w = featmap_sizes[i]
196
+ h, w = pad_shape[:2]
197
+ valid_feat_h = min(int(np.ceil(h / point_stride[1])), feat_h)
198
+ valid_feat_w = min(int(np.ceil(w / point_stride[0])), feat_w)
199
+ flags = self.single_level_valid_flags((feat_h, feat_w),
200
+ (valid_feat_h, valid_feat_w),
201
+ device=device)
202
+ multi_level_flags.append(flags)
203
+ return multi_level_flags
204
+
205
+ def single_level_valid_flags(self,
206
+ featmap_size,
207
+ valid_size,
208
+ device='cuda'):
209
+ """Generate the valid flags of points of a single feature map.
210
+
211
+ Args:
212
+ featmap_size (tuple[int]): The size of feature maps, arrange as
213
+ as (h, w).
214
+ valid_size (tuple[int]): The valid size of the feature maps.
215
+ The size arrange as as (h, w).
216
+ device (str, optional): The device where the flags will be put on.
217
+ Defaults to 'cuda'.
218
+
219
+ Returns:
220
+ torch.Tensor: The valid flags of each points in a single level \
221
+ feature map.
222
+ """
223
+ feat_h, feat_w = featmap_size
224
+ valid_h, valid_w = valid_size
225
+ assert valid_h <= feat_h and valid_w <= feat_w
226
+ valid_x = torch.zeros(feat_w, dtype=torch.bool, device=device)
227
+ valid_y = torch.zeros(feat_h, dtype=torch.bool, device=device)
228
+ valid_x[:valid_w] = 1
229
+ valid_y[:valid_h] = 1
230
+ valid_xx, valid_yy = self._meshgrid(valid_x, valid_y)
231
+ valid = valid_xx & valid_yy
232
+ return valid
233
+
234
+ def sparse_priors(self,
235
+ prior_idxs,
236
+ featmap_size,
237
+ level_idx,
238
+ dtype=torch.float32,
239
+ device='cuda'):
240
+ """Generate sparse points according to the ``prior_idxs``.
241
+
242
+ Args:
243
+ prior_idxs (Tensor): The index of corresponding anchors
244
+ in the feature map.
245
+ featmap_size (tuple[int]): feature map size arrange as (w, h).
246
+ level_idx (int): The level index of corresponding feature
247
+ map.
248
+ dtype (obj:`torch.dtype`): Date type of points. Defaults to
249
+ ``torch.float32``.
250
+ device (obj:`torch.device`): The device where the points is
251
+ located.
252
+ Returns:
253
+ Tensor: Anchor with shape (N, 2), N should be equal to
254
+ the length of ``prior_idxs``. And last dimension
255
+ 2 represent (coord_x, coord_y).
256
+ """
257
+ height, width = featmap_size
258
+ x = (prior_idxs % width + self.offset) * self.strides[level_idx][0]
259
+ y = ((prior_idxs // width) % height +
260
+ self.offset) * self.strides[level_idx][1]
261
+ prioris = torch.stack([x, y], 1).to(dtype)
262
+ prioris = prioris.to(device)
263
+ return prioris
submodules/chartdete/mmdet/core/anchor/utils.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+
5
+ def images_to_levels(target, num_levels):
6
+ """Convert targets by image to targets by feature level.
7
+
8
+ [target_img0, target_img1] -> [target_level0, target_level1, ...]
9
+ """
10
+ target = torch.stack(target, 0)
11
+ level_targets = []
12
+ start = 0
13
+ for n in num_levels:
14
+ end = start + n
15
+ # level_targets.append(target[:, start:end].squeeze(0))
16
+ level_targets.append(target[:, start:end])
17
+ start = end
18
+ return level_targets
19
+
20
+
21
+ def anchor_inside_flags(flat_anchors,
22
+ valid_flags,
23
+ img_shape,
24
+ allowed_border=0):
25
+ """Check whether the anchors are inside the border.
26
+
27
+ Args:
28
+ flat_anchors (torch.Tensor): Flatten anchors, shape (n, 4).
29
+ valid_flags (torch.Tensor): An existing valid flags of anchors.
30
+ img_shape (tuple(int)): Shape of current image.
31
+ allowed_border (int, optional): The border to allow the valid anchor.
32
+ Defaults to 0.
33
+
34
+ Returns:
35
+ torch.Tensor: Flags indicating whether the anchors are inside a \
36
+ valid range.
37
+ """
38
+ img_h, img_w = img_shape[:2]
39
+ if allowed_border >= 0:
40
+ inside_flags = valid_flags & \
41
+ (flat_anchors[:, 0] >= -allowed_border) & \
42
+ (flat_anchors[:, 1] >= -allowed_border) & \
43
+ (flat_anchors[:, 2] < img_w + allowed_border) & \
44
+ (flat_anchors[:, 3] < img_h + allowed_border)
45
+ else:
46
+ inside_flags = valid_flags
47
+ return inside_flags
48
+
49
+
50
+ def calc_region(bbox, ratio, featmap_size=None):
51
+ """Calculate a proportional bbox region.
52
+
53
+ The bbox center are fixed and the new h' and w' is h * ratio and w * ratio.
54
+
55
+ Args:
56
+ bbox (Tensor): Bboxes to calculate regions, shape (n, 4).
57
+ ratio (float): Ratio of the output region.
58
+ featmap_size (tuple): Feature map size used for clipping the boundary.
59
+
60
+ Returns:
61
+ tuple: x1, y1, x2, y2
62
+ """
63
+ x1 = torch.round((1 - ratio) * bbox[0] + ratio * bbox[2]).long()
64
+ y1 = torch.round((1 - ratio) * bbox[1] + ratio * bbox[3]).long()
65
+ x2 = torch.round(ratio * bbox[0] + (1 - ratio) * bbox[2]).long()
66
+ y2 = torch.round(ratio * bbox[1] + (1 - ratio) * bbox[3]).long()
67
+ if featmap_size is not None:
68
+ x1 = x1.clamp(min=0, max=featmap_size[1])
69
+ y1 = y1.clamp(min=0, max=featmap_size[0])
70
+ x2 = x2.clamp(min=0, max=featmap_size[1])
71
+ y2 = y2.clamp(min=0, max=featmap_size[0])
72
+ return (x1, y1, x2, y2)
submodules/chartdete/mmdet/core/bbox/__init__.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from .assigners import (AssignResult, BaseAssigner, CenterRegionAssigner,
3
+ MaxIoUAssigner, RegionAssigner)
4
+ from .builder import build_assigner, build_bbox_coder, build_sampler
5
+ from .coder import (BaseBBoxCoder, DeltaXYWHBBoxCoder, DistancePointBBoxCoder,
6
+ PseudoBBoxCoder, TBLRBBoxCoder)
7
+ from .iou_calculators import BboxOverlaps2D, bbox_overlaps
8
+ from .samplers import (BaseSampler, CombinedSampler,
9
+ InstanceBalancedPosSampler, IoUBalancedNegSampler,
10
+ OHEMSampler, PseudoSampler, RandomSampler,
11
+ SamplingResult, ScoreHLRSampler)
12
+ from .transforms import (bbox2distance, bbox2result, bbox2roi,
13
+ bbox_cxcywh_to_xyxy, bbox_flip, bbox_mapping,
14
+ bbox_mapping_back, bbox_rescale, bbox_xyxy_to_cxcywh,
15
+ distance2bbox, find_inside_bboxes, roi2bbox)
16
+
17
+ __all__ = [
18
+ 'bbox_overlaps', 'BboxOverlaps2D', 'BaseAssigner', 'MaxIoUAssigner',
19
+ 'AssignResult', 'BaseSampler', 'PseudoSampler', 'RandomSampler',
20
+ 'InstanceBalancedPosSampler', 'IoUBalancedNegSampler', 'CombinedSampler',
21
+ 'OHEMSampler', 'SamplingResult', 'ScoreHLRSampler', 'build_assigner',
22
+ 'build_sampler', 'bbox_flip', 'bbox_mapping', 'bbox_mapping_back',
23
+ 'bbox2roi', 'roi2bbox', 'bbox2result', 'distance2bbox', 'bbox2distance',
24
+ 'build_bbox_coder', 'BaseBBoxCoder', 'PseudoBBoxCoder',
25
+ 'DeltaXYWHBBoxCoder', 'TBLRBBoxCoder', 'DistancePointBBoxCoder',
26
+ 'CenterRegionAssigner', 'bbox_rescale', 'bbox_cxcywh_to_xyxy',
27
+ 'bbox_xyxy_to_cxcywh', 'RegionAssigner', 'find_inside_bboxes'
28
+ ]
submodules/chartdete/mmdet/core/bbox/assigners/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from .approx_max_iou_assigner import ApproxMaxIoUAssigner
3
+ from .ascend_assign_result import AscendAssignResult
4
+ from .ascend_max_iou_assigner import AscendMaxIoUAssigner
5
+ from .assign_result import AssignResult
6
+ from .atss_assigner import ATSSAssigner
7
+ from .base_assigner import BaseAssigner
8
+ from .center_region_assigner import CenterRegionAssigner
9
+ from .grid_assigner import GridAssigner
10
+ from .hungarian_assigner import HungarianAssigner
11
+ from .mask_hungarian_assigner import MaskHungarianAssigner
12
+ from .max_iou_assigner import MaxIoUAssigner
13
+ from .point_assigner import PointAssigner
14
+ from .region_assigner import RegionAssigner
15
+ from .sim_ota_assigner import SimOTAAssigner
16
+ from .task_aligned_assigner import TaskAlignedAssigner
17
+ from .uniform_assigner import UniformAssigner
18
+
19
+ __all__ = [
20
+ 'BaseAssigner', 'MaxIoUAssigner', 'ApproxMaxIoUAssigner', 'AssignResult',
21
+ 'PointAssigner', 'ATSSAssigner', 'CenterRegionAssigner', 'GridAssigner',
22
+ 'HungarianAssigner', 'RegionAssigner', 'UniformAssigner', 'SimOTAAssigner',
23
+ 'TaskAlignedAssigner', 'MaskHungarianAssigner', 'AscendAssignResult',
24
+ 'AscendMaxIoUAssigner'
25
+ ]
submodules/chartdete/mmdet/core/bbox/assigners/approx_max_iou_assigner.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+ from ..builder import BBOX_ASSIGNERS
5
+ from ..iou_calculators import build_iou_calculator
6
+ from .max_iou_assigner import MaxIoUAssigner
7
+
8
+
9
+ @BBOX_ASSIGNERS.register_module()
10
+ class ApproxMaxIoUAssigner(MaxIoUAssigner):
11
+ """Assign a corresponding gt bbox or background to each bbox.
12
+
13
+ Each proposals will be assigned with an integer indicating the ground truth
14
+ index. (semi-positive index: gt label (0-based), -1: background)
15
+
16
+ - -1: negative sample, no assigned gt
17
+ - semi-positive integer: positive sample, index (0-based) of assigned gt
18
+
19
+ Args:
20
+ pos_iou_thr (float): IoU threshold for positive bboxes.
21
+ neg_iou_thr (float or tuple): IoU threshold for negative bboxes.
22
+ min_pos_iou (float): Minimum iou for a bbox to be considered as a
23
+ positive bbox. Positive samples can have smaller IoU than
24
+ pos_iou_thr due to the 4th step (assign max IoU sample to each gt).
25
+ gt_max_assign_all (bool): Whether to assign all bboxes with the same
26
+ highest overlap with some gt to that gt.
27
+ ignore_iof_thr (float): IoF threshold for ignoring bboxes (if
28
+ `gt_bboxes_ignore` is specified). Negative values mean not
29
+ ignoring any bboxes.
30
+ ignore_wrt_candidates (bool): Whether to compute the iof between
31
+ `bboxes` and `gt_bboxes_ignore`, or the contrary.
32
+ match_low_quality (bool): Whether to allow quality matches. This is
33
+ usually allowed for RPN and single stage detectors, but not allowed
34
+ in the second stage.
35
+ gpu_assign_thr (int): The upper bound of the number of GT for GPU
36
+ assign. When the number of gt is above this threshold, will assign
37
+ on CPU device. Negative values mean not assign on CPU.
38
+ """
39
+
40
+ def __init__(self,
41
+ pos_iou_thr,
42
+ neg_iou_thr,
43
+ min_pos_iou=.0,
44
+ gt_max_assign_all=True,
45
+ ignore_iof_thr=-1,
46
+ ignore_wrt_candidates=True,
47
+ match_low_quality=True,
48
+ gpu_assign_thr=-1,
49
+ iou_calculator=dict(type='BboxOverlaps2D')):
50
+ self.pos_iou_thr = pos_iou_thr
51
+ self.neg_iou_thr = neg_iou_thr
52
+ self.min_pos_iou = min_pos_iou
53
+ self.gt_max_assign_all = gt_max_assign_all
54
+ self.ignore_iof_thr = ignore_iof_thr
55
+ self.ignore_wrt_candidates = ignore_wrt_candidates
56
+ self.gpu_assign_thr = gpu_assign_thr
57
+ self.match_low_quality = match_low_quality
58
+ self.iou_calculator = build_iou_calculator(iou_calculator)
59
+
60
+ def assign(self,
61
+ approxs,
62
+ squares,
63
+ approxs_per_octave,
64
+ gt_bboxes,
65
+ gt_bboxes_ignore=None,
66
+ gt_labels=None):
67
+ """Assign gt to approxs.
68
+
69
+ This method assign a gt bbox to each group of approxs (bboxes),
70
+ each group of approxs is represent by a base approx (bbox) and
71
+ will be assigned with -1, or a semi-positive number.
72
+ background_label (-1) means negative sample,
73
+ semi-positive number is the index (0-based) of assigned gt.
74
+ The assignment is done in following steps, the order matters.
75
+
76
+ 1. assign every bbox to background_label (-1)
77
+ 2. use the max IoU of each group of approxs to assign
78
+ 2. assign proposals whose iou with all gts < neg_iou_thr to background
79
+ 3. for each bbox, if the iou with its nearest gt >= pos_iou_thr,
80
+ assign it to that bbox
81
+ 4. for each gt bbox, assign its nearest proposals (may be more than
82
+ one) to itself
83
+
84
+ Args:
85
+ approxs (Tensor): Bounding boxes to be assigned,
86
+ shape(approxs_per_octave*n, 4).
87
+ squares (Tensor): Base Bounding boxes to be assigned,
88
+ shape(n, 4).
89
+ approxs_per_octave (int): number of approxs per octave
90
+ gt_bboxes (Tensor): Groundtruth boxes, shape (k, 4).
91
+ gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are
92
+ labelled as `ignored`, e.g., crowd boxes in COCO.
93
+ gt_labels (Tensor, optional): Label of gt_bboxes, shape (k, ).
94
+
95
+ Returns:
96
+ :obj:`AssignResult`: The assign result.
97
+ """
98
+ num_squares = squares.size(0)
99
+ num_gts = gt_bboxes.size(0)
100
+
101
+ if num_squares == 0 or num_gts == 0:
102
+ # No predictions and/or truth, return empty assignment
103
+ overlaps = approxs.new(num_gts, num_squares)
104
+ assign_result = self.assign_wrt_overlaps(overlaps, gt_labels)
105
+ return assign_result
106
+
107
+ # re-organize anchors by approxs_per_octave x num_squares
108
+ approxs = torch.transpose(
109
+ approxs.view(num_squares, approxs_per_octave, 4), 0,
110
+ 1).contiguous().view(-1, 4)
111
+ assign_on_cpu = True if (self.gpu_assign_thr > 0) and (
112
+ num_gts > self.gpu_assign_thr) else False
113
+ # compute overlap and assign gt on CPU when number of GT is large
114
+ if assign_on_cpu:
115
+ device = approxs.device
116
+ approxs = approxs.cpu()
117
+ gt_bboxes = gt_bboxes.cpu()
118
+ if gt_bboxes_ignore is not None:
119
+ gt_bboxes_ignore = gt_bboxes_ignore.cpu()
120
+ if gt_labels is not None:
121
+ gt_labels = gt_labels.cpu()
122
+ all_overlaps = self.iou_calculator(approxs, gt_bboxes)
123
+
124
+ overlaps, _ = all_overlaps.view(approxs_per_octave, num_squares,
125
+ num_gts).max(dim=0)
126
+ overlaps = torch.transpose(overlaps, 0, 1)
127
+
128
+ if (self.ignore_iof_thr > 0 and gt_bboxes_ignore is not None
129
+ and gt_bboxes_ignore.numel() > 0 and squares.numel() > 0):
130
+ if self.ignore_wrt_candidates:
131
+ ignore_overlaps = self.iou_calculator(
132
+ squares, gt_bboxes_ignore, mode='iof')
133
+ ignore_max_overlaps, _ = ignore_overlaps.max(dim=1)
134
+ else:
135
+ ignore_overlaps = self.iou_calculator(
136
+ gt_bboxes_ignore, squares, mode='iof')
137
+ ignore_max_overlaps, _ = ignore_overlaps.max(dim=0)
138
+ overlaps[:, ignore_max_overlaps > self.ignore_iof_thr] = -1
139
+
140
+ assign_result = self.assign_wrt_overlaps(overlaps, gt_labels)
141
+ if assign_on_cpu:
142
+ assign_result.gt_inds = assign_result.gt_inds.to(device)
143
+ assign_result.max_overlaps = assign_result.max_overlaps.to(device)
144
+ if assign_result.labels is not None:
145
+ assign_result.labels = assign_result.labels.to(device)
146
+ return assign_result
submodules/chartdete/mmdet/core/bbox/assigners/ascend_assign_result.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from mmdet.utils import util_mixins
3
+
4
+
5
+ class AscendAssignResult(util_mixins.NiceRepr):
6
+ """Stores ascend assignments between predicted and truth boxes.
7
+
8
+ Arguments:
9
+ batch_num_gts (list[int]): the number of truth boxes considered.
10
+ batch_pos_mask (IntTensor): Positive samples mask in all images.
11
+ batch_neg_mask (IntTensor): Negative samples mask in all images.
12
+ batch_max_overlaps (FloatTensor): The max overlaps of all bboxes
13
+ and ground truth boxes.
14
+ batch_anchor_gt_indes(None | LongTensor): The assigned truth
15
+ box index of all anchors.
16
+ batch_anchor_gt_labels(None | LongTensor): The gt labels
17
+ of all anchors
18
+ """
19
+
20
+ def __init__(self,
21
+ batch_num_gts,
22
+ batch_pos_mask,
23
+ batch_neg_mask,
24
+ batch_max_overlaps,
25
+ batch_anchor_gt_indes=None,
26
+ batch_anchor_gt_labels=None):
27
+ self.batch_num_gts = batch_num_gts
28
+ self.batch_pos_mask = batch_pos_mask
29
+ self.batch_neg_mask = batch_neg_mask
30
+ self.batch_max_overlaps = batch_max_overlaps
31
+ self.batch_anchor_gt_indes = batch_anchor_gt_indes
32
+ self.batch_anchor_gt_labels = batch_anchor_gt_labels
33
+ # Interface for possible user-defined properties
34
+ self._extra_properties = {}
submodules/chartdete/mmdet/core/bbox/assigners/ascend_max_iou_assigner.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+ from ....utils import masked_fill
5
+ from ..builder import BBOX_ASSIGNERS
6
+ from ..iou_calculators import build_iou_calculator
7
+ from .ascend_assign_result import AscendAssignResult
8
+ from .base_assigner import BaseAssigner
9
+
10
+
11
+ @BBOX_ASSIGNERS.register_module()
12
+ class AscendMaxIoUAssigner(BaseAssigner):
13
+ """Assign a corresponding gt bbox or background to each bbox.
14
+
15
+ Each proposals will be assigned with `-1`, or a semi-positive integer
16
+ indicating the ground truth index.
17
+
18
+ - -1: negative sample, no assigned gt
19
+ - semi-positive integer: positive sample, index (0-based) of assigned gt
20
+
21
+ Args:
22
+ pos_iou_thr (float): IoU threshold for positive bboxes.
23
+ neg_iou_thr (float or tuple): IoU threshold for negative bboxes.
24
+ min_pos_iou (float): Minimum iou for a bbox to be considered as a
25
+ positive bbox. Positive samples can have smaller IoU than
26
+ pos_iou_thr due to the 4th step (assign max IoU sample to each gt).
27
+ `min_pos_iou` is set to avoid assigning bboxes that have extremely
28
+ small iou with GT as positive samples. It brings about 0.3 mAP
29
+ improvements in 1x schedule but does not affect the performance of
30
+ 3x schedule. More comparisons can be found in
31
+ `PR #7464 <https://github.com/open-mmlab/mmdetection/pull/7464>`_.
32
+ gt_max_assign_all (bool): Whether to assign all bboxes with the same
33
+ highest overlap with some gt to that gt.
34
+ ignore_iof_thr (float): IoF threshold for ignoring bboxes (if
35
+ `gt_bboxes_ignore` is specified). Negative values mean not
36
+ ignoring any bboxes.
37
+ ignore_wrt_candidates (bool): Whether to compute the iof between
38
+ `bboxes` and `gt_bboxes_ignore`, or the contrary.
39
+ match_low_quality (bool): Whether to allow low quality matches. This is
40
+ usually allowed for RPN and single stage detectors, but not allowed
41
+ in the second stage. Details are demonstrated in Step 4.
42
+ gpu_assign_thr (int): The upper bound of the number of GT for GPU
43
+ assign. When the number of gt is above this threshold, will assign
44
+ on CPU device. Negative values mean not assign on CPU.
45
+ """
46
+
47
+ def __init__(self,
48
+ pos_iou_thr,
49
+ neg_iou_thr,
50
+ min_pos_iou=.0,
51
+ gt_max_assign_all=True,
52
+ ignore_iof_thr=-1,
53
+ ignore_wrt_candidates=True,
54
+ match_low_quality=True,
55
+ gpu_assign_thr=-1,
56
+ iou_calculator=dict(type='BboxOverlaps2D')):
57
+ self.pos_iou_thr = pos_iou_thr
58
+ self.neg_iou_thr = neg_iou_thr
59
+ self.min_pos_iou = min_pos_iou
60
+ self.gt_max_assign_all = gt_max_assign_all
61
+ self.ignore_iof_thr = ignore_iof_thr
62
+ self.ignore_wrt_candidates = ignore_wrt_candidates
63
+ self.gpu_assign_thr = gpu_assign_thr
64
+ self.match_low_quality = match_low_quality
65
+ self.iou_calculator = build_iou_calculator(iou_calculator)
66
+
67
+ def assign(self,
68
+ batch_bboxes,
69
+ batch_gt_bboxes,
70
+ batch_gt_bboxes_ignore=None,
71
+ batch_gt_labels=None,
72
+ batch_bboxes_ignore_mask=None,
73
+ batch_num_gts=None):
74
+ """Assign gt to bboxes.
75
+
76
+ Args:
77
+ batch_bboxes (Tensor): Bounding boxes to be assigned,
78
+ shape(b, n, 4).
79
+ batch_gt_bboxes (Tensor): Ground truth boxes,
80
+ shape (b, k, 4).
81
+ batch_gt_bboxes_ignore (Tensor, optional): Ground truth
82
+ bboxes that are labelled as `ignored`,
83
+ e.g., crowd boxes in COCO.
84
+ batch_gt_labels (Tensor, optional): Label of gt_bboxes,
85
+ shape (b, k, ).
86
+ batch_bboxes_ignore_mask: (b, n)
87
+ batch_num_gts:(b, )
88
+ Returns:
89
+ :obj:`AssignResult`: The assign result.
90
+ """
91
+ batch_overlaps = self.iou_calculator(batch_gt_bboxes, batch_bboxes)
92
+ batch_overlaps = masked_fill(
93
+ batch_overlaps,
94
+ batch_bboxes_ignore_mask.unsqueeze(1).float(),
95
+ -1,
96
+ neg=True)
97
+ if self.ignore_iof_thr > 0 and batch_gt_bboxes_ignore is not None:
98
+ if self.ignore_wrt_candidates:
99
+ batch_ignore_overlaps = self.iou_calculator(
100
+ batch_bboxes, batch_gt_bboxes_ignore, mode='iof')
101
+ batch_ignore_overlaps = masked_fill(batch_ignore_overlaps,
102
+ batch_bboxes_ignore_mask,
103
+ -1)
104
+ batch_ignore_max_overlaps, _ = batch_ignore_overlaps.max(dim=2)
105
+ else:
106
+ batch_ignore_overlaps = self.iou_calculator(
107
+ batch_gt_bboxes_ignore, batch_bboxes, mode='iof')
108
+ batch_ignore_overlaps = masked_fill(batch_ignore_overlaps,
109
+ batch_bboxes_ignore_mask,
110
+ -1)
111
+ batch_ignore_max_overlaps, _ = \
112
+ batch_ignore_overlaps.max(dim=1)
113
+ batch_ignore_mask = \
114
+ batch_ignore_max_overlaps > self.ignore_iof_thr
115
+ batch_overlaps = masked_fill(batch_overlaps, batch_ignore_mask, -1)
116
+ batch_assign_result = self.batch_assign_wrt_overlaps(
117
+ batch_overlaps, batch_gt_labels, batch_num_gts)
118
+ return batch_assign_result
119
+
120
+ def batch_assign_wrt_overlaps(self,
121
+ batch_overlaps,
122
+ batch_gt_labels=None,
123
+ batch_num_gts=None):
124
+ num_images, num_gts, num_bboxes = batch_overlaps.size()
125
+ batch_max_overlaps, batch_argmax_overlaps = batch_overlaps.max(dim=1)
126
+ if isinstance(self.neg_iou_thr, float):
127
+ batch_neg_mask = \
128
+ ((batch_max_overlaps >= 0)
129
+ & (batch_max_overlaps < self.neg_iou_thr)).int()
130
+ elif isinstance(self.neg_iou_thr, tuple):
131
+ assert len(self.neg_iou_thr) == 2
132
+ batch_neg_mask = \
133
+ ((batch_max_overlaps >= self.neg_iou_thr[0])
134
+ & (batch_max_overlaps < self.neg_iou_thr[1])).int()
135
+ else:
136
+ batch_neg_mask = torch.zeros(
137
+ batch_max_overlaps.size(),
138
+ dtype=torch.int,
139
+ device=batch_max_overlaps.device)
140
+ batch_pos_mask = (batch_max_overlaps >= self.pos_iou_thr).int()
141
+ if self.match_low_quality:
142
+ batch_gt_max_overlaps, batch_gt_argmax_overlaps = \
143
+ batch_overlaps.max(dim=2)
144
+ batch_index_bool = (batch_gt_max_overlaps >= self.min_pos_iou) & \
145
+ (batch_gt_max_overlaps > 0)
146
+ if self.gt_max_assign_all:
147
+ pos_inds_low_quality = \
148
+ (batch_overlaps == batch_gt_max_overlaps.unsqueeze(2)) & \
149
+ batch_index_bool.unsqueeze(2)
150
+ for i in range(num_gts):
151
+ pos_inds_low_quality_gt = pos_inds_low_quality[:, i, :]
152
+ batch_argmax_overlaps[pos_inds_low_quality_gt] = i
153
+ batch_pos_mask[pos_inds_low_quality_gt] = 1
154
+ else:
155
+ index_temp = torch.arange(
156
+ 0, num_gts, device=batch_max_overlaps.device)
157
+ for index_image in range(num_images):
158
+ gt_argmax_overlaps = batch_gt_argmax_overlaps[index_image]
159
+ index_bool = batch_index_bool[index_image]
160
+ pos_inds_low_quality = gt_argmax_overlaps[index_bool]
161
+ batch_argmax_overlaps[index_image][pos_inds_low_quality] \
162
+ = index_temp[index_bool]
163
+ batch_pos_mask[index_image][pos_inds_low_quality] = 1
164
+ batch_neg_mask = batch_neg_mask * (1 - batch_pos_mask)
165
+ if batch_gt_labels is not None:
166
+ batch_anchor_gt_labels = torch.zeros((num_images, num_bboxes),
167
+ dtype=batch_gt_labels.dtype,
168
+ device=batch_gt_labels.device)
169
+ for index_image in range(num_images):
170
+ batch_anchor_gt_labels[index_image] = torch.index_select(
171
+ batch_gt_labels[index_image], 0,
172
+ batch_argmax_overlaps[index_image])
173
+ else:
174
+ batch_anchor_gt_labels = None
175
+ return AscendAssignResult(batch_num_gts, batch_pos_mask,
176
+ batch_neg_mask, batch_max_overlaps,
177
+ batch_argmax_overlaps,
178
+ batch_anchor_gt_labels)
submodules/chartdete/mmdet/core/bbox/assigners/assign_result.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+ from mmdet.utils import util_mixins
5
+
6
+
7
+ class AssignResult(util_mixins.NiceRepr):
8
+ """Stores assignments between predicted and truth boxes.
9
+
10
+ Attributes:
11
+ num_gts (int): the number of truth boxes considered when computing this
12
+ assignment
13
+
14
+ gt_inds (LongTensor): for each predicted box indicates the 1-based
15
+ index of the assigned truth box. 0 means unassigned and -1 means
16
+ ignore.
17
+
18
+ max_overlaps (FloatTensor): the iou between the predicted box and its
19
+ assigned truth box.
20
+
21
+ labels (None | LongTensor): If specified, for each predicted box
22
+ indicates the category label of the assigned truth box.
23
+
24
+ Example:
25
+ >>> # An assign result between 4 predicted boxes and 9 true boxes
26
+ >>> # where only two boxes were assigned.
27
+ >>> num_gts = 9
28
+ >>> max_overlaps = torch.LongTensor([0, .5, .9, 0])
29
+ >>> gt_inds = torch.LongTensor([-1, 1, 2, 0])
30
+ >>> labels = torch.LongTensor([0, 3, 4, 0])
31
+ >>> self = AssignResult(num_gts, gt_inds, max_overlaps, labels)
32
+ >>> print(str(self)) # xdoctest: +IGNORE_WANT
33
+ <AssignResult(num_gts=9, gt_inds.shape=(4,), max_overlaps.shape=(4,),
34
+ labels.shape=(4,))>
35
+ >>> # Force addition of gt labels (when adding gt as proposals)
36
+ >>> new_labels = torch.LongTensor([3, 4, 5])
37
+ >>> self.add_gt_(new_labels)
38
+ >>> print(str(self)) # xdoctest: +IGNORE_WANT
39
+ <AssignResult(num_gts=9, gt_inds.shape=(7,), max_overlaps.shape=(7,),
40
+ labels.shape=(7,))>
41
+ """
42
+
43
+ def __init__(self, num_gts, gt_inds, max_overlaps, labels=None):
44
+ self.num_gts = num_gts
45
+ self.gt_inds = gt_inds
46
+ self.max_overlaps = max_overlaps
47
+ self.labels = labels
48
+ # Interface for possible user-defined properties
49
+ self._extra_properties = {}
50
+
51
+ @property
52
+ def num_preds(self):
53
+ """int: the number of predictions in this assignment"""
54
+ return len(self.gt_inds)
55
+
56
+ def set_extra_property(self, key, value):
57
+ """Set user-defined new property."""
58
+ assert key not in self.info
59
+ self._extra_properties[key] = value
60
+
61
+ def get_extra_property(self, key):
62
+ """Get user-defined property."""
63
+ return self._extra_properties.get(key, None)
64
+
65
+ @property
66
+ def info(self):
67
+ """dict: a dictionary of info about the object"""
68
+ basic_info = {
69
+ 'num_gts': self.num_gts,
70
+ 'num_preds': self.num_preds,
71
+ 'gt_inds': self.gt_inds,
72
+ 'max_overlaps': self.max_overlaps,
73
+ 'labels': self.labels,
74
+ }
75
+ basic_info.update(self._extra_properties)
76
+ return basic_info
77
+
78
+ def __nice__(self):
79
+ """str: a "nice" summary string describing this assign result"""
80
+ parts = []
81
+ parts.append(f'num_gts={self.num_gts!r}')
82
+ if self.gt_inds is None:
83
+ parts.append(f'gt_inds={self.gt_inds!r}')
84
+ else:
85
+ parts.append(f'gt_inds.shape={tuple(self.gt_inds.shape)!r}')
86
+ if self.max_overlaps is None:
87
+ parts.append(f'max_overlaps={self.max_overlaps!r}')
88
+ else:
89
+ parts.append('max_overlaps.shape='
90
+ f'{tuple(self.max_overlaps.shape)!r}')
91
+ if self.labels is None:
92
+ parts.append(f'labels={self.labels!r}')
93
+ else:
94
+ parts.append(f'labels.shape={tuple(self.labels.shape)!r}')
95
+ return ', '.join(parts)
96
+
97
+ @classmethod
98
+ def random(cls, **kwargs):
99
+ """Create random AssignResult for tests or debugging.
100
+
101
+ Args:
102
+ num_preds: number of predicted boxes
103
+ num_gts: number of true boxes
104
+ p_ignore (float): probability of a predicted box assigned to an
105
+ ignored truth
106
+ p_assigned (float): probability of a predicted box not being
107
+ assigned
108
+ p_use_label (float | bool): with labels or not
109
+ rng (None | int | numpy.random.RandomState): seed or state
110
+
111
+ Returns:
112
+ :obj:`AssignResult`: Randomly generated assign results.
113
+
114
+ Example:
115
+ >>> from mmdet.core.bbox.assigners.assign_result import * # NOQA
116
+ >>> self = AssignResult.random()
117
+ >>> print(self.info)
118
+ """
119
+ from mmdet.core.bbox import demodata
120
+ rng = demodata.ensure_rng(kwargs.get('rng', None))
121
+
122
+ num_gts = kwargs.get('num_gts', None)
123
+ num_preds = kwargs.get('num_preds', None)
124
+ p_ignore = kwargs.get('p_ignore', 0.3)
125
+ p_assigned = kwargs.get('p_assigned', 0.7)
126
+ p_use_label = kwargs.get('p_use_label', 0.5)
127
+ num_classes = kwargs.get('p_use_label', 3)
128
+
129
+ if num_gts is None:
130
+ num_gts = rng.randint(0, 8)
131
+ if num_preds is None:
132
+ num_preds = rng.randint(0, 16)
133
+
134
+ if num_gts == 0:
135
+ max_overlaps = torch.zeros(num_preds, dtype=torch.float32)
136
+ gt_inds = torch.zeros(num_preds, dtype=torch.int64)
137
+ if p_use_label is True or p_use_label < rng.rand():
138
+ labels = torch.zeros(num_preds, dtype=torch.int64)
139
+ else:
140
+ labels = None
141
+ else:
142
+ import numpy as np
143
+
144
+ # Create an overlap for each predicted box
145
+ max_overlaps = torch.from_numpy(rng.rand(num_preds))
146
+
147
+ # Construct gt_inds for each predicted box
148
+ is_assigned = torch.from_numpy(rng.rand(num_preds) < p_assigned)
149
+ # maximum number of assignments constraints
150
+ n_assigned = min(num_preds, min(num_gts, is_assigned.sum()))
151
+
152
+ assigned_idxs = np.where(is_assigned)[0]
153
+ rng.shuffle(assigned_idxs)
154
+ assigned_idxs = assigned_idxs[0:n_assigned]
155
+ assigned_idxs.sort()
156
+
157
+ is_assigned[:] = 0
158
+ is_assigned[assigned_idxs] = True
159
+
160
+ is_ignore = torch.from_numpy(
161
+ rng.rand(num_preds) < p_ignore) & is_assigned
162
+
163
+ gt_inds = torch.zeros(num_preds, dtype=torch.int64)
164
+
165
+ true_idxs = np.arange(num_gts)
166
+ rng.shuffle(true_idxs)
167
+ true_idxs = torch.from_numpy(true_idxs)
168
+ gt_inds[is_assigned] = true_idxs[:n_assigned].long()
169
+
170
+ gt_inds = torch.from_numpy(
171
+ rng.randint(1, num_gts + 1, size=num_preds))
172
+ gt_inds[is_ignore] = -1
173
+ gt_inds[~is_assigned] = 0
174
+ max_overlaps[~is_assigned] = 0
175
+
176
+ if p_use_label is True or p_use_label < rng.rand():
177
+ if num_classes == 0:
178
+ labels = torch.zeros(num_preds, dtype=torch.int64)
179
+ else:
180
+ labels = torch.from_numpy(
181
+ # remind that we set FG labels to [0, num_class-1]
182
+ # since mmdet v2.0
183
+ # BG cat_id: num_class
184
+ rng.randint(0, num_classes, size=num_preds))
185
+ labels[~is_assigned] = 0
186
+ else:
187
+ labels = None
188
+
189
+ self = cls(num_gts, gt_inds, max_overlaps, labels)
190
+ return self
191
+
192
+ def add_gt_(self, gt_labels):
193
+ """Add ground truth as assigned results.
194
+
195
+ Args:
196
+ gt_labels (torch.Tensor): Labels of gt boxes
197
+ """
198
+ self_inds = torch.arange(
199
+ 1, len(gt_labels) + 1, dtype=torch.long, device=gt_labels.device)
200
+ self.gt_inds = torch.cat([self_inds, self.gt_inds])
201
+
202
+ self.max_overlaps = torch.cat(
203
+ [self.max_overlaps.new_ones(len(gt_labels)), self.max_overlaps])
204
+
205
+ if self.labels is not None:
206
+ self.labels = torch.cat([gt_labels, self.labels])
submodules/chartdete/mmdet/core/bbox/assigners/atss_assigner.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import warnings
3
+
4
+ import torch
5
+
6
+ from ..builder import BBOX_ASSIGNERS
7
+ from ..iou_calculators import build_iou_calculator
8
+ from .assign_result import AssignResult
9
+ from .base_assigner import BaseAssigner
10
+
11
+
12
+ @BBOX_ASSIGNERS.register_module()
13
+ class ATSSAssigner(BaseAssigner):
14
+ """Assign a corresponding gt bbox or background to each bbox.
15
+
16
+ Each proposals will be assigned with `0` or a positive integer
17
+ indicating the ground truth index.
18
+
19
+ - 0: negative sample, no assigned gt
20
+ - positive integer: positive sample, index (1-based) of assigned gt
21
+
22
+ If ``alpha`` is not None, it means that the dynamic cost
23
+ ATSSAssigner is adopted, which is currently only used in the DDOD.
24
+
25
+ Args:
26
+ topk (float): number of bbox selected in each level
27
+ """
28
+
29
+ def __init__(self,
30
+ topk,
31
+ alpha=None,
32
+ iou_calculator=dict(type='BboxOverlaps2D'),
33
+ ignore_iof_thr=-1):
34
+ self.topk = topk
35
+ self.alpha = alpha
36
+ self.iou_calculator = build_iou_calculator(iou_calculator)
37
+ self.ignore_iof_thr = ignore_iof_thr
38
+
39
+ """Assign a corresponding gt bbox or background to each bbox.
40
+
41
+ Args:
42
+ topk (int): number of bbox selected in each level.
43
+ alpha (float): param of cost rate for each proposal only in DDOD.
44
+ Default None.
45
+ iou_calculator (dict): builder of IoU calculator.
46
+ Default dict(type='BboxOverlaps2D').
47
+ ignore_iof_thr (int): whether ignore max overlaps or not.
48
+ Default -1 (1 or -1).
49
+ """
50
+
51
+ # https://github.com/sfzhang15/ATSS/blob/master/atss_core/modeling/rpn/atss/loss.py
52
+ def assign(self,
53
+ bboxes,
54
+ num_level_bboxes,
55
+ gt_bboxes,
56
+ gt_bboxes_ignore=None,
57
+ gt_labels=None,
58
+ cls_scores=None,
59
+ bbox_preds=None):
60
+ """Assign gt to bboxes.
61
+
62
+ The assignment is done in following steps
63
+
64
+ 1. compute iou between all bbox (bbox of all pyramid levels) and gt
65
+ 2. compute center distance between all bbox and gt
66
+ 3. on each pyramid level, for each gt, select k bbox whose center
67
+ are closest to the gt center, so we total select k*l bbox as
68
+ candidates for each gt
69
+ 4. get corresponding iou for the these candidates, and compute the
70
+ mean and std, set mean + std as the iou threshold
71
+ 5. select these candidates whose iou are greater than or equal to
72
+ the threshold as positive
73
+ 6. limit the positive sample's center in gt
74
+
75
+ If ``alpha`` is not None, and ``cls_scores`` and `bbox_preds`
76
+ are not None, the overlaps calculation in the first step
77
+ will also include dynamic cost, which is currently only used in
78
+ the DDOD.
79
+
80
+ Args:
81
+ bboxes (Tensor): Bounding boxes to be assigned, shape(n, 4).
82
+ num_level_bboxes (List): num of bboxes in each level
83
+ gt_bboxes (Tensor): Groundtruth boxes, shape (k, 4).
84
+ gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are
85
+ labelled as `ignored`, e.g., crowd boxes in COCO. Default None.
86
+ gt_labels (Tensor, optional): Label of gt_bboxes, shape (k, ).
87
+ cls_scores (list[Tensor]): Classification scores for all scale
88
+ levels, each is a 4D-tensor, the channels number is
89
+ num_base_priors * num_classes. Default None.
90
+ bbox_preds (list[Tensor]): Box energies / deltas for all scale
91
+ levels, each is a 4D-tensor, the channels number is
92
+ num_base_priors * 4. Default None.
93
+
94
+ Returns:
95
+ :obj:`AssignResult`: The assign result.
96
+ """
97
+ INF = 100000000
98
+ bboxes = bboxes[:, :4]
99
+ num_gt, num_bboxes = gt_bboxes.size(0), bboxes.size(0)
100
+
101
+ message = 'Invalid alpha parameter because cls_scores or ' \
102
+ 'bbox_preds are None. If you want to use the ' \
103
+ 'cost-based ATSSAssigner, please set cls_scores, ' \
104
+ 'bbox_preds and self.alpha at the same time. '
105
+
106
+ if self.alpha is None:
107
+ # ATSSAssigner
108
+ overlaps = self.iou_calculator(bboxes, gt_bboxes)
109
+ if cls_scores is not None or bbox_preds is not None:
110
+ warnings.warn(message)
111
+ else:
112
+ # Dynamic cost ATSSAssigner in DDOD
113
+ assert cls_scores is not None and bbox_preds is not None, message
114
+
115
+ # compute cls cost for bbox and GT
116
+ cls_cost = torch.sigmoid(cls_scores[:, gt_labels])
117
+
118
+ # compute iou between all bbox and gt
119
+ overlaps = self.iou_calculator(bbox_preds, gt_bboxes)
120
+
121
+ # make sure that we are in element-wise multiplication
122
+ assert cls_cost.shape == overlaps.shape
123
+
124
+ # overlaps is actually a cost matrix
125
+ overlaps = cls_cost**(1 - self.alpha) * overlaps**self.alpha
126
+
127
+ # assign 0 by default
128
+ assigned_gt_inds = overlaps.new_full((num_bboxes, ),
129
+ 0,
130
+ dtype=torch.long)
131
+
132
+ if num_gt == 0 or num_bboxes == 0:
133
+ # No ground truth or boxes, return empty assignment
134
+ max_overlaps = overlaps.new_zeros((num_bboxes, ))
135
+ if num_gt == 0:
136
+ # No truth, assign everything to background
137
+ assigned_gt_inds[:] = 0
138
+ if gt_labels is None:
139
+ assigned_labels = None
140
+ else:
141
+ assigned_labels = overlaps.new_full((num_bboxes, ),
142
+ -1,
143
+ dtype=torch.long)
144
+ return AssignResult(
145
+ num_gt, assigned_gt_inds, max_overlaps, labels=assigned_labels)
146
+
147
+ # compute center distance between all bbox and gt
148
+ gt_cx = (gt_bboxes[:, 0] + gt_bboxes[:, 2]) / 2.0
149
+ gt_cy = (gt_bboxes[:, 1] + gt_bboxes[:, 3]) / 2.0
150
+ gt_points = torch.stack((gt_cx, gt_cy), dim=1)
151
+
152
+ bboxes_cx = (bboxes[:, 0] + bboxes[:, 2]) / 2.0
153
+ bboxes_cy = (bboxes[:, 1] + bboxes[:, 3]) / 2.0
154
+ bboxes_points = torch.stack((bboxes_cx, bboxes_cy), dim=1)
155
+
156
+ distances = (bboxes_points[:, None, :] -
157
+ gt_points[None, :, :]).pow(2).sum(-1).sqrt()
158
+
159
+ if (self.ignore_iof_thr > 0 and gt_bboxes_ignore is not None
160
+ and gt_bboxes_ignore.numel() > 0 and bboxes.numel() > 0):
161
+ ignore_overlaps = self.iou_calculator(
162
+ bboxes, gt_bboxes_ignore, mode='iof')
163
+ ignore_max_overlaps, _ = ignore_overlaps.max(dim=1)
164
+ ignore_idxs = ignore_max_overlaps > self.ignore_iof_thr
165
+ distances[ignore_idxs, :] = INF
166
+ assigned_gt_inds[ignore_idxs] = -1
167
+
168
+ # Selecting candidates based on the center distance
169
+ candidate_idxs = []
170
+ start_idx = 0
171
+ for level, bboxes_per_level in enumerate(num_level_bboxes):
172
+ # on each pyramid level, for each gt,
173
+ # select k bbox whose center are closest to the gt center
174
+ end_idx = start_idx + bboxes_per_level
175
+ distances_per_level = distances[start_idx:end_idx, :]
176
+ selectable_k = min(self.topk, bboxes_per_level)
177
+
178
+ _, topk_idxs_per_level = distances_per_level.topk(
179
+ selectable_k, dim=0, largest=False)
180
+ candidate_idxs.append(topk_idxs_per_level + start_idx)
181
+ start_idx = end_idx
182
+ candidate_idxs = torch.cat(candidate_idxs, dim=0)
183
+
184
+ # get corresponding iou for the these candidates, and compute the
185
+ # mean and std, set mean + std as the iou threshold
186
+ candidate_overlaps = overlaps[candidate_idxs, torch.arange(num_gt)]
187
+ overlaps_mean_per_gt = candidate_overlaps.mean(0)
188
+ overlaps_std_per_gt = candidate_overlaps.std(0)
189
+ overlaps_thr_per_gt = overlaps_mean_per_gt + overlaps_std_per_gt
190
+
191
+ is_pos = candidate_overlaps >= overlaps_thr_per_gt[None, :]
192
+
193
+ # limit the positive sample's center in gt
194
+ for gt_idx in range(num_gt):
195
+ candidate_idxs[:, gt_idx] += gt_idx * num_bboxes
196
+ ep_bboxes_cx = bboxes_cx.view(1, -1).expand(
197
+ num_gt, num_bboxes).contiguous().view(-1)
198
+ ep_bboxes_cy = bboxes_cy.view(1, -1).expand(
199
+ num_gt, num_bboxes).contiguous().view(-1)
200
+ candidate_idxs = candidate_idxs.view(-1)
201
+
202
+ # calculate the left, top, right, bottom distance between positive
203
+ # bbox center and gt side
204
+ l_ = ep_bboxes_cx[candidate_idxs].view(-1, num_gt) - gt_bboxes[:, 0]
205
+ t_ = ep_bboxes_cy[candidate_idxs].view(-1, num_gt) - gt_bboxes[:, 1]
206
+ r_ = gt_bboxes[:, 2] - ep_bboxes_cx[candidate_idxs].view(-1, num_gt)
207
+ b_ = gt_bboxes[:, 3] - ep_bboxes_cy[candidate_idxs].view(-1, num_gt)
208
+ is_in_gts = torch.stack([l_, t_, r_, b_], dim=1).min(dim=1)[0] > 0.01
209
+
210
+ is_pos = is_pos & is_in_gts
211
+
212
+ # if an anchor box is assigned to multiple gts,
213
+ # the one with the highest IoU will be selected.
214
+ overlaps_inf = torch.full_like(overlaps,
215
+ -INF).t().contiguous().view(-1)
216
+ index = candidate_idxs.view(-1)[is_pos.view(-1)]
217
+ overlaps_inf[index] = overlaps.t().contiguous().view(-1)[index]
218
+ overlaps_inf = overlaps_inf.view(num_gt, -1).t()
219
+
220
+ max_overlaps, argmax_overlaps = overlaps_inf.max(dim=1)
221
+ assigned_gt_inds[
222
+ max_overlaps != -INF] = argmax_overlaps[max_overlaps != -INF] + 1
223
+
224
+ if gt_labels is not None:
225
+ assigned_labels = assigned_gt_inds.new_full((num_bboxes, ), -1)
226
+ pos_inds = torch.nonzero(
227
+ assigned_gt_inds > 0, as_tuple=False).squeeze()
228
+ if pos_inds.numel() > 0:
229
+ assigned_labels[pos_inds] = gt_labels[
230
+ assigned_gt_inds[pos_inds] - 1]
231
+ else:
232
+ assigned_labels = None
233
+ return AssignResult(
234
+ num_gt, assigned_gt_inds, max_overlaps, labels=assigned_labels)
submodules/chartdete/mmdet/core/bbox/assigners/base_assigner.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from abc import ABCMeta, abstractmethod
3
+
4
+
5
+ class BaseAssigner(metaclass=ABCMeta):
6
+ """Base assigner that assigns boxes to ground truth boxes."""
7
+
8
+ @abstractmethod
9
+ def assign(self, bboxes, gt_bboxes, gt_bboxes_ignore=None, gt_labels=None):
10
+ """Assign boxes to either a ground truth boxes or a negative boxes."""
submodules/chartdete/mmdet/core/bbox/assigners/center_region_assigner.py ADDED
@@ -0,0 +1,336 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+ from ..builder import BBOX_ASSIGNERS
5
+ from ..iou_calculators import build_iou_calculator
6
+ from .assign_result import AssignResult
7
+ from .base_assigner import BaseAssigner
8
+
9
+
10
+ def scale_boxes(bboxes, scale):
11
+ """Expand an array of boxes by a given scale.
12
+
13
+ Args:
14
+ bboxes (Tensor): Shape (m, 4)
15
+ scale (float): The scale factor of bboxes
16
+
17
+ Returns:
18
+ (Tensor): Shape (m, 4). Scaled bboxes
19
+ """
20
+ assert bboxes.size(1) == 4
21
+ w_half = (bboxes[:, 2] - bboxes[:, 0]) * .5
22
+ h_half = (bboxes[:, 3] - bboxes[:, 1]) * .5
23
+ x_c = (bboxes[:, 2] + bboxes[:, 0]) * .5
24
+ y_c = (bboxes[:, 3] + bboxes[:, 1]) * .5
25
+
26
+ w_half *= scale
27
+ h_half *= scale
28
+
29
+ boxes_scaled = torch.zeros_like(bboxes)
30
+ boxes_scaled[:, 0] = x_c - w_half
31
+ boxes_scaled[:, 2] = x_c + w_half
32
+ boxes_scaled[:, 1] = y_c - h_half
33
+ boxes_scaled[:, 3] = y_c + h_half
34
+ return boxes_scaled
35
+
36
+
37
+ def is_located_in(points, bboxes):
38
+ """Are points located in bboxes.
39
+
40
+ Args:
41
+ points (Tensor): Points, shape: (m, 2).
42
+ bboxes (Tensor): Bounding boxes, shape: (n, 4).
43
+
44
+ Return:
45
+ Tensor: Flags indicating if points are located in bboxes, shape: (m, n).
46
+ """
47
+ assert points.size(1) == 2
48
+ assert bboxes.size(1) == 4
49
+ return (points[:, 0].unsqueeze(1) > bboxes[:, 0].unsqueeze(0)) & \
50
+ (points[:, 0].unsqueeze(1) < bboxes[:, 2].unsqueeze(0)) & \
51
+ (points[:, 1].unsqueeze(1) > bboxes[:, 1].unsqueeze(0)) & \
52
+ (points[:, 1].unsqueeze(1) < bboxes[:, 3].unsqueeze(0))
53
+
54
+
55
+ def bboxes_area(bboxes):
56
+ """Compute the area of an array of bboxes.
57
+
58
+ Args:
59
+ bboxes (Tensor): The coordinates ox bboxes. Shape: (m, 4)
60
+
61
+ Returns:
62
+ Tensor: Area of the bboxes. Shape: (m, )
63
+ """
64
+ assert bboxes.size(1) == 4
65
+ w = (bboxes[:, 2] - bboxes[:, 0])
66
+ h = (bboxes[:, 3] - bboxes[:, 1])
67
+ areas = w * h
68
+ return areas
69
+
70
+
71
+ @BBOX_ASSIGNERS.register_module()
72
+ class CenterRegionAssigner(BaseAssigner):
73
+ """Assign pixels at the center region of a bbox as positive.
74
+
75
+ Each proposals will be assigned with `-1`, `0`, or a positive integer
76
+ indicating the ground truth index.
77
+ - -1: negative samples
78
+ - semi-positive numbers: positive sample, index (0-based) of assigned gt
79
+
80
+ Args:
81
+ pos_scale (float): Threshold within which pixels are
82
+ labelled as positive.
83
+ neg_scale (float): Threshold above which pixels are
84
+ labelled as positive.
85
+ min_pos_iof (float): Minimum iof of a pixel with a gt to be
86
+ labelled as positive. Default: 1e-2
87
+ ignore_gt_scale (float): Threshold within which the pixels
88
+ are ignored when the gt is labelled as shadowed. Default: 0.5
89
+ foreground_dominate (bool): If True, the bbox will be assigned as
90
+ positive when a gt's kernel region overlaps with another's shadowed
91
+ (ignored) region, otherwise it is set as ignored. Default to False.
92
+ """
93
+
94
+ def __init__(self,
95
+ pos_scale,
96
+ neg_scale,
97
+ min_pos_iof=1e-2,
98
+ ignore_gt_scale=0.5,
99
+ foreground_dominate=False,
100
+ iou_calculator=dict(type='BboxOverlaps2D')):
101
+ self.pos_scale = pos_scale
102
+ self.neg_scale = neg_scale
103
+ self.min_pos_iof = min_pos_iof
104
+ self.ignore_gt_scale = ignore_gt_scale
105
+ self.foreground_dominate = foreground_dominate
106
+ self.iou_calculator = build_iou_calculator(iou_calculator)
107
+
108
+ def get_gt_priorities(self, gt_bboxes):
109
+ """Get gt priorities according to their areas.
110
+
111
+ Smaller gt has higher priority.
112
+
113
+ Args:
114
+ gt_bboxes (Tensor): Ground truth boxes, shape (k, 4).
115
+
116
+ Returns:
117
+ Tensor: The priority of gts so that gts with larger priority is \
118
+ more likely to be assigned. Shape (k, )
119
+ """
120
+ gt_areas = bboxes_area(gt_bboxes)
121
+ # Rank all gt bbox areas. Smaller objects has larger priority
122
+ _, sort_idx = gt_areas.sort(descending=True)
123
+ sort_idx = sort_idx.argsort()
124
+ return sort_idx
125
+
126
+ def assign(self, bboxes, gt_bboxes, gt_bboxes_ignore=None, gt_labels=None):
127
+ """Assign gt to bboxes.
128
+
129
+ This method assigns gts to every bbox (proposal/anchor), each bbox \
130
+ will be assigned with -1, or a semi-positive number. -1 means \
131
+ negative sample, semi-positive number is the index (0-based) of \
132
+ assigned gt.
133
+
134
+ Args:
135
+ bboxes (Tensor): Bounding boxes to be assigned, shape(n, 4).
136
+ gt_bboxes (Tensor): Groundtruth boxes, shape (k, 4).
137
+ gt_bboxes_ignore (tensor, optional): Ground truth bboxes that are
138
+ labelled as `ignored`, e.g., crowd boxes in COCO.
139
+ gt_labels (tensor, optional): Label of gt_bboxes, shape (num_gts,).
140
+
141
+ Returns:
142
+ :obj:`AssignResult`: The assigned result. Note that \
143
+ shadowed_labels of shape (N, 2) is also added as an \
144
+ `assign_result` attribute. `shadowed_labels` is a tensor \
145
+ composed of N pairs of anchor_ind, class_label], where N \
146
+ is the number of anchors that lie in the outer region of a \
147
+ gt, anchor_ind is the shadowed anchor index and class_label \
148
+ is the shadowed class label.
149
+
150
+ Example:
151
+ >>> self = CenterRegionAssigner(0.2, 0.2)
152
+ >>> bboxes = torch.Tensor([[0, 0, 10, 10], [10, 10, 20, 20]])
153
+ >>> gt_bboxes = torch.Tensor([[0, 0, 10, 10]])
154
+ >>> assign_result = self.assign(bboxes, gt_bboxes)
155
+ >>> expected_gt_inds = torch.LongTensor([1, 0])
156
+ >>> assert torch.all(assign_result.gt_inds == expected_gt_inds)
157
+ """
158
+ # There are in total 5 steps in the pixel assignment
159
+ # 1. Find core (the center region, say inner 0.2)
160
+ # and shadow (the relatively ourter part, say inner 0.2-0.5)
161
+ # regions of every gt.
162
+ # 2. Find all prior bboxes that lie in gt_core and gt_shadow regions
163
+ # 3. Assign prior bboxes in gt_core with a one-hot id of the gt in
164
+ # the image.
165
+ # 3.1. For overlapping objects, the prior bboxes in gt_core is
166
+ # assigned with the object with smallest area
167
+ # 4. Assign prior bboxes with class label according to its gt id.
168
+ # 4.1. Assign -1 to prior bboxes lying in shadowed gts
169
+ # 4.2. Assign positive prior boxes with the corresponding label
170
+ # 5. Find pixels lying in the shadow of an object and assign them with
171
+ # background label, but set the loss weight of its corresponding
172
+ # gt to zero.
173
+ assert bboxes.size(1) == 4, 'bboxes must have size of 4'
174
+ # 1. Find core positive and shadow region of every gt
175
+ gt_core = scale_boxes(gt_bboxes, self.pos_scale)
176
+ gt_shadow = scale_boxes(gt_bboxes, self.neg_scale)
177
+
178
+ # 2. Find prior bboxes that lie in gt_core and gt_shadow regions
179
+ bbox_centers = (bboxes[:, 2:4] + bboxes[:, 0:2]) / 2
180
+ # The center points lie within the gt boxes
181
+ is_bbox_in_gt = is_located_in(bbox_centers, gt_bboxes)
182
+ # Only calculate bbox and gt_core IoF. This enables small prior bboxes
183
+ # to match large gts
184
+ bbox_and_gt_core_overlaps = self.iou_calculator(
185
+ bboxes, gt_core, mode='iof')
186
+ # The center point of effective priors should be within the gt box
187
+ is_bbox_in_gt_core = is_bbox_in_gt & (
188
+ bbox_and_gt_core_overlaps > self.min_pos_iof) # shape (n, k)
189
+
190
+ is_bbox_in_gt_shadow = (
191
+ self.iou_calculator(bboxes, gt_shadow, mode='iof') >
192
+ self.min_pos_iof)
193
+ # Rule out center effective positive pixels
194
+ is_bbox_in_gt_shadow &= (~is_bbox_in_gt_core)
195
+
196
+ num_gts, num_bboxes = gt_bboxes.size(0), bboxes.size(0)
197
+ if num_gts == 0 or num_bboxes == 0:
198
+ # If no gts exist, assign all pixels to negative
199
+ assigned_gt_ids = \
200
+ is_bbox_in_gt_core.new_zeros((num_bboxes,),
201
+ dtype=torch.long)
202
+ pixels_in_gt_shadow = assigned_gt_ids.new_empty((0, 2))
203
+ else:
204
+ # Step 3: assign a one-hot gt id to each pixel, and smaller objects
205
+ # have high priority to assign the pixel.
206
+ sort_idx = self.get_gt_priorities(gt_bboxes)
207
+ assigned_gt_ids, pixels_in_gt_shadow = \
208
+ self.assign_one_hot_gt_indices(is_bbox_in_gt_core,
209
+ is_bbox_in_gt_shadow,
210
+ gt_priority=sort_idx)
211
+
212
+ if gt_bboxes_ignore is not None and gt_bboxes_ignore.numel() > 0:
213
+ # No ground truth or boxes, return empty assignment
214
+ gt_bboxes_ignore = scale_boxes(
215
+ gt_bboxes_ignore, scale=self.ignore_gt_scale)
216
+ is_bbox_in_ignored_gts = is_located_in(bbox_centers,
217
+ gt_bboxes_ignore)
218
+ is_bbox_in_ignored_gts = is_bbox_in_ignored_gts.any(dim=1)
219
+ assigned_gt_ids[is_bbox_in_ignored_gts] = -1
220
+
221
+ # 4. Assign prior bboxes with class label according to its gt id.
222
+ assigned_labels = None
223
+ shadowed_pixel_labels = None
224
+ if gt_labels is not None:
225
+ # Default assigned label is the background (-1)
226
+ assigned_labels = assigned_gt_ids.new_full((num_bboxes, ), -1)
227
+ pos_inds = torch.nonzero(
228
+ assigned_gt_ids > 0, as_tuple=False).squeeze()
229
+ if pos_inds.numel() > 0:
230
+ assigned_labels[pos_inds] = gt_labels[assigned_gt_ids[pos_inds]
231
+ - 1]
232
+ # 5. Find pixels lying in the shadow of an object
233
+ shadowed_pixel_labels = pixels_in_gt_shadow.clone()
234
+ if pixels_in_gt_shadow.numel() > 0:
235
+ pixel_idx, gt_idx =\
236
+ pixels_in_gt_shadow[:, 0], pixels_in_gt_shadow[:, 1]
237
+ assert (assigned_gt_ids[pixel_idx] != gt_idx).all(), \
238
+ 'Some pixels are dually assigned to ignore and gt!'
239
+ shadowed_pixel_labels[:, 1] = gt_labels[gt_idx - 1]
240
+ override = (
241
+ assigned_labels[pixel_idx] == shadowed_pixel_labels[:, 1])
242
+ if self.foreground_dominate:
243
+ # When a pixel is both positive and shadowed, set it as pos
244
+ shadowed_pixel_labels = shadowed_pixel_labels[~override]
245
+ else:
246
+ # When a pixel is both pos and shadowed, set it as shadowed
247
+ assigned_labels[pixel_idx[override]] = -1
248
+ assigned_gt_ids[pixel_idx[override]] = 0
249
+
250
+ assign_result = AssignResult(
251
+ num_gts, assigned_gt_ids, None, labels=assigned_labels)
252
+ # Add shadowed_labels as assign_result property. Shape: (num_shadow, 2)
253
+ assign_result.set_extra_property('shadowed_labels',
254
+ shadowed_pixel_labels)
255
+ return assign_result
256
+
257
+ def assign_one_hot_gt_indices(self,
258
+ is_bbox_in_gt_core,
259
+ is_bbox_in_gt_shadow,
260
+ gt_priority=None):
261
+ """Assign only one gt index to each prior box.
262
+
263
+ Gts with large gt_priority are more likely to be assigned.
264
+
265
+ Args:
266
+ is_bbox_in_gt_core (Tensor): Bool tensor indicating the bbox center
267
+ is in the core area of a gt (e.g. 0-0.2).
268
+ Shape: (num_prior, num_gt).
269
+ is_bbox_in_gt_shadow (Tensor): Bool tensor indicating the bbox
270
+ center is in the shadowed area of a gt (e.g. 0.2-0.5).
271
+ Shape: (num_prior, num_gt).
272
+ gt_priority (Tensor): Priorities of gts. The gt with a higher
273
+ priority is more likely to be assigned to the bbox when the bbox
274
+ match with multiple gts. Shape: (num_gt, ).
275
+
276
+ Returns:
277
+ tuple: Returns (assigned_gt_inds, shadowed_gt_inds).
278
+
279
+ - assigned_gt_inds: The assigned gt index of each prior bbox \
280
+ (i.e. index from 1 to num_gts). Shape: (num_prior, ).
281
+ - shadowed_gt_inds: shadowed gt indices. It is a tensor of \
282
+ shape (num_ignore, 2) with first column being the \
283
+ shadowed prior bbox indices and the second column the \
284
+ shadowed gt indices (1-based).
285
+ """
286
+ num_bboxes, num_gts = is_bbox_in_gt_core.shape
287
+
288
+ if gt_priority is None:
289
+ gt_priority = torch.arange(
290
+ num_gts, device=is_bbox_in_gt_core.device)
291
+ assert gt_priority.size(0) == num_gts
292
+ # The bigger gt_priority, the more preferable to be assigned
293
+ # The assigned inds are by default 0 (background)
294
+ assigned_gt_inds = is_bbox_in_gt_core.new_zeros((num_bboxes, ),
295
+ dtype=torch.long)
296
+ # Shadowed bboxes are assigned to be background. But the corresponding
297
+ # label is ignored during loss calculation, which is done through
298
+ # shadowed_gt_inds
299
+ shadowed_gt_inds = torch.nonzero(is_bbox_in_gt_shadow, as_tuple=False)
300
+ if is_bbox_in_gt_core.sum() == 0: # No gt match
301
+ shadowed_gt_inds[:, 1] += 1 # 1-based. For consistency issue
302
+ return assigned_gt_inds, shadowed_gt_inds
303
+
304
+ # The priority of each prior box and gt pair. If one prior box is
305
+ # matched bo multiple gts. Only the pair with the highest priority
306
+ # is saved
307
+ pair_priority = is_bbox_in_gt_core.new_full((num_bboxes, num_gts),
308
+ -1,
309
+ dtype=torch.long)
310
+
311
+ # Each bbox could match with multiple gts.
312
+ # The following codes deal with this situation
313
+ # Matched bboxes (to any gt). Shape: (num_pos_anchor, )
314
+ inds_of_match = torch.any(is_bbox_in_gt_core, dim=1)
315
+ # The matched gt index of each positive bbox. Length >= num_pos_anchor
316
+ # , since one bbox could match multiple gts
317
+ matched_bbox_gt_inds = torch.nonzero(
318
+ is_bbox_in_gt_core, as_tuple=False)[:, 1]
319
+ # Assign priority to each bbox-gt pair.
320
+ pair_priority[is_bbox_in_gt_core] = gt_priority[matched_bbox_gt_inds]
321
+ _, argmax_priority = pair_priority[inds_of_match].max(dim=1)
322
+ assigned_gt_inds[inds_of_match] = argmax_priority + 1 # 1-based
323
+ # Zero-out the assigned anchor box to filter the shadowed gt indices
324
+ is_bbox_in_gt_core[inds_of_match, argmax_priority] = 0
325
+ # Concat the shadowed indices due to overlapping with that out side of
326
+ # effective scale. shape: (total_num_ignore, 2)
327
+ shadowed_gt_inds = torch.cat(
328
+ (shadowed_gt_inds, torch.nonzero(
329
+ is_bbox_in_gt_core, as_tuple=False)),
330
+ dim=0)
331
+ # `is_bbox_in_gt_core` should be changed back to keep arguments intact.
332
+ is_bbox_in_gt_core[inds_of_match, argmax_priority] = 1
333
+ # 1-based shadowed gt indices, to be consistent with `assigned_gt_inds`
334
+ if shadowed_gt_inds.numel() > 0:
335
+ shadowed_gt_inds[:, 1] += 1
336
+ return assigned_gt_inds, shadowed_gt_inds
submodules/chartdete/mmdet/core/bbox/assigners/grid_assigner.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+ from ..builder import BBOX_ASSIGNERS
5
+ from ..iou_calculators import build_iou_calculator
6
+ from .assign_result import AssignResult
7
+ from .base_assigner import BaseAssigner
8
+
9
+
10
+ @BBOX_ASSIGNERS.register_module()
11
+ class GridAssigner(BaseAssigner):
12
+ """Assign a corresponding gt bbox or background to each bbox.
13
+
14
+ Each proposals will be assigned with `-1`, `0`, or a positive integer
15
+ indicating the ground truth index.
16
+
17
+ - -1: don't care
18
+ - 0: negative sample, no assigned gt
19
+ - positive integer: positive sample, index (1-based) of assigned gt
20
+
21
+ Args:
22
+ pos_iou_thr (float): IoU threshold for positive bboxes.
23
+ neg_iou_thr (float or tuple): IoU threshold for negative bboxes.
24
+ min_pos_iou (float): Minimum iou for a bbox to be considered as a
25
+ positive bbox. Positive samples can have smaller IoU than
26
+ pos_iou_thr due to the 4th step (assign max IoU sample to each gt).
27
+ gt_max_assign_all (bool): Whether to assign all bboxes with the same
28
+ highest overlap with some gt to that gt.
29
+ """
30
+
31
+ def __init__(self,
32
+ pos_iou_thr,
33
+ neg_iou_thr,
34
+ min_pos_iou=.0,
35
+ gt_max_assign_all=True,
36
+ iou_calculator=dict(type='BboxOverlaps2D')):
37
+ self.pos_iou_thr = pos_iou_thr
38
+ self.neg_iou_thr = neg_iou_thr
39
+ self.min_pos_iou = min_pos_iou
40
+ self.gt_max_assign_all = gt_max_assign_all
41
+ self.iou_calculator = build_iou_calculator(iou_calculator)
42
+
43
+ def assign(self, bboxes, box_responsible_flags, gt_bboxes, gt_labels=None):
44
+ """Assign gt to bboxes. The process is very much like the max iou
45
+ assigner, except that positive samples are constrained within the cell
46
+ that the gt boxes fell in.
47
+
48
+ This method assign a gt bbox to every bbox (proposal/anchor), each bbox
49
+ will be assigned with -1, 0, or a positive number. -1 means don't care,
50
+ 0 means negative sample, positive number is the index (1-based) of
51
+ assigned gt.
52
+ The assignment is done in following steps, the order matters.
53
+
54
+ 1. assign every bbox to -1
55
+ 2. assign proposals whose iou with all gts <= neg_iou_thr to 0
56
+ 3. for each bbox within a cell, if the iou with its nearest gt >
57
+ pos_iou_thr and the center of that gt falls inside the cell,
58
+ assign it to that bbox
59
+ 4. for each gt bbox, assign its nearest proposals within the cell the
60
+ gt bbox falls in to itself.
61
+
62
+ Args:
63
+ bboxes (Tensor): Bounding boxes to be assigned, shape(n, 4).
64
+ box_responsible_flags (Tensor): flag to indicate whether box is
65
+ responsible for prediction, shape(n, )
66
+ gt_bboxes (Tensor): Groundtruth boxes, shape (k, 4).
67
+ gt_labels (Tensor, optional): Label of gt_bboxes, shape (k, ).
68
+
69
+ Returns:
70
+ :obj:`AssignResult`: The assign result.
71
+ """
72
+ num_gts, num_bboxes = gt_bboxes.size(0), bboxes.size(0)
73
+
74
+ # compute iou between all gt and bboxes
75
+ overlaps = self.iou_calculator(gt_bboxes, bboxes)
76
+
77
+ # 1. assign -1 by default
78
+ assigned_gt_inds = overlaps.new_full((num_bboxes, ),
79
+ -1,
80
+ dtype=torch.long)
81
+
82
+ if num_gts == 0 or num_bboxes == 0:
83
+ # No ground truth or boxes, return empty assignment
84
+ max_overlaps = overlaps.new_zeros((num_bboxes, ))
85
+ if num_gts == 0:
86
+ # No truth, assign everything to background
87
+ assigned_gt_inds[:] = 0
88
+ if gt_labels is None:
89
+ assigned_labels = None
90
+ else:
91
+ assigned_labels = overlaps.new_full((num_bboxes, ),
92
+ -1,
93
+ dtype=torch.long)
94
+ return AssignResult(
95
+ num_gts,
96
+ assigned_gt_inds,
97
+ max_overlaps,
98
+ labels=assigned_labels)
99
+
100
+ # 2. assign negative: below
101
+ # for each anchor, which gt best overlaps with it
102
+ # for each anchor, the max iou of all gts
103
+ # shape of max_overlaps == argmax_overlaps == num_bboxes
104
+ max_overlaps, argmax_overlaps = overlaps.max(dim=0)
105
+
106
+ if isinstance(self.neg_iou_thr, float):
107
+ assigned_gt_inds[(max_overlaps >= 0)
108
+ & (max_overlaps <= self.neg_iou_thr)] = 0
109
+ elif isinstance(self.neg_iou_thr, (tuple, list)):
110
+ assert len(self.neg_iou_thr) == 2
111
+ assigned_gt_inds[(max_overlaps > self.neg_iou_thr[0])
112
+ & (max_overlaps <= self.neg_iou_thr[1])] = 0
113
+
114
+ # 3. assign positive: falls into responsible cell and above
115
+ # positive IOU threshold, the order matters.
116
+ # the prior condition of comparison is to filter out all
117
+ # unrelated anchors, i.e. not box_responsible_flags
118
+ overlaps[:, ~box_responsible_flags.type(torch.bool)] = -1.
119
+
120
+ # calculate max_overlaps again, but this time we only consider IOUs
121
+ # for anchors responsible for prediction
122
+ max_overlaps, argmax_overlaps = overlaps.max(dim=0)
123
+
124
+ # for each gt, which anchor best overlaps with it
125
+ # for each gt, the max iou of all proposals
126
+ # shape of gt_max_overlaps == gt_argmax_overlaps == num_gts
127
+ gt_max_overlaps, gt_argmax_overlaps = overlaps.max(dim=1)
128
+
129
+ pos_inds = (max_overlaps >
130
+ self.pos_iou_thr) & box_responsible_flags.type(torch.bool)
131
+ assigned_gt_inds[pos_inds] = argmax_overlaps[pos_inds] + 1
132
+
133
+ # 4. assign positive to max overlapped anchors within responsible cell
134
+ for i in range(num_gts):
135
+ if gt_max_overlaps[i] > self.min_pos_iou:
136
+ if self.gt_max_assign_all:
137
+ max_iou_inds = (overlaps[i, :] == gt_max_overlaps[i]) & \
138
+ box_responsible_flags.type(torch.bool)
139
+ assigned_gt_inds[max_iou_inds] = i + 1
140
+ elif box_responsible_flags[gt_argmax_overlaps[i]]:
141
+ assigned_gt_inds[gt_argmax_overlaps[i]] = i + 1
142
+
143
+ # assign labels of positive anchors
144
+ if gt_labels is not None:
145
+ assigned_labels = assigned_gt_inds.new_full((num_bboxes, ), -1)
146
+ pos_inds = torch.nonzero(
147
+ assigned_gt_inds > 0, as_tuple=False).squeeze()
148
+ if pos_inds.numel() > 0:
149
+ assigned_labels[pos_inds] = gt_labels[
150
+ assigned_gt_inds[pos_inds] - 1]
151
+
152
+ else:
153
+ assigned_labels = None
154
+
155
+ return AssignResult(
156
+ num_gts, assigned_gt_inds, max_overlaps, labels=assigned_labels)
submodules/chartdete/mmdet/core/bbox/assigners/hungarian_assigner.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+ from scipy.optimize import linear_sum_assignment
4
+
5
+ from ..builder import BBOX_ASSIGNERS
6
+ from ..match_costs import build_match_cost
7
+ from ..transforms import bbox_cxcywh_to_xyxy
8
+ from .assign_result import AssignResult
9
+ from .base_assigner import BaseAssigner
10
+
11
+
12
+ @BBOX_ASSIGNERS.register_module()
13
+ class HungarianAssigner(BaseAssigner):
14
+ """Computes one-to-one matching between predictions and ground truth.
15
+
16
+ This class computes an assignment between the targets and the predictions
17
+ based on the costs. The costs are weighted sum of three components:
18
+ classification cost, regression L1 cost and regression iou cost. The
19
+ targets don't include the no_object, so generally there are more
20
+ predictions than targets. After the one-to-one matching, the un-matched
21
+ are treated as backgrounds. Thus each query prediction will be assigned
22
+ with `0` or a positive integer indicating the ground truth index:
23
+
24
+ - 0: negative sample, no assigned gt
25
+ - positive integer: positive sample, index (1-based) of assigned gt
26
+
27
+ Args:
28
+ cls_weight (int | float, optional): The scale factor for classification
29
+ cost. Default 1.0.
30
+ bbox_weight (int | float, optional): The scale factor for regression
31
+ L1 cost. Default 1.0.
32
+ iou_weight (int | float, optional): The scale factor for regression
33
+ iou cost. Default 1.0.
34
+ iou_calculator (dict | optional): The config for the iou calculation.
35
+ Default type `BboxOverlaps2D`.
36
+ iou_mode (str | optional): "iou" (intersection over union), "iof"
37
+ (intersection over foreground), or "giou" (generalized
38
+ intersection over union). Default "giou".
39
+ """
40
+
41
+ def __init__(self,
42
+ cls_cost=dict(type='ClassificationCost', weight=1.),
43
+ reg_cost=dict(type='BBoxL1Cost', weight=1.0),
44
+ iou_cost=dict(type='IoUCost', iou_mode='giou', weight=1.0)):
45
+ self.cls_cost = build_match_cost(cls_cost)
46
+ self.reg_cost = build_match_cost(reg_cost)
47
+ self.iou_cost = build_match_cost(iou_cost)
48
+
49
+ def assign(self,
50
+ bbox_pred,
51
+ cls_pred,
52
+ gt_bboxes,
53
+ gt_labels,
54
+ img_meta,
55
+ gt_bboxes_ignore=None,
56
+ eps=1e-7):
57
+ """Computes one-to-one matching based on the weighted costs.
58
+
59
+ This method assign each query prediction to a ground truth or
60
+ background. The `assigned_gt_inds` with -1 means don't care,
61
+ 0 means negative sample, and positive number is the index (1-based)
62
+ of assigned gt.
63
+ The assignment is done in the following steps, the order matters.
64
+
65
+ 1. assign every prediction to -1
66
+ 2. compute the weighted costs
67
+ 3. do Hungarian matching on CPU based on the costs
68
+ 4. assign all to 0 (background) first, then for each matched pair
69
+ between predictions and gts, treat this prediction as foreground
70
+ and assign the corresponding gt index (plus 1) to it.
71
+
72
+ Args:
73
+ bbox_pred (Tensor): Predicted boxes with normalized coordinates
74
+ (cx, cy, w, h), which are all in range [0, 1]. Shape
75
+ [num_query, 4].
76
+ cls_pred (Tensor): Predicted classification logits, shape
77
+ [num_query, num_class].
78
+ gt_bboxes (Tensor): Ground truth boxes with unnormalized
79
+ coordinates (x1, y1, x2, y2). Shape [num_gt, 4].
80
+ gt_labels (Tensor): Label of `gt_bboxes`, shape (num_gt,).
81
+ img_meta (dict): Meta information for current image.
82
+ gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are
83
+ labelled as `ignored`. Default None.
84
+ eps (int | float, optional): A value added to the denominator for
85
+ numerical stability. Default 1e-7.
86
+
87
+ Returns:
88
+ :obj:`AssignResult`: The assigned result.
89
+ """
90
+ assert gt_bboxes_ignore is None, \
91
+ 'Only case when gt_bboxes_ignore is None is supported.'
92
+ num_gts, num_bboxes = gt_bboxes.size(0), bbox_pred.size(0)
93
+
94
+ # 1. assign -1 by default
95
+ assigned_gt_inds = bbox_pred.new_full((num_bboxes, ),
96
+ -1,
97
+ dtype=torch.long)
98
+ assigned_labels = bbox_pred.new_full((num_bboxes, ),
99
+ -1,
100
+ dtype=torch.long)
101
+ if num_gts == 0 or num_bboxes == 0:
102
+ # No ground truth or boxes, return empty assignment
103
+ if num_gts == 0:
104
+ # No ground truth, assign all to background
105
+ assigned_gt_inds[:] = 0
106
+ return AssignResult(
107
+ num_gts, assigned_gt_inds, None, labels=assigned_labels)
108
+ img_h, img_w, _ = img_meta['img_shape']
109
+ factor = gt_bboxes.new_tensor([img_w, img_h, img_w,
110
+ img_h]).unsqueeze(0)
111
+
112
+ # 2. compute the weighted costs
113
+ # classification and bboxcost.
114
+ cls_cost = self.cls_cost(cls_pred, gt_labels)
115
+ # regression L1 cost
116
+ normalize_gt_bboxes = gt_bboxes / factor
117
+ reg_cost = self.reg_cost(bbox_pred, normalize_gt_bboxes)
118
+ # regression iou cost, defaultly giou is used in official DETR.
119
+ bboxes = bbox_cxcywh_to_xyxy(bbox_pred) * factor
120
+ iou_cost = self.iou_cost(bboxes, gt_bboxes)
121
+ # weighted sum of above three costs
122
+ cost = cls_cost + reg_cost + iou_cost
123
+
124
+ # 3. do Hungarian matching on CPU using linear_sum_assignment
125
+ cost = cost.detach().cpu()
126
+ matched_row_inds, matched_col_inds = linear_sum_assignment(cost)
127
+ matched_row_inds = torch.from_numpy(matched_row_inds).to(
128
+ bbox_pred.device)
129
+ matched_col_inds = torch.from_numpy(matched_col_inds).to(
130
+ bbox_pred.device)
131
+
132
+ # 4. assign backgrounds and foregrounds
133
+ # assign all indices to backgrounds first
134
+ assigned_gt_inds[:] = 0
135
+ # assign foregrounds based on matching results
136
+ assigned_gt_inds[matched_row_inds] = matched_col_inds + 1
137
+ assigned_labels[matched_row_inds] = gt_labels[matched_col_inds]
138
+ return AssignResult(
139
+ num_gts, assigned_gt_inds, None, labels=assigned_labels)
submodules/chartdete/mmdet/core/bbox/assigners/mask_hungarian_assigner.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+ from scipy.optimize import linear_sum_assignment
4
+
5
+ from mmdet.core.bbox.builder import BBOX_ASSIGNERS
6
+ from mmdet.core.bbox.match_costs.builder import build_match_cost
7
+ from .assign_result import AssignResult
8
+ from .base_assigner import BaseAssigner
9
+
10
+
11
+ @BBOX_ASSIGNERS.register_module()
12
+ class MaskHungarianAssigner(BaseAssigner):
13
+ """Computes one-to-one matching between predictions and ground truth for
14
+ mask.
15
+
16
+ This class computes an assignment between the targets and the predictions
17
+ based on the costs. The costs are weighted sum of three components:
18
+ classification cost, mask focal cost and mask dice cost. The
19
+ targets don't include the no_object, so generally there are more
20
+ predictions than targets. After the one-to-one matching, the un-matched
21
+ are treated as backgrounds. Thus each query prediction will be assigned
22
+ with `0` or a positive integer indicating the ground truth index:
23
+
24
+ - 0: negative sample, no assigned gt
25
+ - positive integer: positive sample, index (1-based) of assigned gt
26
+
27
+ Args:
28
+ cls_cost (:obj:`mmcv.ConfigDict` | dict): Classification cost config.
29
+ mask_cost (:obj:`mmcv.ConfigDict` | dict): Mask cost config.
30
+ dice_cost (:obj:`mmcv.ConfigDict` | dict): Dice cost config.
31
+ """
32
+
33
+ def __init__(self,
34
+ cls_cost=dict(type='ClassificationCost', weight=1.0),
35
+ mask_cost=dict(
36
+ type='FocalLossCost', weight=1.0, binary_input=True),
37
+ dice_cost=dict(type='DiceCost', weight=1.0)):
38
+ self.cls_cost = build_match_cost(cls_cost)
39
+ self.mask_cost = build_match_cost(mask_cost)
40
+ self.dice_cost = build_match_cost(dice_cost)
41
+
42
+ def assign(self,
43
+ cls_pred,
44
+ mask_pred,
45
+ gt_labels,
46
+ gt_mask,
47
+ img_meta,
48
+ gt_bboxes_ignore=None,
49
+ eps=1e-7):
50
+ """Computes one-to-one matching based on the weighted costs.
51
+
52
+ Args:
53
+ cls_pred (Tensor | None): Class prediction in shape
54
+ (num_query, cls_out_channels).
55
+ mask_pred (Tensor): Mask prediction in shape (num_query, H, W).
56
+ gt_labels (Tensor): Label of 'gt_mask'in shape = (num_gt, ).
57
+ gt_mask (Tensor): Ground truth mask in shape = (num_gt, H, W).
58
+ img_meta (dict): Meta information for current image.
59
+ gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are
60
+ labelled as `ignored`. Default None.
61
+ eps (int | float, optional): A value added to the denominator for
62
+ numerical stability. Default 1e-7.
63
+
64
+ Returns:
65
+ :obj:`AssignResult`: The assigned result.
66
+ """
67
+ assert gt_bboxes_ignore is None, \
68
+ 'Only case when gt_bboxes_ignore is None is supported.'
69
+ # K-Net sometimes passes cls_pred=None to this assigner.
70
+ # So we should use the shape of mask_pred
71
+ num_gt, num_query = gt_labels.shape[0], mask_pred.shape[0]
72
+
73
+ # 1. assign -1 by default
74
+ assigned_gt_inds = mask_pred.new_full((num_query, ),
75
+ -1,
76
+ dtype=torch.long)
77
+ assigned_labels = mask_pred.new_full((num_query, ),
78
+ -1,
79
+ dtype=torch.long)
80
+ if num_gt == 0 or num_query == 0:
81
+ # No ground truth or boxes, return empty assignment
82
+ if num_gt == 0:
83
+ # No ground truth, assign all to background
84
+ assigned_gt_inds[:] = 0
85
+ return AssignResult(
86
+ num_gt, assigned_gt_inds, None, labels=assigned_labels)
87
+
88
+ # 2. compute the weighted costs
89
+ # classification and maskcost.
90
+ if self.cls_cost.weight != 0 and cls_pred is not None:
91
+ cls_cost = self.cls_cost(cls_pred, gt_labels)
92
+ else:
93
+ cls_cost = 0
94
+
95
+ if self.mask_cost.weight != 0:
96
+ # mask_pred shape = [num_query, h, w]
97
+ # gt_mask shape = [num_gt, h, w]
98
+ # mask_cost shape = [num_query, num_gt]
99
+ mask_cost = self.mask_cost(mask_pred, gt_mask)
100
+ else:
101
+ mask_cost = 0
102
+
103
+ if self.dice_cost.weight != 0:
104
+ dice_cost = self.dice_cost(mask_pred, gt_mask)
105
+ else:
106
+ dice_cost = 0
107
+ cost = cls_cost + mask_cost + dice_cost
108
+
109
+ # 3. do Hungarian matching on CPU using linear_sum_assignment
110
+ cost = cost.detach().cpu()
111
+
112
+ matched_row_inds, matched_col_inds = linear_sum_assignment(cost)
113
+ matched_row_inds = torch.from_numpy(matched_row_inds).to(
114
+ mask_pred.device)
115
+ matched_col_inds = torch.from_numpy(matched_col_inds).to(
116
+ mask_pred.device)
117
+
118
+ # 4. assign backgrounds and foregrounds
119
+ # assign all indices to backgrounds first
120
+ assigned_gt_inds[:] = 0
121
+ # assign foregrounds based on matching results
122
+ assigned_gt_inds[matched_row_inds] = matched_col_inds + 1
123
+ assigned_labels[matched_row_inds] = gt_labels[matched_col_inds]
124
+ return AssignResult(
125
+ num_gt, assigned_gt_inds, None, labels=assigned_labels)
submodules/chartdete/mmdet/core/bbox/assigners/max_iou_assigner.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+ from ..builder import BBOX_ASSIGNERS
5
+ from ..iou_calculators import build_iou_calculator
6
+ from .assign_result import AssignResult
7
+ from .base_assigner import BaseAssigner
8
+
9
+
10
+ @BBOX_ASSIGNERS.register_module()
11
+ class MaxIoUAssigner(BaseAssigner):
12
+ """Assign a corresponding gt bbox or background to each bbox.
13
+
14
+ Each proposals will be assigned with `-1`, or a semi-positive integer
15
+ indicating the ground truth index.
16
+
17
+ - -1: negative sample, no assigned gt
18
+ - semi-positive integer: positive sample, index (0-based) of assigned gt
19
+
20
+ Args:
21
+ pos_iou_thr (float): IoU threshold for positive bboxes.
22
+ neg_iou_thr (float or tuple): IoU threshold for negative bboxes.
23
+ min_pos_iou (float): Minimum iou for a bbox to be considered as a
24
+ positive bbox. Positive samples can have smaller IoU than
25
+ pos_iou_thr due to the 4th step (assign max IoU sample to each gt).
26
+ `min_pos_iou` is set to avoid assigning bboxes that have extremely
27
+ small iou with GT as positive samples. It brings about 0.3 mAP
28
+ improvements in 1x schedule but does not affect the performance of
29
+ 3x schedule. More comparisons can be found in
30
+ `PR #7464 <https://github.com/open-mmlab/mmdetection/pull/7464>`_.
31
+ gt_max_assign_all (bool): Whether to assign all bboxes with the same
32
+ highest overlap with some gt to that gt.
33
+ ignore_iof_thr (float): IoF threshold for ignoring bboxes (if
34
+ `gt_bboxes_ignore` is specified). Negative values mean not
35
+ ignoring any bboxes.
36
+ ignore_wrt_candidates (bool): Whether to compute the iof between
37
+ `bboxes` and `gt_bboxes_ignore`, or the contrary.
38
+ match_low_quality (bool): Whether to allow low quality matches. This is
39
+ usually allowed for RPN and single stage detectors, but not allowed
40
+ in the second stage. Details are demonstrated in Step 4.
41
+ gpu_assign_thr (int): The upper bound of the number of GT for GPU
42
+ assign. When the number of gt is above this threshold, will assign
43
+ on CPU device. Negative values mean not assign on CPU.
44
+ """
45
+
46
+ def __init__(self,
47
+ pos_iou_thr,
48
+ neg_iou_thr,
49
+ min_pos_iou=.0,
50
+ gt_max_assign_all=True,
51
+ ignore_iof_thr=-1,
52
+ ignore_wrt_candidates=True,
53
+ match_low_quality=True,
54
+ gpu_assign_thr=-1,
55
+ iou_calculator=dict(type='BboxOverlaps2D')):
56
+ self.pos_iou_thr = pos_iou_thr
57
+ self.neg_iou_thr = neg_iou_thr
58
+ self.min_pos_iou = min_pos_iou
59
+ self.gt_max_assign_all = gt_max_assign_all
60
+ self.ignore_iof_thr = ignore_iof_thr
61
+ self.ignore_wrt_candidates = ignore_wrt_candidates
62
+ self.gpu_assign_thr = gpu_assign_thr
63
+ self.match_low_quality = match_low_quality
64
+ self.iou_calculator = build_iou_calculator(iou_calculator)
65
+
66
+ def assign(self, bboxes, gt_bboxes, gt_bboxes_ignore=None, gt_labels=None):
67
+ """Assign gt to bboxes.
68
+
69
+ This method assign a gt bbox to every bbox (proposal/anchor), each bbox
70
+ will be assigned with -1, or a semi-positive number. -1 means negative
71
+ sample, semi-positive number is the index (0-based) of assigned gt.
72
+ The assignment is done in following steps, the order matters.
73
+
74
+ 1. assign every bbox to the background
75
+ 2. assign proposals whose iou with all gts < neg_iou_thr to 0
76
+ 3. for each bbox, if the iou with its nearest gt >= pos_iou_thr,
77
+ assign it to that bbox
78
+ 4. for each gt bbox, assign its nearest proposals (may be more than
79
+ one) to itself
80
+
81
+ Args:
82
+ bboxes (Tensor): Bounding boxes to be assigned, shape(n, 4).
83
+ gt_bboxes (Tensor): Groundtruth boxes, shape (k, 4).
84
+ gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are
85
+ labelled as `ignored`, e.g., crowd boxes in COCO.
86
+ gt_labels (Tensor, optional): Label of gt_bboxes, shape (k, ).
87
+
88
+ Returns:
89
+ :obj:`AssignResult`: The assign result.
90
+
91
+ Example:
92
+ >>> self = MaxIoUAssigner(0.5, 0.5)
93
+ >>> bboxes = torch.Tensor([[0, 0, 10, 10], [10, 10, 20, 20]])
94
+ >>> gt_bboxes = torch.Tensor([[0, 0, 10, 9]])
95
+ >>> assign_result = self.assign(bboxes, gt_bboxes)
96
+ >>> expected_gt_inds = torch.LongTensor([1, 0])
97
+ >>> assert torch.all(assign_result.gt_inds == expected_gt_inds)
98
+ """
99
+ assign_on_cpu = True if (self.gpu_assign_thr > 0) and (
100
+ gt_bboxes.shape[0] > self.gpu_assign_thr) else False
101
+ # compute overlap and assign gt on CPU when number of GT is large
102
+ if assign_on_cpu:
103
+ device = bboxes.device
104
+ bboxes = bboxes.cpu()
105
+ gt_bboxes = gt_bboxes.cpu()
106
+ if gt_bboxes_ignore is not None:
107
+ gt_bboxes_ignore = gt_bboxes_ignore.cpu()
108
+ if gt_labels is not None:
109
+ gt_labels = gt_labels.cpu()
110
+
111
+ overlaps = self.iou_calculator(gt_bboxes, bboxes)
112
+
113
+ if (self.ignore_iof_thr > 0 and gt_bboxes_ignore is not None
114
+ and gt_bboxes_ignore.numel() > 0 and bboxes.numel() > 0):
115
+ if self.ignore_wrt_candidates:
116
+ ignore_overlaps = self.iou_calculator(
117
+ bboxes, gt_bboxes_ignore, mode='iof')
118
+ ignore_max_overlaps, _ = ignore_overlaps.max(dim=1)
119
+ else:
120
+ ignore_overlaps = self.iou_calculator(
121
+ gt_bboxes_ignore, bboxes, mode='iof')
122
+ ignore_max_overlaps, _ = ignore_overlaps.max(dim=0)
123
+ overlaps[:, ignore_max_overlaps > self.ignore_iof_thr] = -1
124
+
125
+ assign_result = self.assign_wrt_overlaps(overlaps, gt_labels)
126
+ if assign_on_cpu:
127
+ assign_result.gt_inds = assign_result.gt_inds.to(device)
128
+ assign_result.max_overlaps = assign_result.max_overlaps.to(device)
129
+ if assign_result.labels is not None:
130
+ assign_result.labels = assign_result.labels.to(device)
131
+ return assign_result
132
+
133
+ def assign_wrt_overlaps(self, overlaps, gt_labels=None):
134
+ """Assign w.r.t. the overlaps of bboxes with gts.
135
+
136
+ Args:
137
+ overlaps (Tensor): Overlaps between k gt_bboxes and n bboxes,
138
+ shape(k, n).
139
+ gt_labels (Tensor, optional): Labels of k gt_bboxes, shape (k, ).
140
+
141
+ Returns:
142
+ :obj:`AssignResult`: The assign result.
143
+ """
144
+ num_gts, num_bboxes = overlaps.size(0), overlaps.size(1)
145
+
146
+ # 1. assign -1 by default
147
+ assigned_gt_inds = overlaps.new_full((num_bboxes, ),
148
+ -1,
149
+ dtype=torch.long)
150
+
151
+ if num_gts == 0 or num_bboxes == 0:
152
+ # No ground truth or boxes, return empty assignment
153
+ max_overlaps = overlaps.new_zeros((num_bboxes, ))
154
+ if num_gts == 0:
155
+ # No truth, assign everything to background
156
+ assigned_gt_inds[:] = 0
157
+ if gt_labels is None:
158
+ assigned_labels = None
159
+ else:
160
+ assigned_labels = overlaps.new_full((num_bboxes, ),
161
+ -1,
162
+ dtype=torch.long)
163
+ return AssignResult(
164
+ num_gts,
165
+ assigned_gt_inds,
166
+ max_overlaps,
167
+ labels=assigned_labels)
168
+
169
+ # for each anchor, which gt best overlaps with it
170
+ # for each anchor, the max iou of all gts
171
+ max_overlaps, argmax_overlaps = overlaps.max(dim=0)
172
+ # for each gt, which anchor best overlaps with it
173
+ # for each gt, the max iou of all proposals
174
+ gt_max_overlaps, gt_argmax_overlaps = overlaps.max(dim=1)
175
+
176
+ # 2. assign negative: below
177
+ # the negative inds are set to be 0
178
+ if isinstance(self.neg_iou_thr, float):
179
+ assigned_gt_inds[(max_overlaps >= 0)
180
+ & (max_overlaps < self.neg_iou_thr)] = 0
181
+ elif isinstance(self.neg_iou_thr, tuple):
182
+ assert len(self.neg_iou_thr) == 2
183
+ assigned_gt_inds[(max_overlaps >= self.neg_iou_thr[0])
184
+ & (max_overlaps < self.neg_iou_thr[1])] = 0
185
+
186
+ # 3. assign positive: above positive IoU threshold
187
+ pos_inds = max_overlaps >= self.pos_iou_thr
188
+ assigned_gt_inds[pos_inds] = argmax_overlaps[pos_inds] + 1
189
+
190
+ if self.match_low_quality:
191
+ # Low-quality matching will overwrite the assigned_gt_inds assigned
192
+ # in Step 3. Thus, the assigned gt might not be the best one for
193
+ # prediction.
194
+ # For example, if bbox A has 0.9 and 0.8 iou with GT bbox 1 & 2,
195
+ # bbox 1 will be assigned as the best target for bbox A in step 3.
196
+ # However, if GT bbox 2's gt_argmax_overlaps = A, bbox A's
197
+ # assigned_gt_inds will be overwritten to be bbox 2.
198
+ # This might be the reason that it is not used in ROI Heads.
199
+ for i in range(num_gts):
200
+ if gt_max_overlaps[i] >= self.min_pos_iou:
201
+ if self.gt_max_assign_all:
202
+ max_iou_inds = overlaps[i, :] == gt_max_overlaps[i]
203
+ assigned_gt_inds[max_iou_inds] = i + 1
204
+ else:
205
+ assigned_gt_inds[gt_argmax_overlaps[i]] = i + 1
206
+
207
+ if gt_labels is not None:
208
+ assigned_labels = assigned_gt_inds.new_full((num_bboxes, ), -1)
209
+ pos_inds = torch.nonzero(
210
+ assigned_gt_inds > 0, as_tuple=False).squeeze()
211
+ if pos_inds.numel() > 0:
212
+ assigned_labels[pos_inds] = gt_labels[
213
+ assigned_gt_inds[pos_inds] - 1]
214
+ else:
215
+ assigned_labels = None
216
+
217
+ return AssignResult(
218
+ num_gts, assigned_gt_inds, max_overlaps, labels=assigned_labels)
submodules/chartdete/mmdet/core/bbox/assigners/point_assigner.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+ from ..builder import BBOX_ASSIGNERS
5
+ from .assign_result import AssignResult
6
+ from .base_assigner import BaseAssigner
7
+
8
+
9
+ @BBOX_ASSIGNERS.register_module()
10
+ class PointAssigner(BaseAssigner):
11
+ """Assign a corresponding gt bbox or background to each point.
12
+
13
+ Each proposals will be assigned with `0`, or a positive integer
14
+ indicating the ground truth index.
15
+
16
+ - 0: negative sample, no assigned gt
17
+ - positive integer: positive sample, index (1-based) of assigned gt
18
+ """
19
+
20
+ def __init__(self, scale=4, pos_num=3):
21
+ self.scale = scale
22
+ self.pos_num = pos_num
23
+
24
+ def assign(self, points, gt_bboxes, gt_bboxes_ignore=None, gt_labels=None):
25
+ """Assign gt to points.
26
+
27
+ This method assign a gt bbox to every points set, each points set
28
+ will be assigned with the background_label (-1), or a label number.
29
+ -1 is background, and semi-positive number is the index (0-based) of
30
+ assigned gt.
31
+ The assignment is done in following steps, the order matters.
32
+
33
+ 1. assign every points to the background_label (-1)
34
+ 2. A point is assigned to some gt bbox if
35
+ (i) the point is within the k closest points to the gt bbox
36
+ (ii) the distance between this point and the gt is smaller than
37
+ other gt bboxes
38
+
39
+ Args:
40
+ points (Tensor): points to be assigned, shape(n, 3) while last
41
+ dimension stands for (x, y, stride).
42
+ gt_bboxes (Tensor): Groundtruth boxes, shape (k, 4).
43
+ gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are
44
+ labelled as `ignored`, e.g., crowd boxes in COCO.
45
+ NOTE: currently unused.
46
+ gt_labels (Tensor, optional): Label of gt_bboxes, shape (k, ).
47
+
48
+ Returns:
49
+ :obj:`AssignResult`: The assign result.
50
+ """
51
+ num_points = points.shape[0]
52
+ num_gts = gt_bboxes.shape[0]
53
+
54
+ if num_gts == 0 or num_points == 0:
55
+ # If no truth assign everything to the background
56
+ assigned_gt_inds = points.new_full((num_points, ),
57
+ 0,
58
+ dtype=torch.long)
59
+ if gt_labels is None:
60
+ assigned_labels = None
61
+ else:
62
+ assigned_labels = points.new_full((num_points, ),
63
+ -1,
64
+ dtype=torch.long)
65
+ return AssignResult(
66
+ num_gts, assigned_gt_inds, None, labels=assigned_labels)
67
+
68
+ points_xy = points[:, :2]
69
+ points_stride = points[:, 2]
70
+ points_lvl = torch.log2(
71
+ points_stride).int() # [3...,4...,5...,6...,7...]
72
+ lvl_min, lvl_max = points_lvl.min(), points_lvl.max()
73
+
74
+ # assign gt box
75
+ gt_bboxes_xy = (gt_bboxes[:, :2] + gt_bboxes[:, 2:]) / 2
76
+ gt_bboxes_wh = (gt_bboxes[:, 2:] - gt_bboxes[:, :2]).clamp(min=1e-6)
77
+ scale = self.scale
78
+ gt_bboxes_lvl = ((torch.log2(gt_bboxes_wh[:, 0] / scale) +
79
+ torch.log2(gt_bboxes_wh[:, 1] / scale)) / 2).int()
80
+ gt_bboxes_lvl = torch.clamp(gt_bboxes_lvl, min=lvl_min, max=lvl_max)
81
+
82
+ # stores the assigned gt index of each point
83
+ assigned_gt_inds = points.new_zeros((num_points, ), dtype=torch.long)
84
+ # stores the assigned gt dist (to this point) of each point
85
+ assigned_gt_dist = points.new_full((num_points, ), float('inf'))
86
+ points_range = torch.arange(points.shape[0])
87
+
88
+ for idx in range(num_gts):
89
+ gt_lvl = gt_bboxes_lvl[idx]
90
+ # get the index of points in this level
91
+ lvl_idx = gt_lvl == points_lvl
92
+ points_index = points_range[lvl_idx]
93
+ # get the points in this level
94
+ lvl_points = points_xy[lvl_idx, :]
95
+ # get the center point of gt
96
+ gt_point = gt_bboxes_xy[[idx], :]
97
+ # get width and height of gt
98
+ gt_wh = gt_bboxes_wh[[idx], :]
99
+ # compute the distance between gt center and
100
+ # all points in this level
101
+ points_gt_dist = ((lvl_points - gt_point) / gt_wh).norm(dim=1)
102
+ # find the nearest k points to gt center in this level
103
+ min_dist, min_dist_index = torch.topk(
104
+ points_gt_dist, self.pos_num, largest=False)
105
+ # the index of nearest k points to gt center in this level
106
+ min_dist_points_index = points_index[min_dist_index]
107
+ # The less_than_recorded_index stores the index
108
+ # of min_dist that is less then the assigned_gt_dist. Where
109
+ # assigned_gt_dist stores the dist from previous assigned gt
110
+ # (if exist) to each point.
111
+ less_than_recorded_index = min_dist < assigned_gt_dist[
112
+ min_dist_points_index]
113
+ # The min_dist_points_index stores the index of points satisfy:
114
+ # (1) it is k nearest to current gt center in this level.
115
+ # (2) it is closer to current gt center than other gt center.
116
+ min_dist_points_index = min_dist_points_index[
117
+ less_than_recorded_index]
118
+ # assign the result
119
+ assigned_gt_inds[min_dist_points_index] = idx + 1
120
+ assigned_gt_dist[min_dist_points_index] = min_dist[
121
+ less_than_recorded_index]
122
+
123
+ if gt_labels is not None:
124
+ assigned_labels = assigned_gt_inds.new_full((num_points, ), -1)
125
+ pos_inds = torch.nonzero(
126
+ assigned_gt_inds > 0, as_tuple=False).squeeze()
127
+ if pos_inds.numel() > 0:
128
+ assigned_labels[pos_inds] = gt_labels[
129
+ assigned_gt_inds[pos_inds] - 1]
130
+ else:
131
+ assigned_labels = None
132
+
133
+ return AssignResult(
134
+ num_gts, assigned_gt_inds, None, labels=assigned_labels)
submodules/chartdete/mmdet/core/bbox/assigners/region_assigner.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+ from mmdet.core import anchor_inside_flags
5
+ from ..builder import BBOX_ASSIGNERS
6
+ from .assign_result import AssignResult
7
+ from .base_assigner import BaseAssigner
8
+
9
+
10
+ def calc_region(bbox, ratio, stride, featmap_size=None):
11
+ """Calculate region of the box defined by the ratio, the ratio is from the
12
+ center of the box to every edge."""
13
+ # project bbox on the feature
14
+ f_bbox = bbox / stride
15
+ x1 = torch.round((1 - ratio) * f_bbox[0] + ratio * f_bbox[2])
16
+ y1 = torch.round((1 - ratio) * f_bbox[1] + ratio * f_bbox[3])
17
+ x2 = torch.round(ratio * f_bbox[0] + (1 - ratio) * f_bbox[2])
18
+ y2 = torch.round(ratio * f_bbox[1] + (1 - ratio) * f_bbox[3])
19
+ if featmap_size is not None:
20
+ x1 = x1.clamp(min=0, max=featmap_size[1])
21
+ y1 = y1.clamp(min=0, max=featmap_size[0])
22
+ x2 = x2.clamp(min=0, max=featmap_size[1])
23
+ y2 = y2.clamp(min=0, max=featmap_size[0])
24
+ return (x1, y1, x2, y2)
25
+
26
+
27
+ def anchor_ctr_inside_region_flags(anchors, stride, region):
28
+ """Get the flag indicate whether anchor centers are inside regions."""
29
+ x1, y1, x2, y2 = region
30
+ f_anchors = anchors / stride
31
+ x = (f_anchors[:, 0] + f_anchors[:, 2]) * 0.5
32
+ y = (f_anchors[:, 1] + f_anchors[:, 3]) * 0.5
33
+ flags = (x >= x1) & (x <= x2) & (y >= y1) & (y <= y2)
34
+ return flags
35
+
36
+
37
+ @BBOX_ASSIGNERS.register_module()
38
+ class RegionAssigner(BaseAssigner):
39
+ """Assign a corresponding gt bbox or background to each bbox.
40
+
41
+ Each proposals will be assigned with `-1`, `0`, or a positive integer
42
+ indicating the ground truth index.
43
+
44
+ - -1: don't care
45
+ - 0: negative sample, no assigned gt
46
+ - positive integer: positive sample, index (1-based) of assigned gt
47
+
48
+ Args:
49
+ center_ratio: ratio of the region in the center of the bbox to
50
+ define positive sample.
51
+ ignore_ratio: ratio of the region to define ignore samples.
52
+ """
53
+
54
+ def __init__(self, center_ratio=0.2, ignore_ratio=0.5):
55
+ self.center_ratio = center_ratio
56
+ self.ignore_ratio = ignore_ratio
57
+
58
+ def assign(self,
59
+ mlvl_anchors,
60
+ mlvl_valid_flags,
61
+ gt_bboxes,
62
+ img_meta,
63
+ featmap_sizes,
64
+ anchor_scale,
65
+ anchor_strides,
66
+ gt_bboxes_ignore=None,
67
+ gt_labels=None,
68
+ allowed_border=0):
69
+ """Assign gt to anchors.
70
+
71
+ This method assign a gt bbox to every bbox (proposal/anchor), each bbox
72
+ will be assigned with -1, 0, or a positive number. -1 means don't care,
73
+ 0 means negative sample, positive number is the index (1-based) of
74
+ assigned gt.
75
+
76
+ The assignment is done in following steps, and the order matters.
77
+
78
+ 1. Assign every anchor to 0 (negative)
79
+ 2. (For each gt_bboxes) Compute ignore flags based on ignore_region
80
+ then assign -1 to anchors w.r.t. ignore flags
81
+ 3. (For each gt_bboxes) Compute pos flags based on center_region then
82
+ assign gt_bboxes to anchors w.r.t. pos flags
83
+ 4. (For each gt_bboxes) Compute ignore flags based on adjacent anchor
84
+ level then assign -1 to anchors w.r.t. ignore flags
85
+ 5. Assign anchor outside of image to -1
86
+
87
+ Args:
88
+ mlvl_anchors (list[Tensor]): Multi level anchors.
89
+ mlvl_valid_flags (list[Tensor]): Multi level valid flags.
90
+ gt_bboxes (Tensor): Ground truth bboxes of image
91
+ img_meta (dict): Meta info of image.
92
+ featmap_sizes (list[Tensor]): Feature mapsize each level
93
+ anchor_scale (int): Scale of the anchor.
94
+ anchor_strides (list[int]): Stride of the anchor.
95
+ gt_bboxes (Tensor): Groundtruth boxes, shape (k, 4).
96
+ gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are
97
+ labelled as `ignored`, e.g., crowd boxes in COCO.
98
+ gt_labels (Tensor, optional): Label of gt_bboxes, shape (k, ).
99
+ allowed_border (int, optional): The border to allow the valid
100
+ anchor. Defaults to 0.
101
+
102
+ Returns:
103
+ :obj:`AssignResult`: The assign result.
104
+ """
105
+ if gt_bboxes_ignore is not None:
106
+ raise NotImplementedError
107
+
108
+ num_gts = gt_bboxes.shape[0]
109
+ num_bboxes = sum(x.shape[0] for x in mlvl_anchors)
110
+
111
+ if num_gts == 0 or num_bboxes == 0:
112
+ # No ground truth or boxes, return empty assignment
113
+ max_overlaps = gt_bboxes.new_zeros((num_bboxes, ))
114
+ assigned_gt_inds = gt_bboxes.new_zeros((num_bboxes, ),
115
+ dtype=torch.long)
116
+ if gt_labels is None:
117
+ assigned_labels = None
118
+ else:
119
+ assigned_labels = gt_bboxes.new_full((num_bboxes, ),
120
+ -1,
121
+ dtype=torch.long)
122
+ return AssignResult(
123
+ num_gts,
124
+ assigned_gt_inds,
125
+ max_overlaps,
126
+ labels=assigned_labels)
127
+
128
+ num_lvls = len(mlvl_anchors)
129
+ r1 = (1 - self.center_ratio) / 2
130
+ r2 = (1 - self.ignore_ratio) / 2
131
+
132
+ scale = torch.sqrt((gt_bboxes[:, 2] - gt_bboxes[:, 0]) *
133
+ (gt_bboxes[:, 3] - gt_bboxes[:, 1]))
134
+ min_anchor_size = scale.new_full(
135
+ (1, ), float(anchor_scale * anchor_strides[0]))
136
+ target_lvls = torch.floor(
137
+ torch.log2(scale) - torch.log2(min_anchor_size) + 0.5)
138
+ target_lvls = target_lvls.clamp(min=0, max=num_lvls - 1).long()
139
+
140
+ # 1. assign 0 (negative) by default
141
+ mlvl_assigned_gt_inds = []
142
+ mlvl_ignore_flags = []
143
+ for lvl in range(num_lvls):
144
+ h, w = featmap_sizes[lvl]
145
+ assert h * w == mlvl_anchors[lvl].shape[0]
146
+ assigned_gt_inds = gt_bboxes.new_full((h * w, ),
147
+ 0,
148
+ dtype=torch.long)
149
+ ignore_flags = torch.zeros_like(assigned_gt_inds)
150
+ mlvl_assigned_gt_inds.append(assigned_gt_inds)
151
+ mlvl_ignore_flags.append(ignore_flags)
152
+
153
+ for gt_id in range(num_gts):
154
+ lvl = target_lvls[gt_id].item()
155
+ featmap_size = featmap_sizes[lvl]
156
+ stride = anchor_strides[lvl]
157
+ anchors = mlvl_anchors[lvl]
158
+ gt_bbox = gt_bboxes[gt_id, :4]
159
+
160
+ # Compute regions
161
+ ignore_region = calc_region(gt_bbox, r2, stride, featmap_size)
162
+ ctr_region = calc_region(gt_bbox, r1, stride, featmap_size)
163
+
164
+ # 2. Assign -1 to ignore flags
165
+ ignore_flags = anchor_ctr_inside_region_flags(
166
+ anchors, stride, ignore_region)
167
+ mlvl_assigned_gt_inds[lvl][ignore_flags] = -1
168
+
169
+ # 3. Assign gt_bboxes to pos flags
170
+ pos_flags = anchor_ctr_inside_region_flags(anchors, stride,
171
+ ctr_region)
172
+ mlvl_assigned_gt_inds[lvl][pos_flags] = gt_id + 1
173
+
174
+ # 4. Assign -1 to ignore adjacent lvl
175
+ if lvl > 0:
176
+ d_lvl = lvl - 1
177
+ d_anchors = mlvl_anchors[d_lvl]
178
+ d_featmap_size = featmap_sizes[d_lvl]
179
+ d_stride = anchor_strides[d_lvl]
180
+ d_ignore_region = calc_region(gt_bbox, r2, d_stride,
181
+ d_featmap_size)
182
+ ignore_flags = anchor_ctr_inside_region_flags(
183
+ d_anchors, d_stride, d_ignore_region)
184
+ mlvl_ignore_flags[d_lvl][ignore_flags] = 1
185
+ if lvl < num_lvls - 1:
186
+ u_lvl = lvl + 1
187
+ u_anchors = mlvl_anchors[u_lvl]
188
+ u_featmap_size = featmap_sizes[u_lvl]
189
+ u_stride = anchor_strides[u_lvl]
190
+ u_ignore_region = calc_region(gt_bbox, r2, u_stride,
191
+ u_featmap_size)
192
+ ignore_flags = anchor_ctr_inside_region_flags(
193
+ u_anchors, u_stride, u_ignore_region)
194
+ mlvl_ignore_flags[u_lvl][ignore_flags] = 1
195
+
196
+ # 4. (cont.) Assign -1 to ignore adjacent lvl
197
+ for lvl in range(num_lvls):
198
+ ignore_flags = mlvl_ignore_flags[lvl]
199
+ mlvl_assigned_gt_inds[lvl][ignore_flags] = -1
200
+
201
+ # 5. Assign -1 to anchor outside of image
202
+ flat_assigned_gt_inds = torch.cat(mlvl_assigned_gt_inds)
203
+ flat_anchors = torch.cat(mlvl_anchors)
204
+ flat_valid_flags = torch.cat(mlvl_valid_flags)
205
+ assert (flat_assigned_gt_inds.shape[0] == flat_anchors.shape[0] ==
206
+ flat_valid_flags.shape[0])
207
+ inside_flags = anchor_inside_flags(flat_anchors, flat_valid_flags,
208
+ img_meta['img_shape'],
209
+ allowed_border)
210
+ outside_flags = ~inside_flags
211
+ flat_assigned_gt_inds[outside_flags] = -1
212
+
213
+ if gt_labels is not None:
214
+ assigned_labels = torch.zeros_like(flat_assigned_gt_inds)
215
+ pos_flags = assigned_gt_inds > 0
216
+ assigned_labels[pos_flags] = gt_labels[
217
+ flat_assigned_gt_inds[pos_flags] - 1]
218
+ else:
219
+ assigned_labels = None
220
+
221
+ return AssignResult(
222
+ num_gts, flat_assigned_gt_inds, None, labels=assigned_labels)
submodules/chartdete/mmdet/core/bbox/assigners/sim_ota_assigner.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import warnings
3
+
4
+ import torch
5
+ import torch.nn.functional as F
6
+
7
+ from ..builder import BBOX_ASSIGNERS
8
+ from ..iou_calculators import bbox_overlaps
9
+ from .assign_result import AssignResult
10
+ from .base_assigner import BaseAssigner
11
+
12
+
13
+ @BBOX_ASSIGNERS.register_module()
14
+ class SimOTAAssigner(BaseAssigner):
15
+ """Computes matching between predictions and ground truth.
16
+
17
+ Args:
18
+ center_radius (int | float, optional): Ground truth center size
19
+ to judge whether a prior is in center. Default 2.5.
20
+ candidate_topk (int, optional): The candidate top-k which used to
21
+ get top-k ious to calculate dynamic-k. Default 10.
22
+ iou_weight (int | float, optional): The scale factor for regression
23
+ iou cost. Default 3.0.
24
+ cls_weight (int | float, optional): The scale factor for classification
25
+ cost. Default 1.0.
26
+ """
27
+
28
+ def __init__(self,
29
+ center_radius=2.5,
30
+ candidate_topk=10,
31
+ iou_weight=3.0,
32
+ cls_weight=1.0):
33
+ self.center_radius = center_radius
34
+ self.candidate_topk = candidate_topk
35
+ self.iou_weight = iou_weight
36
+ self.cls_weight = cls_weight
37
+
38
+ def assign(self,
39
+ pred_scores,
40
+ priors,
41
+ decoded_bboxes,
42
+ gt_bboxes,
43
+ gt_labels,
44
+ gt_bboxes_ignore=None,
45
+ eps=1e-7):
46
+ """Assign gt to priors using SimOTA. It will switch to CPU mode when
47
+ GPU is out of memory.
48
+ Args:
49
+ pred_scores (Tensor): Classification scores of one image,
50
+ a 2D-Tensor with shape [num_priors, num_classes]
51
+ priors (Tensor): All priors of one image, a 2D-Tensor with shape
52
+ [num_priors, 4] in [cx, xy, stride_w, stride_y] format.
53
+ decoded_bboxes (Tensor): Predicted bboxes, a 2D-Tensor with shape
54
+ [num_priors, 4] in [tl_x, tl_y, br_x, br_y] format.
55
+ gt_bboxes (Tensor): Ground truth bboxes of one image, a 2D-Tensor
56
+ with shape [num_gts, 4] in [tl_x, tl_y, br_x, br_y] format.
57
+ gt_labels (Tensor): Ground truth labels of one image, a Tensor
58
+ with shape [num_gts].
59
+ gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are
60
+ labelled as `ignored`, e.g., crowd boxes in COCO.
61
+ eps (float): A value added to the denominator for numerical
62
+ stability. Default 1e-7.
63
+ Returns:
64
+ assign_result (obj:`AssignResult`): The assigned result.
65
+ """
66
+ try:
67
+ assign_result = self._assign(pred_scores, priors, decoded_bboxes,
68
+ gt_bboxes, gt_labels,
69
+ gt_bboxes_ignore, eps)
70
+ return assign_result
71
+ except RuntimeError:
72
+ origin_device = pred_scores.device
73
+ warnings.warn('OOM RuntimeError is raised due to the huge memory '
74
+ 'cost during label assignment. CPU mode is applied '
75
+ 'in this batch. If you want to avoid this issue, '
76
+ 'try to reduce the batch size or image size.')
77
+ torch.cuda.empty_cache()
78
+
79
+ pred_scores = pred_scores.cpu()
80
+ priors = priors.cpu()
81
+ decoded_bboxes = decoded_bboxes.cpu()
82
+ gt_bboxes = gt_bboxes.cpu().float()
83
+ gt_labels = gt_labels.cpu()
84
+
85
+ assign_result = self._assign(pred_scores, priors, decoded_bboxes,
86
+ gt_bboxes, gt_labels,
87
+ gt_bboxes_ignore, eps)
88
+ assign_result.gt_inds = assign_result.gt_inds.to(origin_device)
89
+ assign_result.max_overlaps = assign_result.max_overlaps.to(
90
+ origin_device)
91
+ assign_result.labels = assign_result.labels.to(origin_device)
92
+
93
+ return assign_result
94
+
95
+ def _assign(self,
96
+ pred_scores,
97
+ priors,
98
+ decoded_bboxes,
99
+ gt_bboxes,
100
+ gt_labels,
101
+ gt_bboxes_ignore=None,
102
+ eps=1e-7):
103
+ """Assign gt to priors using SimOTA.
104
+ Args:
105
+ pred_scores (Tensor): Classification scores of one image,
106
+ a 2D-Tensor with shape [num_priors, num_classes]
107
+ priors (Tensor): All priors of one image, a 2D-Tensor with shape
108
+ [num_priors, 4] in [cx, xy, stride_w, stride_y] format.
109
+ decoded_bboxes (Tensor): Predicted bboxes, a 2D-Tensor with shape
110
+ [num_priors, 4] in [tl_x, tl_y, br_x, br_y] format.
111
+ gt_bboxes (Tensor): Ground truth bboxes of one image, a 2D-Tensor
112
+ with shape [num_gts, 4] in [tl_x, tl_y, br_x, br_y] format.
113
+ gt_labels (Tensor): Ground truth labels of one image, a Tensor
114
+ with shape [num_gts].
115
+ gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are
116
+ labelled as `ignored`, e.g., crowd boxes in COCO.
117
+ eps (float): A value added to the denominator for numerical
118
+ stability. Default 1e-7.
119
+ Returns:
120
+ :obj:`AssignResult`: The assigned result.
121
+ """
122
+ INF = 100000.0
123
+ num_gt = gt_bboxes.size(0)
124
+ num_bboxes = decoded_bboxes.size(0)
125
+
126
+ # assign 0 by default
127
+ assigned_gt_inds = decoded_bboxes.new_full((num_bboxes, ),
128
+ 0,
129
+ dtype=torch.long)
130
+ valid_mask, is_in_boxes_and_center = self.get_in_gt_and_in_center_info(
131
+ priors, gt_bboxes)
132
+ valid_decoded_bbox = decoded_bboxes[valid_mask]
133
+ valid_pred_scores = pred_scores[valid_mask]
134
+ num_valid = valid_decoded_bbox.size(0)
135
+
136
+ if num_gt == 0 or num_bboxes == 0 or num_valid == 0:
137
+ # No ground truth or boxes, return empty assignment
138
+ max_overlaps = decoded_bboxes.new_zeros((num_bboxes, ))
139
+ if num_gt == 0:
140
+ # No truth, assign everything to background
141
+ assigned_gt_inds[:] = 0
142
+ if gt_labels is None:
143
+ assigned_labels = None
144
+ else:
145
+ assigned_labels = decoded_bboxes.new_full((num_bboxes, ),
146
+ -1,
147
+ dtype=torch.long)
148
+ return AssignResult(
149
+ num_gt, assigned_gt_inds, max_overlaps, labels=assigned_labels)
150
+
151
+ pairwise_ious = bbox_overlaps(valid_decoded_bbox, gt_bboxes)
152
+ iou_cost = -torch.log(pairwise_ious + eps)
153
+
154
+ gt_onehot_label = (
155
+ F.one_hot(gt_labels.to(torch.int64),
156
+ pred_scores.shape[-1]).float().unsqueeze(0).repeat(
157
+ num_valid, 1, 1))
158
+
159
+ valid_pred_scores = valid_pred_scores.unsqueeze(1).repeat(1, num_gt, 1)
160
+ cls_cost = (
161
+ F.binary_cross_entropy(
162
+ valid_pred_scores.to(dtype=torch.float32).sqrt_(),
163
+ gt_onehot_label,
164
+ reduction='none',
165
+ ).sum(-1).to(dtype=valid_pred_scores.dtype))
166
+
167
+ cost_matrix = (
168
+ cls_cost * self.cls_weight + iou_cost * self.iou_weight +
169
+ (~is_in_boxes_and_center) * INF)
170
+
171
+ matched_pred_ious, matched_gt_inds = \
172
+ self.dynamic_k_matching(
173
+ cost_matrix, pairwise_ious, num_gt, valid_mask)
174
+
175
+ # convert to AssignResult format
176
+ assigned_gt_inds[valid_mask] = matched_gt_inds + 1
177
+ assigned_labels = assigned_gt_inds.new_full((num_bboxes, ), -1)
178
+ assigned_labels[valid_mask] = gt_labels[matched_gt_inds].long()
179
+ max_overlaps = assigned_gt_inds.new_full((num_bboxes, ),
180
+ -INF,
181
+ dtype=torch.float32)
182
+ max_overlaps[valid_mask] = matched_pred_ious
183
+ return AssignResult(
184
+ num_gt, assigned_gt_inds, max_overlaps, labels=assigned_labels)
185
+
186
+ def get_in_gt_and_in_center_info(self, priors, gt_bboxes):
187
+ num_gt = gt_bboxes.size(0)
188
+
189
+ repeated_x = priors[:, 0].unsqueeze(1).repeat(1, num_gt)
190
+ repeated_y = priors[:, 1].unsqueeze(1).repeat(1, num_gt)
191
+ repeated_stride_x = priors[:, 2].unsqueeze(1).repeat(1, num_gt)
192
+ repeated_stride_y = priors[:, 3].unsqueeze(1).repeat(1, num_gt)
193
+
194
+ # is prior centers in gt bboxes, shape: [n_prior, n_gt]
195
+ l_ = repeated_x - gt_bboxes[:, 0]
196
+ t_ = repeated_y - gt_bboxes[:, 1]
197
+ r_ = gt_bboxes[:, 2] - repeated_x
198
+ b_ = gt_bboxes[:, 3] - repeated_y
199
+
200
+ deltas = torch.stack([l_, t_, r_, b_], dim=1)
201
+ is_in_gts = deltas.min(dim=1).values > 0
202
+ is_in_gts_all = is_in_gts.sum(dim=1) > 0
203
+
204
+ # is prior centers in gt centers
205
+ gt_cxs = (gt_bboxes[:, 0] + gt_bboxes[:, 2]) / 2.0
206
+ gt_cys = (gt_bboxes[:, 1] + gt_bboxes[:, 3]) / 2.0
207
+ ct_box_l = gt_cxs - self.center_radius * repeated_stride_x
208
+ ct_box_t = gt_cys - self.center_radius * repeated_stride_y
209
+ ct_box_r = gt_cxs + self.center_radius * repeated_stride_x
210
+ ct_box_b = gt_cys + self.center_radius * repeated_stride_y
211
+
212
+ cl_ = repeated_x - ct_box_l
213
+ ct_ = repeated_y - ct_box_t
214
+ cr_ = ct_box_r - repeated_x
215
+ cb_ = ct_box_b - repeated_y
216
+
217
+ ct_deltas = torch.stack([cl_, ct_, cr_, cb_], dim=1)
218
+ is_in_cts = ct_deltas.min(dim=1).values > 0
219
+ is_in_cts_all = is_in_cts.sum(dim=1) > 0
220
+
221
+ # in boxes or in centers, shape: [num_priors]
222
+ is_in_gts_or_centers = is_in_gts_all | is_in_cts_all
223
+
224
+ # both in boxes and centers, shape: [num_fg, num_gt]
225
+ is_in_boxes_and_centers = (
226
+ is_in_gts[is_in_gts_or_centers, :]
227
+ & is_in_cts[is_in_gts_or_centers, :])
228
+ return is_in_gts_or_centers, is_in_boxes_and_centers
229
+
230
+ def dynamic_k_matching(self, cost, pairwise_ious, num_gt, valid_mask):
231
+ matching_matrix = torch.zeros_like(cost, dtype=torch.uint8)
232
+ # select candidate topk ious for dynamic-k calculation
233
+ candidate_topk = min(self.candidate_topk, pairwise_ious.size(0))
234
+ topk_ious, _ = torch.topk(pairwise_ious, candidate_topk, dim=0)
235
+ # calculate dynamic k for each gt
236
+ dynamic_ks = torch.clamp(topk_ious.sum(0).int(), min=1)
237
+ for gt_idx in range(num_gt):
238
+ _, pos_idx = torch.topk(
239
+ cost[:, gt_idx], k=dynamic_ks[gt_idx], largest=False)
240
+ matching_matrix[:, gt_idx][pos_idx] = 1
241
+
242
+ del topk_ious, dynamic_ks, pos_idx
243
+
244
+ prior_match_gt_mask = matching_matrix.sum(1) > 1
245
+ if prior_match_gt_mask.sum() > 0:
246
+ cost_min, cost_argmin = torch.min(
247
+ cost[prior_match_gt_mask, :], dim=1)
248
+ matching_matrix[prior_match_gt_mask, :] *= 0
249
+ matching_matrix[prior_match_gt_mask, cost_argmin] = 1
250
+ # get foreground mask inside box and center prior
251
+ fg_mask_inboxes = matching_matrix.sum(1) > 0
252
+ valid_mask[valid_mask.clone()] = fg_mask_inboxes
253
+
254
+ matched_gt_inds = matching_matrix[fg_mask_inboxes, :].argmax(1)
255
+ matched_pred_ious = (matching_matrix *
256
+ pairwise_ious).sum(1)[fg_mask_inboxes]
257
+ return matched_pred_ious, matched_gt_inds
submodules/chartdete/mmdet/core/bbox/assigners/task_aligned_assigner.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+ from ..builder import BBOX_ASSIGNERS
5
+ from ..iou_calculators import build_iou_calculator
6
+ from .assign_result import AssignResult
7
+ from .base_assigner import BaseAssigner
8
+
9
+ INF = 100000000
10
+
11
+
12
+ @BBOX_ASSIGNERS.register_module()
13
+ class TaskAlignedAssigner(BaseAssigner):
14
+ """Task aligned assigner used in the paper:
15
+ `TOOD: Task-aligned One-stage Object Detection.
16
+ <https://arxiv.org/abs/2108.07755>`_.
17
+
18
+ Assign a corresponding gt bbox or background to each predicted bbox.
19
+ Each bbox will be assigned with `0` or a positive integer
20
+ indicating the ground truth index.
21
+
22
+ - 0: negative sample, no assigned gt
23
+ - positive integer: positive sample, index (1-based) of assigned gt
24
+
25
+ Args:
26
+ topk (int): number of bbox selected in each level
27
+ iou_calculator (dict): Config dict for iou calculator.
28
+ Default: dict(type='BboxOverlaps2D')
29
+ """
30
+
31
+ def __init__(self, topk, iou_calculator=dict(type='BboxOverlaps2D')):
32
+ assert topk >= 1
33
+ self.topk = topk
34
+ self.iou_calculator = build_iou_calculator(iou_calculator)
35
+
36
+ def assign(self,
37
+ pred_scores,
38
+ decode_bboxes,
39
+ anchors,
40
+ gt_bboxes,
41
+ gt_bboxes_ignore=None,
42
+ gt_labels=None,
43
+ alpha=1,
44
+ beta=6):
45
+ """Assign gt to bboxes.
46
+
47
+ The assignment is done in following steps
48
+
49
+ 1. compute alignment metric between all bbox (bbox of all pyramid
50
+ levels) and gt
51
+ 2. select top-k bbox as candidates for each gt
52
+ 3. limit the positive sample's center in gt (because the anchor-free
53
+ detector only can predict positive distance)
54
+
55
+
56
+ Args:
57
+ pred_scores (Tensor): predicted class probability,
58
+ shape(n, num_classes)
59
+ decode_bboxes (Tensor): predicted bounding boxes, shape(n, 4)
60
+ anchors (Tensor): pre-defined anchors, shape(n, 4).
61
+ gt_bboxes (Tensor): Groundtruth boxes, shape (k, 4).
62
+ gt_bboxes_ignore (Tensor, optional): Ground truth bboxes that are
63
+ labelled as `ignored`, e.g., crowd boxes in COCO.
64
+ gt_labels (Tensor, optional): Label of gt_bboxes, shape (k, ).
65
+
66
+ Returns:
67
+ :obj:`TaskAlignedAssignResult`: The assign result.
68
+ """
69
+ anchors = anchors[:, :4]
70
+ num_gt, num_bboxes = gt_bboxes.size(0), anchors.size(0)
71
+ # compute alignment metric between all bbox and gt
72
+ overlaps = self.iou_calculator(decode_bboxes, gt_bboxes).detach()
73
+ bbox_scores = pred_scores[:, gt_labels].detach()
74
+ # assign 0 by default
75
+ assigned_gt_inds = anchors.new_full((num_bboxes, ),
76
+ 0,
77
+ dtype=torch.long)
78
+ assign_metrics = anchors.new_zeros((num_bboxes, ))
79
+
80
+ if num_gt == 0 or num_bboxes == 0:
81
+ # No ground truth or boxes, return empty assignment
82
+ max_overlaps = anchors.new_zeros((num_bboxes, ))
83
+ if num_gt == 0:
84
+ # No gt boxes, assign everything to background
85
+ assigned_gt_inds[:] = 0
86
+ if gt_labels is None:
87
+ assigned_labels = None
88
+ else:
89
+ assigned_labels = anchors.new_full((num_bboxes, ),
90
+ -1,
91
+ dtype=torch.long)
92
+ assign_result = AssignResult(
93
+ num_gt, assigned_gt_inds, max_overlaps, labels=assigned_labels)
94
+ assign_result.assign_metrics = assign_metrics
95
+ return assign_result
96
+
97
+ # select top-k bboxes as candidates for each gt
98
+ alignment_metrics = bbox_scores**alpha * overlaps**beta
99
+ topk = min(self.topk, alignment_metrics.size(0))
100
+ _, candidate_idxs = alignment_metrics.topk(topk, dim=0, largest=True)
101
+ candidate_metrics = alignment_metrics[candidate_idxs,
102
+ torch.arange(num_gt)]
103
+ is_pos = candidate_metrics > 0
104
+
105
+ # limit the positive sample's center in gt
106
+ anchors_cx = (anchors[:, 0] + anchors[:, 2]) / 2.0
107
+ anchors_cy = (anchors[:, 1] + anchors[:, 3]) / 2.0
108
+ for gt_idx in range(num_gt):
109
+ candidate_idxs[:, gt_idx] += gt_idx * num_bboxes
110
+ ep_anchors_cx = anchors_cx.view(1, -1).expand(
111
+ num_gt, num_bboxes).contiguous().view(-1)
112
+ ep_anchors_cy = anchors_cy.view(1, -1).expand(
113
+ num_gt, num_bboxes).contiguous().view(-1)
114
+ candidate_idxs = candidate_idxs.view(-1)
115
+
116
+ # calculate the left, top, right, bottom distance between positive
117
+ # bbox center and gt side
118
+ l_ = ep_anchors_cx[candidate_idxs].view(-1, num_gt) - gt_bboxes[:, 0]
119
+ t_ = ep_anchors_cy[candidate_idxs].view(-1, num_gt) - gt_bboxes[:, 1]
120
+ r_ = gt_bboxes[:, 2] - ep_anchors_cx[candidate_idxs].view(-1, num_gt)
121
+ b_ = gt_bboxes[:, 3] - ep_anchors_cy[candidate_idxs].view(-1, num_gt)
122
+ is_in_gts = torch.stack([l_, t_, r_, b_], dim=1).min(dim=1)[0] > 0.01
123
+ is_pos = is_pos & is_in_gts
124
+
125
+ # if an anchor box is assigned to multiple gts,
126
+ # the one with the highest iou will be selected.
127
+ overlaps_inf = torch.full_like(overlaps,
128
+ -INF).t().contiguous().view(-1)
129
+ index = candidate_idxs.view(-1)[is_pos.view(-1)]
130
+ overlaps_inf[index] = overlaps.t().contiguous().view(-1)[index]
131
+ overlaps_inf = overlaps_inf.view(num_gt, -1).t()
132
+
133
+ max_overlaps, argmax_overlaps = overlaps_inf.max(dim=1)
134
+ assigned_gt_inds[
135
+ max_overlaps != -INF] = argmax_overlaps[max_overlaps != -INF] + 1
136
+ assign_metrics[max_overlaps != -INF] = alignment_metrics[
137
+ max_overlaps != -INF, argmax_overlaps[max_overlaps != -INF]]
138
+
139
+ if gt_labels is not None:
140
+ assigned_labels = assigned_gt_inds.new_full((num_bboxes, ), -1)
141
+ pos_inds = torch.nonzero(
142
+ assigned_gt_inds > 0, as_tuple=False).squeeze()
143
+ if pos_inds.numel() > 0:
144
+ assigned_labels[pos_inds] = gt_labels[
145
+ assigned_gt_inds[pos_inds] - 1]
146
+ else:
147
+ assigned_labels = None
148
+ assign_result = AssignResult(
149
+ num_gt, assigned_gt_inds, max_overlaps, labels=assigned_labels)
150
+ assign_result.assign_metrics = assign_metrics
151
+ return assign_result
submodules/chartdete/mmdet/core/bbox/assigners/uniform_assigner.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import torch
3
+
4
+ from ..builder import BBOX_ASSIGNERS
5
+ from ..iou_calculators import build_iou_calculator
6
+ from ..transforms import bbox_xyxy_to_cxcywh
7
+ from .assign_result import AssignResult
8
+ from .base_assigner import BaseAssigner
9
+
10
+
11
+ @BBOX_ASSIGNERS.register_module()
12
+ class UniformAssigner(BaseAssigner):
13
+ """Uniform Matching between the anchors and gt boxes, which can achieve
14
+ balance in positive anchors, and gt_bboxes_ignore was not considered for
15
+ now.
16
+
17
+ Args:
18
+ pos_ignore_thr (float): the threshold to ignore positive anchors
19
+ neg_ignore_thr (float): the threshold to ignore negative anchors
20
+ match_times(int): Number of positive anchors for each gt box.
21
+ Default 4.
22
+ iou_calculator (dict): iou_calculator config
23
+ """
24
+
25
+ def __init__(self,
26
+ pos_ignore_thr,
27
+ neg_ignore_thr,
28
+ match_times=4,
29
+ iou_calculator=dict(type='BboxOverlaps2D')):
30
+ self.match_times = match_times
31
+ self.pos_ignore_thr = pos_ignore_thr
32
+ self.neg_ignore_thr = neg_ignore_thr
33
+ self.iou_calculator = build_iou_calculator(iou_calculator)
34
+
35
+ def assign(self,
36
+ bbox_pred,
37
+ anchor,
38
+ gt_bboxes,
39
+ gt_bboxes_ignore=None,
40
+ gt_labels=None):
41
+ num_gts, num_bboxes = gt_bboxes.size(0), bbox_pred.size(0)
42
+
43
+ # 1. assign -1 by default
44
+ assigned_gt_inds = bbox_pred.new_full((num_bboxes, ),
45
+ 0,
46
+ dtype=torch.long)
47
+ assigned_labels = bbox_pred.new_full((num_bboxes, ),
48
+ -1,
49
+ dtype=torch.long)
50
+ if num_gts == 0 or num_bboxes == 0:
51
+ # No ground truth or boxes, return empty assignment
52
+ if num_gts == 0:
53
+ # No ground truth, assign all to background
54
+ assigned_gt_inds[:] = 0
55
+ assign_result = AssignResult(
56
+ num_gts, assigned_gt_inds, None, labels=assigned_labels)
57
+ assign_result.set_extra_property(
58
+ 'pos_idx', bbox_pred.new_empty(0, dtype=torch.bool))
59
+ assign_result.set_extra_property('pos_predicted_boxes',
60
+ bbox_pred.new_empty((0, 4)))
61
+ assign_result.set_extra_property('target_boxes',
62
+ bbox_pred.new_empty((0, 4)))
63
+ return assign_result
64
+
65
+ # 2. Compute the L1 cost between boxes
66
+ # Note that we use anchors and predict boxes both
67
+ cost_bbox = torch.cdist(
68
+ bbox_xyxy_to_cxcywh(bbox_pred),
69
+ bbox_xyxy_to_cxcywh(gt_bboxes),
70
+ p=1)
71
+ cost_bbox_anchors = torch.cdist(
72
+ bbox_xyxy_to_cxcywh(anchor), bbox_xyxy_to_cxcywh(gt_bboxes), p=1)
73
+
74
+ # We found that topk function has different results in cpu and
75
+ # cuda mode. In order to ensure consistency with the source code,
76
+ # we also use cpu mode.
77
+ # TODO: Check whether the performance of cpu and cuda are the same.
78
+ C = cost_bbox.cpu()
79
+ C1 = cost_bbox_anchors.cpu()
80
+
81
+ # self.match_times x n
82
+ index = torch.topk(
83
+ C, # c=b,n,x c[i]=n,x
84
+ k=self.match_times,
85
+ dim=0,
86
+ largest=False)[1]
87
+
88
+ # self.match_times x n
89
+ index1 = torch.topk(C1, k=self.match_times, dim=0, largest=False)[1]
90
+ # (self.match_times*2) x n
91
+ indexes = torch.cat((index, index1),
92
+ dim=1).reshape(-1).to(bbox_pred.device)
93
+
94
+ pred_overlaps = self.iou_calculator(bbox_pred, gt_bboxes)
95
+ anchor_overlaps = self.iou_calculator(anchor, gt_bboxes)
96
+ pred_max_overlaps, _ = pred_overlaps.max(dim=1)
97
+ anchor_max_overlaps, _ = anchor_overlaps.max(dim=0)
98
+
99
+ # 3. Compute the ignore indexes use gt_bboxes and predict boxes
100
+ ignore_idx = pred_max_overlaps > self.neg_ignore_thr
101
+ assigned_gt_inds[ignore_idx] = -1
102
+
103
+ # 4. Compute the ignore indexes of positive sample use anchors
104
+ # and predict boxes
105
+ pos_gt_index = torch.arange(
106
+ 0, C1.size(1),
107
+ device=bbox_pred.device).repeat(self.match_times * 2)
108
+ pos_ious = anchor_overlaps[indexes, pos_gt_index]
109
+ pos_ignore_idx = pos_ious < self.pos_ignore_thr
110
+
111
+ pos_gt_index_with_ignore = pos_gt_index + 1
112
+ pos_gt_index_with_ignore[pos_ignore_idx] = -1
113
+ assigned_gt_inds[indexes] = pos_gt_index_with_ignore
114
+
115
+ if gt_labels is not None:
116
+ assigned_labels = assigned_gt_inds.new_full((num_bboxes, ), -1)
117
+ pos_inds = torch.nonzero(
118
+ assigned_gt_inds > 0, as_tuple=False).squeeze()
119
+ if pos_inds.numel() > 0:
120
+ assigned_labels[pos_inds] = gt_labels[
121
+ assigned_gt_inds[pos_inds] - 1]
122
+ else:
123
+ assigned_labels = None
124
+
125
+ assign_result = AssignResult(
126
+ num_gts,
127
+ assigned_gt_inds,
128
+ anchor_max_overlaps,
129
+ labels=assigned_labels)
130
+ assign_result.set_extra_property('pos_idx', ~pos_ignore_idx)
131
+ assign_result.set_extra_property('pos_predicted_boxes',
132
+ bbox_pred[indexes])
133
+ assign_result.set_extra_property('target_boxes',
134
+ gt_bboxes[pos_gt_index])
135
+ return assign_result
submodules/chartdete/mmdet/core/bbox/builder.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from mmcv.utils import Registry, build_from_cfg
3
+
4
+ BBOX_ASSIGNERS = Registry('bbox_assigner')
5
+ BBOX_SAMPLERS = Registry('bbox_sampler')
6
+ BBOX_CODERS = Registry('bbox_coder')
7
+
8
+
9
+ def build_assigner(cfg, **default_args):
10
+ """Builder of box assigner."""
11
+ return build_from_cfg(cfg, BBOX_ASSIGNERS, default_args)
12
+
13
+
14
+ def build_sampler(cfg, **default_args):
15
+ """Builder of box sampler."""
16
+ return build_from_cfg(cfg, BBOX_SAMPLERS, default_args)
17
+
18
+
19
+ def build_bbox_coder(cfg, **default_args):
20
+ """Builder of box coder."""
21
+ return build_from_cfg(cfg, BBOX_CODERS, default_args)
submodules/chartdete/mmdet/core/bbox/coder/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from .base_bbox_coder import BaseBBoxCoder
3
+ from .bucketing_bbox_coder import BucketingBBoxCoder
4
+ from .delta_xywh_bbox_coder import DeltaXYWHBBoxCoder
5
+ from .distance_point_bbox_coder import DistancePointBBoxCoder
6
+ from .legacy_delta_xywh_bbox_coder import LegacyDeltaXYWHBBoxCoder
7
+ from .pseudo_bbox_coder import PseudoBBoxCoder
8
+ from .tblr_bbox_coder import TBLRBBoxCoder
9
+ from .yolo_bbox_coder import YOLOBBoxCoder
10
+
11
+ __all__ = [
12
+ 'BaseBBoxCoder', 'PseudoBBoxCoder', 'DeltaXYWHBBoxCoder',
13
+ 'LegacyDeltaXYWHBBoxCoder', 'TBLRBBoxCoder', 'YOLOBBoxCoder',
14
+ 'BucketingBBoxCoder', 'DistancePointBBoxCoder'
15
+ ]
submodules/chartdete/mmdet/core/bbox/coder/base_bbox_coder.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from abc import ABCMeta, abstractmethod
3
+
4
+
5
+ class BaseBBoxCoder(metaclass=ABCMeta):
6
+ """Base bounding box coder."""
7
+
8
+ def __init__(self, **kwargs):
9
+ pass
10
+
11
+ @abstractmethod
12
+ def encode(self, bboxes, gt_bboxes):
13
+ """Encode deltas between bboxes and ground truth boxes."""
14
+
15
+ @abstractmethod
16
+ def decode(self, bboxes, bboxes_pred):
17
+ """Decode the predicted bboxes according to prediction and base
18
+ boxes."""
submodules/chartdete/mmdet/core/bbox/coder/bucketing_bbox_coder.py ADDED
@@ -0,0 +1,351 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import mmcv
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn.functional as F
6
+
7
+ from ..builder import BBOX_CODERS
8
+ from ..transforms import bbox_rescale
9
+ from .base_bbox_coder import BaseBBoxCoder
10
+
11
+
12
+ @BBOX_CODERS.register_module()
13
+ class BucketingBBoxCoder(BaseBBoxCoder):
14
+ """Bucketing BBox Coder for Side-Aware Boundary Localization (SABL).
15
+
16
+ Boundary Localization with Bucketing and Bucketing Guided Rescoring
17
+ are implemented here.
18
+
19
+ Please refer to https://arxiv.org/abs/1912.04260 for more details.
20
+
21
+ Args:
22
+ num_buckets (int): Number of buckets.
23
+ scale_factor (int): Scale factor of proposals to generate buckets.
24
+ offset_topk (int): Topk buckets are used to generate
25
+ bucket fine regression targets. Defaults to 2.
26
+ offset_upperbound (float): Offset upperbound to generate
27
+ bucket fine regression targets.
28
+ To avoid too large offset displacements. Defaults to 1.0.
29
+ cls_ignore_neighbor (bool): Ignore second nearest bucket or Not.
30
+ Defaults to True.
31
+ clip_border (bool, optional): Whether clip the objects outside the
32
+ border of the image. Defaults to True.
33
+ """
34
+
35
+ def __init__(self,
36
+ num_buckets,
37
+ scale_factor,
38
+ offset_topk=2,
39
+ offset_upperbound=1.0,
40
+ cls_ignore_neighbor=True,
41
+ clip_border=True):
42
+ super(BucketingBBoxCoder, self).__init__()
43
+ self.num_buckets = num_buckets
44
+ self.scale_factor = scale_factor
45
+ self.offset_topk = offset_topk
46
+ self.offset_upperbound = offset_upperbound
47
+ self.cls_ignore_neighbor = cls_ignore_neighbor
48
+ self.clip_border = clip_border
49
+
50
+ def encode(self, bboxes, gt_bboxes):
51
+ """Get bucketing estimation and fine regression targets during
52
+ training.
53
+
54
+ Args:
55
+ bboxes (torch.Tensor): source boxes, e.g., object proposals.
56
+ gt_bboxes (torch.Tensor): target of the transformation, e.g.,
57
+ ground truth boxes.
58
+
59
+ Returns:
60
+ encoded_bboxes(tuple[Tensor]): bucketing estimation
61
+ and fine regression targets and weights
62
+ """
63
+
64
+ assert bboxes.size(0) == gt_bboxes.size(0)
65
+ assert bboxes.size(-1) == gt_bboxes.size(-1) == 4
66
+ encoded_bboxes = bbox2bucket(bboxes, gt_bboxes, self.num_buckets,
67
+ self.scale_factor, self.offset_topk,
68
+ self.offset_upperbound,
69
+ self.cls_ignore_neighbor)
70
+ return encoded_bboxes
71
+
72
+ def decode(self, bboxes, pred_bboxes, max_shape=None):
73
+ """Apply transformation `pred_bboxes` to `boxes`.
74
+ Args:
75
+ boxes (torch.Tensor): Basic boxes.
76
+ pred_bboxes (torch.Tensor): Predictions for bucketing estimation
77
+ and fine regression
78
+ max_shape (tuple[int], optional): Maximum shape of boxes.
79
+ Defaults to None.
80
+
81
+ Returns:
82
+ torch.Tensor: Decoded boxes.
83
+ """
84
+ assert len(pred_bboxes) == 2
85
+ cls_preds, offset_preds = pred_bboxes
86
+ assert cls_preds.size(0) == bboxes.size(0) and offset_preds.size(
87
+ 0) == bboxes.size(0)
88
+ decoded_bboxes = bucket2bbox(bboxes, cls_preds, offset_preds,
89
+ self.num_buckets, self.scale_factor,
90
+ max_shape, self.clip_border)
91
+
92
+ return decoded_bboxes
93
+
94
+
95
+ @mmcv.jit(coderize=True)
96
+ def generat_buckets(proposals, num_buckets, scale_factor=1.0):
97
+ """Generate buckets w.r.t bucket number and scale factor of proposals.
98
+
99
+ Args:
100
+ proposals (Tensor): Shape (n, 4)
101
+ num_buckets (int): Number of buckets.
102
+ scale_factor (float): Scale factor to rescale proposals.
103
+
104
+ Returns:
105
+ tuple[Tensor]: (bucket_w, bucket_h, l_buckets, r_buckets,
106
+ t_buckets, d_buckets)
107
+
108
+ - bucket_w: Width of buckets on x-axis. Shape (n, ).
109
+ - bucket_h: Height of buckets on y-axis. Shape (n, ).
110
+ - l_buckets: Left buckets. Shape (n, ceil(side_num/2)).
111
+ - r_buckets: Right buckets. Shape (n, ceil(side_num/2)).
112
+ - t_buckets: Top buckets. Shape (n, ceil(side_num/2)).
113
+ - d_buckets: Down buckets. Shape (n, ceil(side_num/2)).
114
+ """
115
+ proposals = bbox_rescale(proposals, scale_factor)
116
+
117
+ # number of buckets in each side
118
+ side_num = int(np.ceil(num_buckets / 2.0))
119
+ pw = proposals[..., 2] - proposals[..., 0]
120
+ ph = proposals[..., 3] - proposals[..., 1]
121
+ px1 = proposals[..., 0]
122
+ py1 = proposals[..., 1]
123
+ px2 = proposals[..., 2]
124
+ py2 = proposals[..., 3]
125
+
126
+ bucket_w = pw / num_buckets
127
+ bucket_h = ph / num_buckets
128
+
129
+ # left buckets
130
+ l_buckets = px1[:, None] + (0.5 + torch.arange(
131
+ 0, side_num).to(proposals).float())[None, :] * bucket_w[:, None]
132
+ # right buckets
133
+ r_buckets = px2[:, None] - (0.5 + torch.arange(
134
+ 0, side_num).to(proposals).float())[None, :] * bucket_w[:, None]
135
+ # top buckets
136
+ t_buckets = py1[:, None] + (0.5 + torch.arange(
137
+ 0, side_num).to(proposals).float())[None, :] * bucket_h[:, None]
138
+ # down buckets
139
+ d_buckets = py2[:, None] - (0.5 + torch.arange(
140
+ 0, side_num).to(proposals).float())[None, :] * bucket_h[:, None]
141
+ return bucket_w, bucket_h, l_buckets, r_buckets, t_buckets, d_buckets
142
+
143
+
144
+ @mmcv.jit(coderize=True)
145
+ def bbox2bucket(proposals,
146
+ gt,
147
+ num_buckets,
148
+ scale_factor,
149
+ offset_topk=2,
150
+ offset_upperbound=1.0,
151
+ cls_ignore_neighbor=True):
152
+ """Generate buckets estimation and fine regression targets.
153
+
154
+ Args:
155
+ proposals (Tensor): Shape (n, 4)
156
+ gt (Tensor): Shape (n, 4)
157
+ num_buckets (int): Number of buckets.
158
+ scale_factor (float): Scale factor to rescale proposals.
159
+ offset_topk (int): Topk buckets are used to generate
160
+ bucket fine regression targets. Defaults to 2.
161
+ offset_upperbound (float): Offset allowance to generate
162
+ bucket fine regression targets.
163
+ To avoid too large offset displacements. Defaults to 1.0.
164
+ cls_ignore_neighbor (bool): Ignore second nearest bucket or Not.
165
+ Defaults to True.
166
+
167
+ Returns:
168
+ tuple[Tensor]: (offsets, offsets_weights, bucket_labels, cls_weights).
169
+
170
+ - offsets: Fine regression targets. \
171
+ Shape (n, num_buckets*2).
172
+ - offsets_weights: Fine regression weights. \
173
+ Shape (n, num_buckets*2).
174
+ - bucket_labels: Bucketing estimation labels. \
175
+ Shape (n, num_buckets*2).
176
+ - cls_weights: Bucketing estimation weights. \
177
+ Shape (n, num_buckets*2).
178
+ """
179
+ assert proposals.size() == gt.size()
180
+
181
+ # generate buckets
182
+ proposals = proposals.float()
183
+ gt = gt.float()
184
+ (bucket_w, bucket_h, l_buckets, r_buckets, t_buckets,
185
+ d_buckets) = generat_buckets(proposals, num_buckets, scale_factor)
186
+
187
+ gx1 = gt[..., 0]
188
+ gy1 = gt[..., 1]
189
+ gx2 = gt[..., 2]
190
+ gy2 = gt[..., 3]
191
+
192
+ # generate offset targets and weights
193
+ # offsets from buckets to gts
194
+ l_offsets = (l_buckets - gx1[:, None]) / bucket_w[:, None]
195
+ r_offsets = (r_buckets - gx2[:, None]) / bucket_w[:, None]
196
+ t_offsets = (t_buckets - gy1[:, None]) / bucket_h[:, None]
197
+ d_offsets = (d_buckets - gy2[:, None]) / bucket_h[:, None]
198
+
199
+ # select top-k nearest buckets
200
+ l_topk, l_label = l_offsets.abs().topk(
201
+ offset_topk, dim=1, largest=False, sorted=True)
202
+ r_topk, r_label = r_offsets.abs().topk(
203
+ offset_topk, dim=1, largest=False, sorted=True)
204
+ t_topk, t_label = t_offsets.abs().topk(
205
+ offset_topk, dim=1, largest=False, sorted=True)
206
+ d_topk, d_label = d_offsets.abs().topk(
207
+ offset_topk, dim=1, largest=False, sorted=True)
208
+
209
+ offset_l_weights = l_offsets.new_zeros(l_offsets.size())
210
+ offset_r_weights = r_offsets.new_zeros(r_offsets.size())
211
+ offset_t_weights = t_offsets.new_zeros(t_offsets.size())
212
+ offset_d_weights = d_offsets.new_zeros(d_offsets.size())
213
+ inds = torch.arange(0, proposals.size(0)).to(proposals).long()
214
+
215
+ # generate offset weights of top-k nearest buckets
216
+ for k in range(offset_topk):
217
+ if k >= 1:
218
+ offset_l_weights[inds, l_label[:,
219
+ k]] = (l_topk[:, k] <
220
+ offset_upperbound).float()
221
+ offset_r_weights[inds, r_label[:,
222
+ k]] = (r_topk[:, k] <
223
+ offset_upperbound).float()
224
+ offset_t_weights[inds, t_label[:,
225
+ k]] = (t_topk[:, k] <
226
+ offset_upperbound).float()
227
+ offset_d_weights[inds, d_label[:,
228
+ k]] = (d_topk[:, k] <
229
+ offset_upperbound).float()
230
+ else:
231
+ offset_l_weights[inds, l_label[:, k]] = 1.0
232
+ offset_r_weights[inds, r_label[:, k]] = 1.0
233
+ offset_t_weights[inds, t_label[:, k]] = 1.0
234
+ offset_d_weights[inds, d_label[:, k]] = 1.0
235
+
236
+ offsets = torch.cat([l_offsets, r_offsets, t_offsets, d_offsets], dim=-1)
237
+ offsets_weights = torch.cat([
238
+ offset_l_weights, offset_r_weights, offset_t_weights, offset_d_weights
239
+ ],
240
+ dim=-1)
241
+
242
+ # generate bucket labels and weight
243
+ side_num = int(np.ceil(num_buckets / 2.0))
244
+ labels = torch.stack(
245
+ [l_label[:, 0], r_label[:, 0], t_label[:, 0], d_label[:, 0]], dim=-1)
246
+
247
+ batch_size = labels.size(0)
248
+ bucket_labels = F.one_hot(labels.view(-1), side_num).view(batch_size,
249
+ -1).float()
250
+ bucket_cls_l_weights = (l_offsets.abs() < 1).float()
251
+ bucket_cls_r_weights = (r_offsets.abs() < 1).float()
252
+ bucket_cls_t_weights = (t_offsets.abs() < 1).float()
253
+ bucket_cls_d_weights = (d_offsets.abs() < 1).float()
254
+ bucket_cls_weights = torch.cat([
255
+ bucket_cls_l_weights, bucket_cls_r_weights, bucket_cls_t_weights,
256
+ bucket_cls_d_weights
257
+ ],
258
+ dim=-1)
259
+ # ignore second nearest buckets for cls if necessary
260
+ if cls_ignore_neighbor:
261
+ bucket_cls_weights = (~((bucket_cls_weights == 1) &
262
+ (bucket_labels == 0))).float()
263
+ else:
264
+ bucket_cls_weights[:] = 1.0
265
+ return offsets, offsets_weights, bucket_labels, bucket_cls_weights
266
+
267
+
268
+ @mmcv.jit(coderize=True)
269
+ def bucket2bbox(proposals,
270
+ cls_preds,
271
+ offset_preds,
272
+ num_buckets,
273
+ scale_factor=1.0,
274
+ max_shape=None,
275
+ clip_border=True):
276
+ """Apply bucketing estimation (cls preds) and fine regression (offset
277
+ preds) to generate det bboxes.
278
+
279
+ Args:
280
+ proposals (Tensor): Boxes to be transformed. Shape (n, 4)
281
+ cls_preds (Tensor): bucketing estimation. Shape (n, num_buckets*2).
282
+ offset_preds (Tensor): fine regression. Shape (n, num_buckets*2).
283
+ num_buckets (int): Number of buckets.
284
+ scale_factor (float): Scale factor to rescale proposals.
285
+ max_shape (tuple[int, int]): Maximum bounds for boxes. specifies (H, W)
286
+ clip_border (bool, optional): Whether clip the objects outside the
287
+ border of the image. Defaults to True.
288
+
289
+ Returns:
290
+ tuple[Tensor]: (bboxes, loc_confidence).
291
+
292
+ - bboxes: predicted bboxes. Shape (n, 4)
293
+ - loc_confidence: localization confidence of predicted bboxes.
294
+ Shape (n,).
295
+ """
296
+
297
+ side_num = int(np.ceil(num_buckets / 2.0))
298
+ cls_preds = cls_preds.view(-1, side_num)
299
+ offset_preds = offset_preds.view(-1, side_num)
300
+
301
+ scores = F.softmax(cls_preds, dim=1)
302
+ score_topk, score_label = scores.topk(2, dim=1, largest=True, sorted=True)
303
+
304
+ rescaled_proposals = bbox_rescale(proposals, scale_factor)
305
+
306
+ pw = rescaled_proposals[..., 2] - rescaled_proposals[..., 0]
307
+ ph = rescaled_proposals[..., 3] - rescaled_proposals[..., 1]
308
+ px1 = rescaled_proposals[..., 0]
309
+ py1 = rescaled_proposals[..., 1]
310
+ px2 = rescaled_proposals[..., 2]
311
+ py2 = rescaled_proposals[..., 3]
312
+
313
+ bucket_w = pw / num_buckets
314
+ bucket_h = ph / num_buckets
315
+
316
+ score_inds_l = score_label[0::4, 0]
317
+ score_inds_r = score_label[1::4, 0]
318
+ score_inds_t = score_label[2::4, 0]
319
+ score_inds_d = score_label[3::4, 0]
320
+ l_buckets = px1 + (0.5 + score_inds_l.float()) * bucket_w
321
+ r_buckets = px2 - (0.5 + score_inds_r.float()) * bucket_w
322
+ t_buckets = py1 + (0.5 + score_inds_t.float()) * bucket_h
323
+ d_buckets = py2 - (0.5 + score_inds_d.float()) * bucket_h
324
+
325
+ offsets = offset_preds.view(-1, 4, side_num)
326
+ inds = torch.arange(proposals.size(0)).to(proposals).long()
327
+ l_offsets = offsets[:, 0, :][inds, score_inds_l]
328
+ r_offsets = offsets[:, 1, :][inds, score_inds_r]
329
+ t_offsets = offsets[:, 2, :][inds, score_inds_t]
330
+ d_offsets = offsets[:, 3, :][inds, score_inds_d]
331
+
332
+ x1 = l_buckets - l_offsets * bucket_w
333
+ x2 = r_buckets - r_offsets * bucket_w
334
+ y1 = t_buckets - t_offsets * bucket_h
335
+ y2 = d_buckets - d_offsets * bucket_h
336
+
337
+ if clip_border and max_shape is not None:
338
+ x1 = x1.clamp(min=0, max=max_shape[1] - 1)
339
+ y1 = y1.clamp(min=0, max=max_shape[0] - 1)
340
+ x2 = x2.clamp(min=0, max=max_shape[1] - 1)
341
+ y2 = y2.clamp(min=0, max=max_shape[0] - 1)
342
+ bboxes = torch.cat([x1[:, None], y1[:, None], x2[:, None], y2[:, None]],
343
+ dim=-1)
344
+
345
+ # bucketing guided rescoring
346
+ loc_confidence = score_topk[:, 0]
347
+ top2_neighbor_inds = (score_label[:, 0] - score_label[:, 1]).abs() == 1
348
+ loc_confidence += score_topk[:, 1] * top2_neighbor_inds.float()
349
+ loc_confidence = loc_confidence.view(-1, 4).mean(dim=1)
350
+
351
+ return bboxes, loc_confidence
submodules/chartdete/mmdet/core/bbox/coder/delta_xywh_bbox_coder.py ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import warnings
3
+
4
+ import mmcv
5
+ import numpy as np
6
+ import torch
7
+
8
+ from ..builder import BBOX_CODERS
9
+ from .base_bbox_coder import BaseBBoxCoder
10
+
11
+
12
+ @BBOX_CODERS.register_module()
13
+ class DeltaXYWHBBoxCoder(BaseBBoxCoder):
14
+ """Delta XYWH BBox coder.
15
+
16
+ Following the practice in `R-CNN <https://arxiv.org/abs/1311.2524>`_,
17
+ this coder encodes bbox (x1, y1, x2, y2) into delta (dx, dy, dw, dh) and
18
+ decodes delta (dx, dy, dw, dh) back to original bbox (x1, y1, x2, y2).
19
+
20
+ Args:
21
+ target_means (Sequence[float]): Denormalizing means of target for
22
+ delta coordinates
23
+ target_stds (Sequence[float]): Denormalizing standard deviation of
24
+ target for delta coordinates
25
+ clip_border (bool, optional): Whether clip the objects outside the
26
+ border of the image. Defaults to True.
27
+ add_ctr_clamp (bool): Whether to add center clamp, when added, the
28
+ predicted box is clamped is its center is too far away from
29
+ the original anchor's center. Only used by YOLOF. Default False.
30
+ ctr_clamp (int): the maximum pixel shift to clamp. Only used by YOLOF.
31
+ Default 32.
32
+ """
33
+
34
+ def __init__(self,
35
+ target_means=(0., 0., 0., 0.),
36
+ target_stds=(1., 1., 1., 1.),
37
+ clip_border=True,
38
+ add_ctr_clamp=False,
39
+ ctr_clamp=32):
40
+ super(BaseBBoxCoder, self).__init__()
41
+ self.means = target_means
42
+ self.stds = target_stds
43
+ self.clip_border = clip_border
44
+ self.add_ctr_clamp = add_ctr_clamp
45
+ self.ctr_clamp = ctr_clamp
46
+
47
+ def encode(self, bboxes, gt_bboxes):
48
+ """Get box regression transformation deltas that can be used to
49
+ transform the ``bboxes`` into the ``gt_bboxes``.
50
+
51
+ Args:
52
+ bboxes (torch.Tensor): Source boxes, e.g., object proposals.
53
+ gt_bboxes (torch.Tensor): Target of the transformation, e.g.,
54
+ ground-truth boxes.
55
+
56
+ Returns:
57
+ torch.Tensor: Box transformation deltas
58
+ """
59
+
60
+ assert bboxes.size(0) == gt_bboxes.size(0)
61
+ assert bboxes.size(-1) == gt_bboxes.size(-1) == 4
62
+ encoded_bboxes = bbox2delta(bboxes, gt_bboxes, self.means, self.stds)
63
+ return encoded_bboxes
64
+
65
+ def decode(self,
66
+ bboxes,
67
+ pred_bboxes,
68
+ max_shape=None,
69
+ wh_ratio_clip=16 / 1000):
70
+ """Apply transformation `pred_bboxes` to `boxes`.
71
+
72
+ Args:
73
+ bboxes (torch.Tensor): Basic boxes. Shape (B, N, 4) or (N, 4)
74
+ pred_bboxes (Tensor): Encoded offsets with respect to each roi.
75
+ Has shape (B, N, num_classes * 4) or (B, N, 4) or
76
+ (N, num_classes * 4) or (N, 4). Note N = num_anchors * W * H
77
+ when rois is a grid of anchors.Offset encoding follows [1]_.
78
+ max_shape (Sequence[int] or torch.Tensor or Sequence[
79
+ Sequence[int]],optional): Maximum bounds for boxes, specifies
80
+ (H, W, C) or (H, W). If bboxes shape is (B, N, 4), then
81
+ the max_shape should be a Sequence[Sequence[int]]
82
+ and the length of max_shape should also be B.
83
+ wh_ratio_clip (float, optional): The allowed ratio between
84
+ width and height.
85
+
86
+ Returns:
87
+ torch.Tensor: Decoded boxes.
88
+ """
89
+
90
+ assert pred_bboxes.size(0) == bboxes.size(0)
91
+ if pred_bboxes.ndim == 3:
92
+ assert pred_bboxes.size(1) == bboxes.size(1)
93
+
94
+ if pred_bboxes.ndim == 2 and not torch.onnx.is_in_onnx_export():
95
+ # single image decode
96
+ decoded_bboxes = delta2bbox(bboxes, pred_bboxes, self.means,
97
+ self.stds, max_shape, wh_ratio_clip,
98
+ self.clip_border, self.add_ctr_clamp,
99
+ self.ctr_clamp)
100
+ else:
101
+ if pred_bboxes.ndim == 3 and not torch.onnx.is_in_onnx_export():
102
+ warnings.warn(
103
+ 'DeprecationWarning: onnx_delta2bbox is deprecated '
104
+ 'in the case of batch decoding and non-ONNX, '
105
+ 'please use “delta2bbox” instead. In order to improve '
106
+ 'the decoding speed, the batch function will no '
107
+ 'longer be supported. ')
108
+ decoded_bboxes = onnx_delta2bbox(bboxes, pred_bboxes, self.means,
109
+ self.stds, max_shape,
110
+ wh_ratio_clip, self.clip_border,
111
+ self.add_ctr_clamp,
112
+ self.ctr_clamp)
113
+
114
+ return decoded_bboxes
115
+
116
+
117
+ @mmcv.jit(coderize=True)
118
+ def bbox2delta(proposals, gt, means=(0., 0., 0., 0.), stds=(1., 1., 1., 1.)):
119
+ """Compute deltas of proposals w.r.t. gt.
120
+
121
+ We usually compute the deltas of x, y, w, h of proposals w.r.t ground
122
+ truth bboxes to get regression target.
123
+ This is the inverse function of :func:`delta2bbox`.
124
+
125
+ Args:
126
+ proposals (Tensor): Boxes to be transformed, shape (N, ..., 4)
127
+ gt (Tensor): Gt bboxes to be used as base, shape (N, ..., 4)
128
+ means (Sequence[float]): Denormalizing means for delta coordinates
129
+ stds (Sequence[float]): Denormalizing standard deviation for delta
130
+ coordinates
131
+
132
+ Returns:
133
+ Tensor: deltas with shape (N, 4), where columns represent dx, dy,
134
+ dw, dh.
135
+ """
136
+ assert proposals.size() == gt.size()
137
+
138
+ proposals = proposals.float()
139
+ gt = gt.float()
140
+ px = (proposals[..., 0] + proposals[..., 2]) * 0.5
141
+ py = (proposals[..., 1] + proposals[..., 3]) * 0.5
142
+ pw = proposals[..., 2] - proposals[..., 0]
143
+ ph = proposals[..., 3] - proposals[..., 1]
144
+
145
+ gx = (gt[..., 0] + gt[..., 2]) * 0.5
146
+ gy = (gt[..., 1] + gt[..., 3]) * 0.5
147
+ gw = gt[..., 2] - gt[..., 0]
148
+ gh = gt[..., 3] - gt[..., 1]
149
+
150
+ dx = (gx - px) / pw
151
+ dy = (gy - py) / ph
152
+ dw = torch.log(gw / pw)
153
+ dh = torch.log(gh / ph)
154
+ deltas = torch.stack([dx, dy, dw, dh], dim=-1)
155
+
156
+ means = deltas.new_tensor(means).unsqueeze(0)
157
+ stds = deltas.new_tensor(stds).unsqueeze(0)
158
+ deltas = deltas.sub_(means).div_(stds)
159
+
160
+ return deltas
161
+
162
+
163
+ @mmcv.jit(coderize=True)
164
+ def delta2bbox(rois,
165
+ deltas,
166
+ means=(0., 0., 0., 0.),
167
+ stds=(1., 1., 1., 1.),
168
+ max_shape=None,
169
+ wh_ratio_clip=16 / 1000,
170
+ clip_border=True,
171
+ add_ctr_clamp=False,
172
+ ctr_clamp=32):
173
+ """Apply deltas to shift/scale base boxes.
174
+
175
+ Typically the rois are anchor or proposed bounding boxes and the deltas are
176
+ network outputs used to shift/scale those boxes.
177
+ This is the inverse function of :func:`bbox2delta`.
178
+
179
+ Args:
180
+ rois (Tensor): Boxes to be transformed. Has shape (N, 4).
181
+ deltas (Tensor): Encoded offsets relative to each roi.
182
+ Has shape (N, num_classes * 4) or (N, 4). Note
183
+ N = num_base_anchors * W * H, when rois is a grid of
184
+ anchors. Offset encoding follows [1]_.
185
+ means (Sequence[float]): Denormalizing means for delta coordinates.
186
+ Default (0., 0., 0., 0.).
187
+ stds (Sequence[float]): Denormalizing standard deviation for delta
188
+ coordinates. Default (1., 1., 1., 1.).
189
+ max_shape (tuple[int, int]): Maximum bounds for boxes, specifies
190
+ (H, W). Default None.
191
+ wh_ratio_clip (float): Maximum aspect ratio for boxes. Default
192
+ 16 / 1000.
193
+ clip_border (bool, optional): Whether clip the objects outside the
194
+ border of the image. Default True.
195
+ add_ctr_clamp (bool): Whether to add center clamp. When set to True,
196
+ the center of the prediction bounding box will be clamped to
197
+ avoid being too far away from the center of the anchor.
198
+ Only used by YOLOF. Default False.
199
+ ctr_clamp (int): the maximum pixel shift to clamp. Only used by YOLOF.
200
+ Default 32.
201
+
202
+ Returns:
203
+ Tensor: Boxes with shape (N, num_classes * 4) or (N, 4), where 4
204
+ represent tl_x, tl_y, br_x, br_y.
205
+
206
+ References:
207
+ .. [1] https://arxiv.org/abs/1311.2524
208
+
209
+ Example:
210
+ >>> rois = torch.Tensor([[ 0., 0., 1., 1.],
211
+ >>> [ 0., 0., 1., 1.],
212
+ >>> [ 0., 0., 1., 1.],
213
+ >>> [ 5., 5., 5., 5.]])
214
+ >>> deltas = torch.Tensor([[ 0., 0., 0., 0.],
215
+ >>> [ 1., 1., 1., 1.],
216
+ >>> [ 0., 0., 2., -1.],
217
+ >>> [ 0.7, -1.9, -0.5, 0.3]])
218
+ >>> delta2bbox(rois, deltas, max_shape=(32, 32, 3))
219
+ tensor([[0.0000, 0.0000, 1.0000, 1.0000],
220
+ [0.1409, 0.1409, 2.8591, 2.8591],
221
+ [0.0000, 0.3161, 4.1945, 0.6839],
222
+ [5.0000, 5.0000, 5.0000, 5.0000]])
223
+ """
224
+ num_bboxes, num_classes = deltas.size(0), deltas.size(1) // 4
225
+ if num_bboxes == 0:
226
+ return deltas
227
+
228
+ deltas = deltas.reshape(-1, 4)
229
+
230
+ means = deltas.new_tensor(means).view(1, -1)
231
+ stds = deltas.new_tensor(stds).view(1, -1)
232
+ denorm_deltas = deltas * stds + means
233
+
234
+ dxy = denorm_deltas[:, :2]
235
+ dwh = denorm_deltas[:, 2:]
236
+
237
+ # Compute width/height of each roi
238
+ rois_ = rois.repeat(1, num_classes).reshape(-1, 4)
239
+ pxy = ((rois_[:, :2] + rois_[:, 2:]) * 0.5)
240
+ pwh = (rois_[:, 2:] - rois_[:, :2])
241
+
242
+ dxy_wh = pwh * dxy
243
+
244
+ max_ratio = np.abs(np.log(wh_ratio_clip))
245
+ if add_ctr_clamp:
246
+ dxy_wh = torch.clamp(dxy_wh, max=ctr_clamp, min=-ctr_clamp)
247
+ dwh = torch.clamp(dwh, max=max_ratio)
248
+ else:
249
+ dwh = dwh.clamp(min=-max_ratio, max=max_ratio)
250
+
251
+ gxy = pxy + dxy_wh
252
+ gwh = pwh * dwh.exp()
253
+ x1y1 = gxy - (gwh * 0.5)
254
+ x2y2 = gxy + (gwh * 0.5)
255
+ bboxes = torch.cat([x1y1, x2y2], dim=-1)
256
+ if clip_border and max_shape is not None:
257
+ bboxes[..., 0::2].clamp_(min=0, max=max_shape[1])
258
+ bboxes[..., 1::2].clamp_(min=0, max=max_shape[0])
259
+ bboxes = bboxes.reshape(num_bboxes, -1)
260
+ return bboxes
261
+
262
+
263
+ def onnx_delta2bbox(rois,
264
+ deltas,
265
+ means=(0., 0., 0., 0.),
266
+ stds=(1., 1., 1., 1.),
267
+ max_shape=None,
268
+ wh_ratio_clip=16 / 1000,
269
+ clip_border=True,
270
+ add_ctr_clamp=False,
271
+ ctr_clamp=32):
272
+ """Apply deltas to shift/scale base boxes.
273
+
274
+ Typically the rois are anchor or proposed bounding boxes and the deltas are
275
+ network outputs used to shift/scale those boxes.
276
+ This is the inverse function of :func:`bbox2delta`.
277
+
278
+ Args:
279
+ rois (Tensor): Boxes to be transformed. Has shape (N, 4) or (B, N, 4)
280
+ deltas (Tensor): Encoded offsets with respect to each roi.
281
+ Has shape (B, N, num_classes * 4) or (B, N, 4) or
282
+ (N, num_classes * 4) or (N, 4). Note N = num_anchors * W * H
283
+ when rois is a grid of anchors.Offset encoding follows [1]_.
284
+ means (Sequence[float]): Denormalizing means for delta coordinates.
285
+ Default (0., 0., 0., 0.).
286
+ stds (Sequence[float]): Denormalizing standard deviation for delta
287
+ coordinates. Default (1., 1., 1., 1.).
288
+ max_shape (Sequence[int] or torch.Tensor or Sequence[
289
+ Sequence[int]],optional): Maximum bounds for boxes, specifies
290
+ (H, W, C) or (H, W). If rois shape is (B, N, 4), then
291
+ the max_shape should be a Sequence[Sequence[int]]
292
+ and the length of max_shape should also be B. Default None.
293
+ wh_ratio_clip (float): Maximum aspect ratio for boxes.
294
+ Default 16 / 1000.
295
+ clip_border (bool, optional): Whether clip the objects outside the
296
+ border of the image. Default True.
297
+ add_ctr_clamp (bool): Whether to add center clamp, when added, the
298
+ predicted box is clamped is its center is too far away from
299
+ the original anchor's center. Only used by YOLOF. Default False.
300
+ ctr_clamp (int): the maximum pixel shift to clamp. Only used by YOLOF.
301
+ Default 32.
302
+
303
+ Returns:
304
+ Tensor: Boxes with shape (B, N, num_classes * 4) or (B, N, 4) or
305
+ (N, num_classes * 4) or (N, 4), where 4 represent
306
+ tl_x, tl_y, br_x, br_y.
307
+
308
+ References:
309
+ .. [1] https://arxiv.org/abs/1311.2524
310
+
311
+ Example:
312
+ >>> rois = torch.Tensor([[ 0., 0., 1., 1.],
313
+ >>> [ 0., 0., 1., 1.],
314
+ >>> [ 0., 0., 1., 1.],
315
+ >>> [ 5., 5., 5., 5.]])
316
+ >>> deltas = torch.Tensor([[ 0., 0., 0., 0.],
317
+ >>> [ 1., 1., 1., 1.],
318
+ >>> [ 0., 0., 2., -1.],
319
+ >>> [ 0.7, -1.9, -0.5, 0.3]])
320
+ >>> delta2bbox(rois, deltas, max_shape=(32, 32, 3))
321
+ tensor([[0.0000, 0.0000, 1.0000, 1.0000],
322
+ [0.1409, 0.1409, 2.8591, 2.8591],
323
+ [0.0000, 0.3161, 4.1945, 0.6839],
324
+ [5.0000, 5.0000, 5.0000, 5.0000]])
325
+ """
326
+ means = deltas.new_tensor(means).view(1,
327
+ -1).repeat(1,
328
+ deltas.size(-1) // 4)
329
+ stds = deltas.new_tensor(stds).view(1, -1).repeat(1, deltas.size(-1) // 4)
330
+ denorm_deltas = deltas * stds + means
331
+ dx = denorm_deltas[..., 0::4]
332
+ dy = denorm_deltas[..., 1::4]
333
+ dw = denorm_deltas[..., 2::4]
334
+ dh = denorm_deltas[..., 3::4]
335
+
336
+ x1, y1 = rois[..., 0], rois[..., 1]
337
+ x2, y2 = rois[..., 2], rois[..., 3]
338
+ # Compute center of each roi
339
+ px = ((x1 + x2) * 0.5).unsqueeze(-1).expand_as(dx)
340
+ py = ((y1 + y2) * 0.5).unsqueeze(-1).expand_as(dy)
341
+ # Compute width/height of each roi
342
+ pw = (x2 - x1).unsqueeze(-1).expand_as(dw)
343
+ ph = (y2 - y1).unsqueeze(-1).expand_as(dh)
344
+
345
+ dx_width = pw * dx
346
+ dy_height = ph * dy
347
+
348
+ max_ratio = np.abs(np.log(wh_ratio_clip))
349
+ if add_ctr_clamp:
350
+ dx_width = torch.clamp(dx_width, max=ctr_clamp, min=-ctr_clamp)
351
+ dy_height = torch.clamp(dy_height, max=ctr_clamp, min=-ctr_clamp)
352
+ dw = torch.clamp(dw, max=max_ratio)
353
+ dh = torch.clamp(dh, max=max_ratio)
354
+ else:
355
+ dw = dw.clamp(min=-max_ratio, max=max_ratio)
356
+ dh = dh.clamp(min=-max_ratio, max=max_ratio)
357
+ # Use exp(network energy) to enlarge/shrink each roi
358
+ gw = pw * dw.exp()
359
+ gh = ph * dh.exp()
360
+ # Use network energy to shift the center of each roi
361
+ gx = px + dx_width
362
+ gy = py + dy_height
363
+ # Convert center-xy/width/height to top-left, bottom-right
364
+ x1 = gx - gw * 0.5
365
+ y1 = gy - gh * 0.5
366
+ x2 = gx + gw * 0.5
367
+ y2 = gy + gh * 0.5
368
+
369
+ bboxes = torch.stack([x1, y1, x2, y2], dim=-1).view(deltas.size())
370
+
371
+ if clip_border and max_shape is not None:
372
+ # clip bboxes with dynamic `min` and `max` for onnx
373
+ if torch.onnx.is_in_onnx_export():
374
+ from mmdet.core.export import dynamic_clip_for_onnx
375
+ x1, y1, x2, y2 = dynamic_clip_for_onnx(x1, y1, x2, y2, max_shape)
376
+ bboxes = torch.stack([x1, y1, x2, y2], dim=-1).view(deltas.size())
377
+ return bboxes
378
+ if not isinstance(max_shape, torch.Tensor):
379
+ max_shape = x1.new_tensor(max_shape)
380
+ max_shape = max_shape[..., :2].type_as(x1)
381
+ if max_shape.ndim == 2:
382
+ assert bboxes.ndim == 3
383
+ assert max_shape.size(0) == bboxes.size(0)
384
+
385
+ min_xy = x1.new_tensor(0)
386
+ max_xy = torch.cat(
387
+ [max_shape] * (deltas.size(-1) // 2),
388
+ dim=-1).flip(-1).unsqueeze(-2)
389
+ bboxes = torch.where(bboxes < min_xy, min_xy, bboxes)
390
+ bboxes = torch.where(bboxes > max_xy, max_xy, bboxes)
391
+
392
+ return bboxes
submodules/chartdete/mmdet/core/bbox/coder/distance_point_bbox_coder.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from ..builder import BBOX_CODERS
3
+ from ..transforms import bbox2distance, distance2bbox
4
+ from .base_bbox_coder import BaseBBoxCoder
5
+
6
+
7
+ @BBOX_CODERS.register_module()
8
+ class DistancePointBBoxCoder(BaseBBoxCoder):
9
+ """Distance Point BBox coder.
10
+
11
+ This coder encodes gt bboxes (x1, y1, x2, y2) into (top, bottom, left,
12
+ right) and decode it back to the original.
13
+
14
+ Args:
15
+ clip_border (bool, optional): Whether clip the objects outside the
16
+ border of the image. Defaults to True.
17
+ """
18
+
19
+ def __init__(self, clip_border=True):
20
+ super(BaseBBoxCoder, self).__init__()
21
+ self.clip_border = clip_border
22
+
23
+ def encode(self, points, gt_bboxes, max_dis=None, eps=0.1):
24
+ """Encode bounding box to distances.
25
+
26
+ Args:
27
+ points (Tensor): Shape (N, 2), The format is [x, y].
28
+ gt_bboxes (Tensor): Shape (N, 4), The format is "xyxy"
29
+ max_dis (float): Upper bound of the distance. Default None.
30
+ eps (float): a small value to ensure target < max_dis, instead <=.
31
+ Default 0.1.
32
+
33
+ Returns:
34
+ Tensor: Box transformation deltas. The shape is (N, 4).
35
+ """
36
+ assert points.size(0) == gt_bboxes.size(0)
37
+ assert points.size(-1) == 2
38
+ assert gt_bboxes.size(-1) == 4
39
+ return bbox2distance(points, gt_bboxes, max_dis, eps)
40
+
41
+ def decode(self, points, pred_bboxes, max_shape=None):
42
+ """Decode distance prediction to bounding box.
43
+
44
+ Args:
45
+ points (Tensor): Shape (B, N, 2) or (N, 2).
46
+ pred_bboxes (Tensor): Distance from the given point to 4
47
+ boundaries (left, top, right, bottom). Shape (B, N, 4)
48
+ or (N, 4)
49
+ max_shape (Sequence[int] or torch.Tensor or Sequence[
50
+ Sequence[int]],optional): Maximum bounds for boxes, specifies
51
+ (H, W, C) or (H, W). If priors shape is (B, N, 4), then
52
+ the max_shape should be a Sequence[Sequence[int]],
53
+ and the length of max_shape should also be B.
54
+ Default None.
55
+ Returns:
56
+ Tensor: Boxes with shape (N, 4) or (B, N, 4)
57
+ """
58
+ assert points.size(0) == pred_bboxes.size(0)
59
+ assert points.size(-1) == 2
60
+ assert pred_bboxes.size(-1) == 4
61
+ if self.clip_border is False:
62
+ max_shape = None
63
+ return distance2bbox(points, pred_bboxes, max_shape)
submodules/chartdete/mmdet/core/bbox/coder/legacy_delta_xywh_bbox_coder.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import mmcv
3
+ import numpy as np
4
+ import torch
5
+
6
+ from ..builder import BBOX_CODERS
7
+ from .base_bbox_coder import BaseBBoxCoder
8
+
9
+
10
+ @BBOX_CODERS.register_module()
11
+ class LegacyDeltaXYWHBBoxCoder(BaseBBoxCoder):
12
+ """Legacy Delta XYWH BBox coder used in MMDet V1.x.
13
+
14
+ Following the practice in R-CNN [1]_, this coder encodes bbox (x1, y1, x2,
15
+ y2) into delta (dx, dy, dw, dh) and decodes delta (dx, dy, dw, dh)
16
+ back to original bbox (x1, y1, x2, y2).
17
+
18
+ Note:
19
+ The main difference between :class`LegacyDeltaXYWHBBoxCoder` and
20
+ :class:`DeltaXYWHBBoxCoder` is whether ``+ 1`` is used during width and
21
+ height calculation. We suggest to only use this coder when testing with
22
+ MMDet V1.x models.
23
+
24
+ References:
25
+ .. [1] https://arxiv.org/abs/1311.2524
26
+
27
+ Args:
28
+ target_means (Sequence[float]): denormalizing means of target for
29
+ delta coordinates
30
+ target_stds (Sequence[float]): denormalizing standard deviation of
31
+ target for delta coordinates
32
+ """
33
+
34
+ def __init__(self,
35
+ target_means=(0., 0., 0., 0.),
36
+ target_stds=(1., 1., 1., 1.)):
37
+ super(BaseBBoxCoder, self).__init__()
38
+ self.means = target_means
39
+ self.stds = target_stds
40
+
41
+ def encode(self, bboxes, gt_bboxes):
42
+ """Get box regression transformation deltas that can be used to
43
+ transform the ``bboxes`` into the ``gt_bboxes``.
44
+
45
+ Args:
46
+ bboxes (torch.Tensor): source boxes, e.g., object proposals.
47
+ gt_bboxes (torch.Tensor): target of the transformation, e.g.,
48
+ ground-truth boxes.
49
+
50
+ Returns:
51
+ torch.Tensor: Box transformation deltas
52
+ """
53
+ assert bboxes.size(0) == gt_bboxes.size(0)
54
+ assert bboxes.size(-1) == gt_bboxes.size(-1) == 4
55
+ encoded_bboxes = legacy_bbox2delta(bboxes, gt_bboxes, self.means,
56
+ self.stds)
57
+ return encoded_bboxes
58
+
59
+ def decode(self,
60
+ bboxes,
61
+ pred_bboxes,
62
+ max_shape=None,
63
+ wh_ratio_clip=16 / 1000):
64
+ """Apply transformation `pred_bboxes` to `boxes`.
65
+
66
+ Args:
67
+ boxes (torch.Tensor): Basic boxes.
68
+ pred_bboxes (torch.Tensor): Encoded boxes with shape
69
+ max_shape (tuple[int], optional): Maximum shape of boxes.
70
+ Defaults to None.
71
+ wh_ratio_clip (float, optional): The allowed ratio between
72
+ width and height.
73
+
74
+ Returns:
75
+ torch.Tensor: Decoded boxes.
76
+ """
77
+ assert pred_bboxes.size(0) == bboxes.size(0)
78
+ decoded_bboxes = legacy_delta2bbox(bboxes, pred_bboxes, self.means,
79
+ self.stds, max_shape, wh_ratio_clip)
80
+
81
+ return decoded_bboxes
82
+
83
+
84
+ @mmcv.jit(coderize=True)
85
+ def legacy_bbox2delta(proposals,
86
+ gt,
87
+ means=(0., 0., 0., 0.),
88
+ stds=(1., 1., 1., 1.)):
89
+ """Compute deltas of proposals w.r.t. gt in the MMDet V1.x manner.
90
+
91
+ We usually compute the deltas of x, y, w, h of proposals w.r.t ground
92
+ truth bboxes to get regression target.
93
+ This is the inverse function of `delta2bbox()`
94
+
95
+ Args:
96
+ proposals (Tensor): Boxes to be transformed, shape (N, ..., 4)
97
+ gt (Tensor): Gt bboxes to be used as base, shape (N, ..., 4)
98
+ means (Sequence[float]): Denormalizing means for delta coordinates
99
+ stds (Sequence[float]): Denormalizing standard deviation for delta
100
+ coordinates
101
+
102
+ Returns:
103
+ Tensor: deltas with shape (N, 4), where columns represent dx, dy,
104
+ dw, dh.
105
+ """
106
+ assert proposals.size() == gt.size()
107
+
108
+ proposals = proposals.float()
109
+ gt = gt.float()
110
+ px = (proposals[..., 0] + proposals[..., 2]) * 0.5
111
+ py = (proposals[..., 1] + proposals[..., 3]) * 0.5
112
+ pw = proposals[..., 2] - proposals[..., 0] + 1.0
113
+ ph = proposals[..., 3] - proposals[..., 1] + 1.0
114
+
115
+ gx = (gt[..., 0] + gt[..., 2]) * 0.5
116
+ gy = (gt[..., 1] + gt[..., 3]) * 0.5
117
+ gw = gt[..., 2] - gt[..., 0] + 1.0
118
+ gh = gt[..., 3] - gt[..., 1] + 1.0
119
+
120
+ dx = (gx - px) / pw
121
+ dy = (gy - py) / ph
122
+ dw = torch.log(gw / pw)
123
+ dh = torch.log(gh / ph)
124
+ deltas = torch.stack([dx, dy, dw, dh], dim=-1)
125
+
126
+ means = deltas.new_tensor(means).unsqueeze(0)
127
+ stds = deltas.new_tensor(stds).unsqueeze(0)
128
+ deltas = deltas.sub_(means).div_(stds)
129
+
130
+ return deltas
131
+
132
+
133
+ @mmcv.jit(coderize=True)
134
+ def legacy_delta2bbox(rois,
135
+ deltas,
136
+ means=(0., 0., 0., 0.),
137
+ stds=(1., 1., 1., 1.),
138
+ max_shape=None,
139
+ wh_ratio_clip=16 / 1000):
140
+ """Apply deltas to shift/scale base boxes in the MMDet V1.x manner.
141
+
142
+ Typically the rois are anchor or proposed bounding boxes and the deltas are
143
+ network outputs used to shift/scale those boxes.
144
+ This is the inverse function of `bbox2delta()`
145
+
146
+ Args:
147
+ rois (Tensor): Boxes to be transformed. Has shape (N, 4)
148
+ deltas (Tensor): Encoded offsets with respect to each roi.
149
+ Has shape (N, 4 * num_classes). Note N = num_anchors * W * H when
150
+ rois is a grid of anchors. Offset encoding follows [1]_.
151
+ means (Sequence[float]): Denormalizing means for delta coordinates
152
+ stds (Sequence[float]): Denormalizing standard deviation for delta
153
+ coordinates
154
+ max_shape (tuple[int, int]): Maximum bounds for boxes. specifies (H, W)
155
+ wh_ratio_clip (float): Maximum aspect ratio for boxes.
156
+
157
+ Returns:
158
+ Tensor: Boxes with shape (N, 4), where columns represent
159
+ tl_x, tl_y, br_x, br_y.
160
+
161
+ References:
162
+ .. [1] https://arxiv.org/abs/1311.2524
163
+
164
+ Example:
165
+ >>> rois = torch.Tensor([[ 0., 0., 1., 1.],
166
+ >>> [ 0., 0., 1., 1.],
167
+ >>> [ 0., 0., 1., 1.],
168
+ >>> [ 5., 5., 5., 5.]])
169
+ >>> deltas = torch.Tensor([[ 0., 0., 0., 0.],
170
+ >>> [ 1., 1., 1., 1.],
171
+ >>> [ 0., 0., 2., -1.],
172
+ >>> [ 0.7, -1.9, -0.5, 0.3]])
173
+ >>> legacy_delta2bbox(rois, deltas, max_shape=(32, 32))
174
+ tensor([[0.0000, 0.0000, 1.5000, 1.5000],
175
+ [0.0000, 0.0000, 5.2183, 5.2183],
176
+ [0.0000, 0.1321, 7.8891, 0.8679],
177
+ [5.3967, 2.4251, 6.0033, 3.7749]])
178
+ """
179
+ means = deltas.new_tensor(means).repeat(1, deltas.size(1) // 4)
180
+ stds = deltas.new_tensor(stds).repeat(1, deltas.size(1) // 4)
181
+ denorm_deltas = deltas * stds + means
182
+ dx = denorm_deltas[:, 0::4]
183
+ dy = denorm_deltas[:, 1::4]
184
+ dw = denorm_deltas[:, 2::4]
185
+ dh = denorm_deltas[:, 3::4]
186
+ max_ratio = np.abs(np.log(wh_ratio_clip))
187
+ dw = dw.clamp(min=-max_ratio, max=max_ratio)
188
+ dh = dh.clamp(min=-max_ratio, max=max_ratio)
189
+ # Compute center of each roi
190
+ px = ((rois[:, 0] + rois[:, 2]) * 0.5).unsqueeze(1).expand_as(dx)
191
+ py = ((rois[:, 1] + rois[:, 3]) * 0.5).unsqueeze(1).expand_as(dy)
192
+ # Compute width/height of each roi
193
+ pw = (rois[:, 2] - rois[:, 0] + 1.0).unsqueeze(1).expand_as(dw)
194
+ ph = (rois[:, 3] - rois[:, 1] + 1.0).unsqueeze(1).expand_as(dh)
195
+ # Use exp(network energy) to enlarge/shrink each roi
196
+ gw = pw * dw.exp()
197
+ gh = ph * dh.exp()
198
+ # Use network energy to shift the center of each roi
199
+ gx = px + pw * dx
200
+ gy = py + ph * dy
201
+ # Convert center-xy/width/height to top-left, bottom-right
202
+
203
+ # The true legacy box coder should +- 0.5 here.
204
+ # However, current implementation improves the performance when testing
205
+ # the models trained in MMDetection 1.X (~0.5 bbox AP, 0.2 mask AP)
206
+ x1 = gx - gw * 0.5
207
+ y1 = gy - gh * 0.5
208
+ x2 = gx + gw * 0.5
209
+ y2 = gy + gh * 0.5
210
+ if max_shape is not None:
211
+ x1 = x1.clamp(min=0, max=max_shape[1] - 1)
212
+ y1 = y1.clamp(min=0, max=max_shape[0] - 1)
213
+ x2 = x2.clamp(min=0, max=max_shape[1] - 1)
214
+ y2 = y2.clamp(min=0, max=max_shape[0] - 1)
215
+ bboxes = torch.stack([x1, y1, x2, y2], dim=-1).view_as(deltas)
216
+ return bboxes
submodules/chartdete/mmdet/core/bbox/coder/pseudo_bbox_coder.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from ..builder import BBOX_CODERS
3
+ from .base_bbox_coder import BaseBBoxCoder
4
+
5
+
6
+ @BBOX_CODERS.register_module()
7
+ class PseudoBBoxCoder(BaseBBoxCoder):
8
+ """Pseudo bounding box coder."""
9
+
10
+ def __init__(self, **kwargs):
11
+ super(BaseBBoxCoder, self).__init__(**kwargs)
12
+
13
+ def encode(self, bboxes, gt_bboxes):
14
+ """torch.Tensor: return the given ``bboxes``"""
15
+ return gt_bboxes
16
+
17
+ def decode(self, bboxes, pred_bboxes):
18
+ """torch.Tensor: return the given ``pred_bboxes``"""
19
+ return pred_bboxes
submodules/chartdete/mmdet/core/bbox/coder/tblr_bbox_coder.py ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import mmcv
3
+ import torch
4
+
5
+ from ..builder import BBOX_CODERS
6
+ from .base_bbox_coder import BaseBBoxCoder
7
+
8
+
9
+ @BBOX_CODERS.register_module()
10
+ class TBLRBBoxCoder(BaseBBoxCoder):
11
+ """TBLR BBox coder.
12
+
13
+ Following the practice in `FSAF <https://arxiv.org/abs/1903.00621>`_,
14
+ this coder encodes gt bboxes (x1, y1, x2, y2) into (top, bottom, left,
15
+ right) and decode it back to the original.
16
+
17
+ Args:
18
+ normalizer (list | float): Normalization factor to be
19
+ divided with when coding the coordinates. If it is a list, it should
20
+ have length of 4 indicating normalization factor in tblr dims.
21
+ Otherwise it is a unified float factor for all dims. Default: 4.0
22
+ clip_border (bool, optional): Whether clip the objects outside the
23
+ border of the image. Defaults to True.
24
+ """
25
+
26
+ def __init__(self, normalizer=4.0, clip_border=True):
27
+ super(BaseBBoxCoder, self).__init__()
28
+ self.normalizer = normalizer
29
+ self.clip_border = clip_border
30
+
31
+ def encode(self, bboxes, gt_bboxes):
32
+ """Get box regression transformation deltas that can be used to
33
+ transform the ``bboxes`` into the ``gt_bboxes`` in the (top, left,
34
+ bottom, right) order.
35
+
36
+ Args:
37
+ bboxes (torch.Tensor): source boxes, e.g., object proposals.
38
+ gt_bboxes (torch.Tensor): target of the transformation, e.g.,
39
+ ground truth boxes.
40
+
41
+ Returns:
42
+ torch.Tensor: Box transformation deltas
43
+ """
44
+ assert bboxes.size(0) == gt_bboxes.size(0)
45
+ assert bboxes.size(-1) == gt_bboxes.size(-1) == 4
46
+ encoded_bboxes = bboxes2tblr(
47
+ bboxes, gt_bboxes, normalizer=self.normalizer)
48
+ return encoded_bboxes
49
+
50
+ def decode(self, bboxes, pred_bboxes, max_shape=None):
51
+ """Apply transformation `pred_bboxes` to `boxes`.
52
+
53
+ Args:
54
+ bboxes (torch.Tensor): Basic boxes.Shape (B, N, 4) or (N, 4)
55
+ pred_bboxes (torch.Tensor): Encoded boxes with shape
56
+ (B, N, 4) or (N, 4)
57
+ max_shape (Sequence[int] or torch.Tensor or Sequence[
58
+ Sequence[int]],optional): Maximum bounds for boxes, specifies
59
+ (H, W, C) or (H, W). If bboxes shape is (B, N, 4), then
60
+ the max_shape should be a Sequence[Sequence[int]]
61
+ and the length of max_shape should also be B.
62
+
63
+ Returns:
64
+ torch.Tensor: Decoded boxes.
65
+ """
66
+ decoded_bboxes = tblr2bboxes(
67
+ bboxes,
68
+ pred_bboxes,
69
+ normalizer=self.normalizer,
70
+ max_shape=max_shape,
71
+ clip_border=self.clip_border)
72
+
73
+ return decoded_bboxes
74
+
75
+
76
+ @mmcv.jit(coderize=True)
77
+ def bboxes2tblr(priors, gts, normalizer=4.0, normalize_by_wh=True):
78
+ """Encode ground truth boxes to tblr coordinate.
79
+
80
+ It first convert the gt coordinate to tblr format,
81
+ (top, bottom, left, right), relative to prior box centers.
82
+ The tblr coordinate may be normalized by the side length of prior bboxes
83
+ if `normalize_by_wh` is specified as True, and it is then normalized by
84
+ the `normalizer` factor.
85
+
86
+ Args:
87
+ priors (Tensor): Prior boxes in point form
88
+ Shape: (num_proposals,4).
89
+ gts (Tensor): Coords of ground truth for each prior in point-form
90
+ Shape: (num_proposals, 4).
91
+ normalizer (Sequence[float] | float): normalization parameter of
92
+ encoded boxes. If it is a list, it has to have length = 4.
93
+ Default: 4.0
94
+ normalize_by_wh (bool): Whether to normalize tblr coordinate by the
95
+ side length (wh) of prior bboxes.
96
+
97
+ Return:
98
+ encoded boxes (Tensor), Shape: (num_proposals, 4)
99
+ """
100
+
101
+ # dist b/t match center and prior's center
102
+ if not isinstance(normalizer, float):
103
+ normalizer = torch.tensor(normalizer, device=priors.device)
104
+ assert len(normalizer) == 4, 'Normalizer must have length = 4'
105
+ assert priors.size(0) == gts.size(0)
106
+ prior_centers = (priors[:, 0:2] + priors[:, 2:4]) / 2
107
+ xmin, ymin, xmax, ymax = gts.split(1, dim=1)
108
+ top = prior_centers[:, 1].unsqueeze(1) - ymin
109
+ bottom = ymax - prior_centers[:, 1].unsqueeze(1)
110
+ left = prior_centers[:, 0].unsqueeze(1) - xmin
111
+ right = xmax - prior_centers[:, 0].unsqueeze(1)
112
+ loc = torch.cat((top, bottom, left, right), dim=1)
113
+ if normalize_by_wh:
114
+ # Normalize tblr by anchor width and height
115
+ wh = priors[:, 2:4] - priors[:, 0:2]
116
+ w, h = torch.split(wh, 1, dim=1)
117
+ loc[:, :2] /= h # tb is normalized by h
118
+ loc[:, 2:] /= w # lr is normalized by w
119
+ # Normalize tblr by the given normalization factor
120
+ return loc / normalizer
121
+
122
+
123
+ @mmcv.jit(coderize=True)
124
+ def tblr2bboxes(priors,
125
+ tblr,
126
+ normalizer=4.0,
127
+ normalize_by_wh=True,
128
+ max_shape=None,
129
+ clip_border=True):
130
+ """Decode tblr outputs to prediction boxes.
131
+
132
+ The process includes 3 steps: 1) De-normalize tblr coordinates by
133
+ multiplying it with `normalizer`; 2) De-normalize tblr coordinates by the
134
+ prior bbox width and height if `normalize_by_wh` is `True`; 3) Convert
135
+ tblr (top, bottom, left, right) pair relative to the center of priors back
136
+ to (xmin, ymin, xmax, ymax) coordinate.
137
+
138
+ Args:
139
+ priors (Tensor): Prior boxes in point form (x0, y0, x1, y1)
140
+ Shape: (N,4) or (B, N, 4).
141
+ tblr (Tensor): Coords of network output in tblr form
142
+ Shape: (N, 4) or (B, N, 4).
143
+ normalizer (Sequence[float] | float): Normalization parameter of
144
+ encoded boxes. By list, it represents the normalization factors at
145
+ tblr dims. By float, it is the unified normalization factor at all
146
+ dims. Default: 4.0
147
+ normalize_by_wh (bool): Whether the tblr coordinates have been
148
+ normalized by the side length (wh) of prior bboxes.
149
+ max_shape (Sequence[int] or torch.Tensor or Sequence[
150
+ Sequence[int]],optional): Maximum bounds for boxes, specifies
151
+ (H, W, C) or (H, W). If priors shape is (B, N, 4), then
152
+ the max_shape should be a Sequence[Sequence[int]]
153
+ and the length of max_shape should also be B.
154
+ clip_border (bool, optional): Whether clip the objects outside the
155
+ border of the image. Defaults to True.
156
+
157
+ Return:
158
+ encoded boxes (Tensor): Boxes with shape (N, 4) or (B, N, 4)
159
+ """
160
+ if not isinstance(normalizer, float):
161
+ normalizer = torch.tensor(normalizer, device=priors.device)
162
+ assert len(normalizer) == 4, 'Normalizer must have length = 4'
163
+ assert priors.size(0) == tblr.size(0)
164
+ if priors.ndim == 3:
165
+ assert priors.size(1) == tblr.size(1)
166
+
167
+ loc_decode = tblr * normalizer
168
+ prior_centers = (priors[..., 0:2] + priors[..., 2:4]) / 2
169
+ if normalize_by_wh:
170
+ wh = priors[..., 2:4] - priors[..., 0:2]
171
+ w, h = torch.split(wh, 1, dim=-1)
172
+ # Inplace operation with slice would failed for exporting to ONNX
173
+ th = h * loc_decode[..., :2] # tb
174
+ tw = w * loc_decode[..., 2:] # lr
175
+ loc_decode = torch.cat([th, tw], dim=-1)
176
+ # Cannot be exported using onnx when loc_decode.split(1, dim=-1)
177
+ top, bottom, left, right = loc_decode.split((1, 1, 1, 1), dim=-1)
178
+ xmin = prior_centers[..., 0].unsqueeze(-1) - left
179
+ xmax = prior_centers[..., 0].unsqueeze(-1) + right
180
+ ymin = prior_centers[..., 1].unsqueeze(-1) - top
181
+ ymax = prior_centers[..., 1].unsqueeze(-1) + bottom
182
+
183
+ bboxes = torch.cat((xmin, ymin, xmax, ymax), dim=-1)
184
+
185
+ if clip_border and max_shape is not None:
186
+ # clip bboxes with dynamic `min` and `max` for onnx
187
+ if torch.onnx.is_in_onnx_export():
188
+ from mmdet.core.export import dynamic_clip_for_onnx
189
+ xmin, ymin, xmax, ymax = dynamic_clip_for_onnx(
190
+ xmin, ymin, xmax, ymax, max_shape)
191
+ bboxes = torch.cat([xmin, ymin, xmax, ymax], dim=-1)
192
+ return bboxes
193
+ if not isinstance(max_shape, torch.Tensor):
194
+ max_shape = priors.new_tensor(max_shape)
195
+ max_shape = max_shape[..., :2].type_as(priors)
196
+ if max_shape.ndim == 2:
197
+ assert bboxes.ndim == 3
198
+ assert max_shape.size(0) == bboxes.size(0)
199
+
200
+ min_xy = priors.new_tensor(0)
201
+ max_xy = torch.cat([max_shape, max_shape],
202
+ dim=-1).flip(-1).unsqueeze(-2)
203
+ bboxes = torch.where(bboxes < min_xy, min_xy, bboxes)
204
+ bboxes = torch.where(bboxes > max_xy, max_xy, bboxes)
205
+
206
+ return bboxes
submodules/chartdete/mmdet/core/bbox/coder/yolo_bbox_coder.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import mmcv
3
+ import torch
4
+
5
+ from ..builder import BBOX_CODERS
6
+ from .base_bbox_coder import BaseBBoxCoder
7
+
8
+
9
+ @BBOX_CODERS.register_module()
10
+ class YOLOBBoxCoder(BaseBBoxCoder):
11
+ """YOLO BBox coder.
12
+
13
+ Following `YOLO <https://arxiv.org/abs/1506.02640>`_, this coder divide
14
+ image into grids, and encode bbox (x1, y1, x2, y2) into (cx, cy, dw, dh).
15
+ cx, cy in [0., 1.], denotes relative center position w.r.t the center of
16
+ bboxes. dw, dh are the same as :obj:`DeltaXYWHBBoxCoder`.
17
+
18
+ Args:
19
+ eps (float): Min value of cx, cy when encoding.
20
+ """
21
+
22
+ def __init__(self, eps=1e-6):
23
+ super(BaseBBoxCoder, self).__init__()
24
+ self.eps = eps
25
+
26
+ @mmcv.jit(coderize=True)
27
+ def encode(self, bboxes, gt_bboxes, stride):
28
+ """Get box regression transformation deltas that can be used to
29
+ transform the ``bboxes`` into the ``gt_bboxes``.
30
+
31
+ Args:
32
+ bboxes (torch.Tensor): Source boxes, e.g., anchors.
33
+ gt_bboxes (torch.Tensor): Target of the transformation, e.g.,
34
+ ground-truth boxes.
35
+ stride (torch.Tensor | int): Stride of bboxes.
36
+
37
+ Returns:
38
+ torch.Tensor: Box transformation deltas
39
+ """
40
+
41
+ assert bboxes.size(0) == gt_bboxes.size(0)
42
+ assert bboxes.size(-1) == gt_bboxes.size(-1) == 4
43
+ x_center_gt = (gt_bboxes[..., 0] + gt_bboxes[..., 2]) * 0.5
44
+ y_center_gt = (gt_bboxes[..., 1] + gt_bboxes[..., 3]) * 0.5
45
+ w_gt = gt_bboxes[..., 2] - gt_bboxes[..., 0]
46
+ h_gt = gt_bboxes[..., 3] - gt_bboxes[..., 1]
47
+ x_center = (bboxes[..., 0] + bboxes[..., 2]) * 0.5
48
+ y_center = (bboxes[..., 1] + bboxes[..., 3]) * 0.5
49
+ w = bboxes[..., 2] - bboxes[..., 0]
50
+ h = bboxes[..., 3] - bboxes[..., 1]
51
+ w_target = torch.log((w_gt / w).clamp(min=self.eps))
52
+ h_target = torch.log((h_gt / h).clamp(min=self.eps))
53
+ x_center_target = ((x_center_gt - x_center) / stride + 0.5).clamp(
54
+ self.eps, 1 - self.eps)
55
+ y_center_target = ((y_center_gt - y_center) / stride + 0.5).clamp(
56
+ self.eps, 1 - self.eps)
57
+ encoded_bboxes = torch.stack(
58
+ [x_center_target, y_center_target, w_target, h_target], dim=-1)
59
+ return encoded_bboxes
60
+
61
+ @mmcv.jit(coderize=True)
62
+ def decode(self, bboxes, pred_bboxes, stride):
63
+ """Apply transformation `pred_bboxes` to `boxes`.
64
+
65
+ Args:
66
+ boxes (torch.Tensor): Basic boxes, e.g. anchors.
67
+ pred_bboxes (torch.Tensor): Encoded boxes with shape
68
+ stride (torch.Tensor | int): Strides of bboxes.
69
+
70
+ Returns:
71
+ torch.Tensor: Decoded boxes.
72
+ """
73
+ assert pred_bboxes.size(-1) == bboxes.size(-1) == 4
74
+ xy_centers = (bboxes[..., :2] + bboxes[..., 2:]) * 0.5 + (
75
+ pred_bboxes[..., :2] - 0.5) * stride
76
+ whs = (bboxes[..., 2:] -
77
+ bboxes[..., :2]) * 0.5 * pred_bboxes[..., 2:].exp()
78
+ decoded_bboxes = torch.stack(
79
+ (xy_centers[..., 0] - whs[..., 0], xy_centers[..., 1] -
80
+ whs[..., 1], xy_centers[..., 0] + whs[..., 0],
81
+ xy_centers[..., 1] + whs[..., 1]),
82
+ dim=-1)
83
+ return decoded_bboxes
submodules/chartdete/mmdet/core/bbox/demodata.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ import numpy as np
3
+ import torch
4
+
5
+ from mmdet.utils.util_random import ensure_rng
6
+
7
+
8
+ def random_boxes(num=1, scale=1, rng=None):
9
+ """Simple version of ``kwimage.Boxes.random``
10
+
11
+ Returns:
12
+ Tensor: shape (n, 4) in x1, y1, x2, y2 format.
13
+
14
+ References:
15
+ https://gitlab.kitware.com/computer-vision/kwimage/blob/master/kwimage/structs/boxes.py#L1390
16
+
17
+ Example:
18
+ >>> num = 3
19
+ >>> scale = 512
20
+ >>> rng = 0
21
+ >>> boxes = random_boxes(num, scale, rng)
22
+ >>> print(boxes)
23
+ tensor([[280.9925, 278.9802, 308.6148, 366.1769],
24
+ [216.9113, 330.6978, 224.0446, 456.5878],
25
+ [405.3632, 196.3221, 493.3953, 270.7942]])
26
+ """
27
+ rng = ensure_rng(rng)
28
+
29
+ tlbr = rng.rand(num, 4).astype(np.float32)
30
+
31
+ tl_x = np.minimum(tlbr[:, 0], tlbr[:, 2])
32
+ tl_y = np.minimum(tlbr[:, 1], tlbr[:, 3])
33
+ br_x = np.maximum(tlbr[:, 0], tlbr[:, 2])
34
+ br_y = np.maximum(tlbr[:, 1], tlbr[:, 3])
35
+
36
+ tlbr[:, 0] = tl_x * scale
37
+ tlbr[:, 1] = tl_y * scale
38
+ tlbr[:, 2] = br_x * scale
39
+ tlbr[:, 3] = br_y * scale
40
+
41
+ boxes = torch.from_numpy(tlbr)
42
+ return boxes
submodules/chartdete/mmdet/core/bbox/iou_calculators/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from .builder import build_iou_calculator
3
+ from .iou2d_calculator import BboxOverlaps2D, bbox_overlaps
4
+
5
+ __all__ = ['build_iou_calculator', 'BboxOverlaps2D', 'bbox_overlaps']
submodules/chartdete/mmdet/core/bbox/iou_calculators/builder.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) OpenMMLab. All rights reserved.
2
+ from mmcv.utils import Registry, build_from_cfg
3
+
4
+ IOU_CALCULATORS = Registry('IoU calculator')
5
+
6
+
7
+ def build_iou_calculator(cfg, default_args=None):
8
+ """Builder of IoU calculator."""
9
+ return build_from_cfg(cfg, IOU_CALCULATORS, default_args)