Skip to content

Commit 385facc

Browse files
authored
Add support for lightglue validation in accuracy checker (#3962)
* Add hpatches converter * Add disk feature reader * Add image matches homography metrics evaluator * Refactor dict_type input/output params use * Update copyright * Remove commented code * Address pylint warnings * Corrections after pytests * Fix pytests TypeError for pytest.warns(None) * Revert change * Revert change in dlsdk_launcher.py * Remove test code hpatches.py * Update torch with numpy operations * Fix typo * Accept images of different size * Support use of torch.compile with config passed parameters * Rename parameter name
1 parent 69572ae commit 385facc

File tree

16 files changed

+567
-50
lines changed

16 files changed

+567
-50
lines changed

tools/accuracy_checker/accuracy_checker/annotation_converters/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@
133133
from .malware_classification import MalwareClassificationDatasetConverter
134134
from .cvat_hands_and_palm import CVATPalmDetectionConverter, CVATHandLandmarkConverter
135135
from .parti_prompts import PartiPromptsDatasetConverter
136+
from .hpatches import HpatchesConverter
136137

137138
__all__ = [
138139
'BaseFormatConverter',
@@ -267,5 +268,6 @@
267268
'MalwareClassificationDatasetConverter',
268269
'CVATPalmDetectionConverter',
269270
'CVATHandLandmarkConverter',
270-
'PartiPromptsDatasetConverter'
271+
'PartiPromptsDatasetConverter',
272+
'HpatchesConverter'
271273
]
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""
2+
Copyright (c) 2024 Intel Corporation
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
import os
18+
from pathlib import Path
19+
import numpy as np
20+
from .format_converter import DirectoryBasedAnnotationConverter, ConverterReturn
21+
from ..config import NumberField, StringField
22+
from ..representation import ImageFeatureAnnotation
23+
from ..utils import UnsupportedPackage
24+
from ..data_readers import AnnotationDataIdentifier
25+
from ..progress_reporters import TQDMReporter
26+
27+
28+
# Large images that were ignored in previous papers
29+
ignored_scenes = (
30+
"i_contruction",
31+
"i_crownnight",
32+
"i_dc",
33+
"i_pencils",
34+
"i_whitebuilding",
35+
"v_artisans",
36+
"v_astronautis",
37+
"v_talent",
38+
)
39+
40+
41+
class HpatchesConverter(DirectoryBasedAnnotationConverter):
42+
__provider__ = 'hpatches_with_kornia_feature'
43+
44+
@classmethod
45+
def parameters(cls):
46+
params = super().parameters()
47+
params.update({
48+
'sequences_dir_name': StringField(
49+
optional=True, default='hpatches-sequences-release',
50+
description="Dataset subfolder name, where hpatches sequences are located."
51+
),
52+
'max_num_keypoints': NumberField(
53+
optional=True, default=512, value_type=int, min_value=128, max_value=2048,
54+
description='Maksimum number of image keypoints.'
55+
),
56+
'image_side_size': NumberField(
57+
optional=True, default=480, value_type=int, min_value=128, max_value=2048,
58+
description='Image short side size.'
59+
)
60+
})
61+
62+
return params
63+
64+
def configure(self):
65+
try:
66+
import torch # pylint: disable=import-outside-toplevel
67+
self._torch = torch
68+
except ImportError as torch_import_error:
69+
UnsupportedPackage('torch', torch_import_error.msg).raise_error(self.__provider__)
70+
try:
71+
import kornia # pylint: disable=import-outside-toplevel
72+
self._kornia = kornia
73+
except ImportError as kornia_import_error:
74+
UnsupportedPackage('kornia', kornia_import_error.msg).raise_error(self.__provider__)
75+
76+
77+
self.data_dir = self.get_value_from_config('data_dir')
78+
self.sequences_dir = self.get_value_from_config('sequences_dir_name')
79+
self.max_num_keypoints = self.get_value_from_config('max_num_keypoints')
80+
self.side_size = self.get_value_from_config('image_side_size')
81+
82+
def _get_new_image_size(self, h: int, w: int):
83+
side_size = self.side_size
84+
aspect_ratio = w / h
85+
if aspect_ratio < 1.0:
86+
size = int(side_size / aspect_ratio), side_size
87+
else:
88+
size = side_size, int(side_size * aspect_ratio)
89+
return size
90+
91+
92+
def _get_image_data(self, path, image_size = None):
93+
img = self._kornia.io.load_image(path, self._kornia.io.ImageLoadType.RGB32, device='cpu')[None, ...]
94+
95+
h, w = img.shape[-2:]
96+
size = h, w
97+
size = self._get_new_image_size(h, w)
98+
if image_size and size != image_size:
99+
size = image_size
100+
img = self._kornia.geometry.transform.resize(
101+
img,
102+
size,
103+
side='short',
104+
antialias=True,
105+
align_corners=None,
106+
interpolation='bilinear',
107+
)
108+
scale = self._torch.Tensor([img.shape[-1] / w, img.shape[-2] / h]).to(img)
109+
T = np.diag([scale[0], scale[1], 1])
110+
111+
data = {
112+
"scales": scale,
113+
"image_size": np.array(size[::-1]),
114+
"transform": T,
115+
"original_image_size": np.array([w, h]),
116+
"image" : img
117+
}
118+
return data
119+
120+
@staticmethod
121+
def _read_homography(path):
122+
with open(path, encoding="utf-8") as f:
123+
result = []
124+
for line in f.readlines():
125+
while " " in line: # Remove double spaces
126+
line = line.replace(" ", " ")
127+
line = line.replace(" \n", "").replace("\n", "")
128+
# Split and discard empty strings
129+
elements = list(filter(lambda s: s, line.split(" ")))
130+
if elements:
131+
result.append(elements)
132+
return np.array(result).astype(float)
133+
134+
def get_image_features(self, model, data):
135+
with self._torch.inference_mode():
136+
return model(data["image"], self.max_num_keypoints, pad_if_not_divisible=True)[0]
137+
138+
def convert(self, check_content=False, progress_callback=None, progress_interval=50, **kwargs):
139+
annotations = []
140+
items = []
141+
142+
sequences_dir = Path(os.path.join(self.data_dir, self.sequences_dir))
143+
sequences = sorted([x.name for x in sequences_dir.iterdir()])
144+
145+
for seq in sequences:
146+
if seq in ignored_scenes:
147+
continue
148+
for i in range(2, 7):
149+
items.append((seq, i, seq[0] == "i"))
150+
151+
disk_model = self._kornia.feature.DISK().from_pretrained("depth")
152+
153+
num_iterations = len(items)
154+
progress_reporter = TQDMReporter(print_interval=progress_interval)
155+
progress_reporter.reset(num_iterations)
156+
157+
for item_id, item in enumerate(items):
158+
seq, idx, _ = item
159+
160+
if idx == 2:
161+
img_path = Path(sequences_dir / seq / "1.ppm")
162+
data0 = self._get_image_data(img_path)
163+
features0 = self.get_image_features(disk_model, data0)
164+
165+
img_path = Path(sequences_dir / seq / f"{idx}.ppm")
166+
data1 = self._get_image_data(img_path)
167+
features1 = self.get_image_features(disk_model, data1)
168+
169+
H = self._read_homography(Path(sequences_dir / seq / f"H_1_{idx}"))
170+
H = data1["transform"] @ H @ np.linalg.inv(data0["transform"])
171+
172+
data = {
173+
"keypoints0": features0.keypoints.unsqueeze(0),
174+
"keypoints1": features1.keypoints.unsqueeze(0),
175+
"descriptors0": features0.descriptors.unsqueeze(0),
176+
"descriptors1" : features1.descriptors.unsqueeze(0),
177+
"image_size0": data0["image_size"],
178+
"image_size1": data1["image_size"],
179+
"H_0to1": H
180+
}
181+
182+
sequence = f"{seq}/{idx}"
183+
annotated_id = AnnotationDataIdentifier(sequence, data)
184+
annotation = ImageFeatureAnnotation(
185+
identifier = annotated_id,
186+
sequence = sequence
187+
)
188+
annotations.append(annotation)
189+
progress_reporter.update(item_id, 1)
190+
191+
progress_reporter.finish()
192+
return ConverterReturn(annotations, None, None)

tools/accuracy_checker/accuracy_checker/data_readers/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
create_reader,
3939
REQUIRES_ANNOTATIONS
4040
)
41-
from .annotation_readers import AnnotationFeaturesReader
41+
from .annotation_readers import AnnotationFeaturesReader, DiskImageFeaturesExtractor
4242
from .binary_data_readers import PickleReader, ByteFileReader, LMDBReader
4343
from .medical_imaging_readers import NiftiImageReader, DicomReader
4444
from .audio_readers import WavReader, KaldiARKReader, FlacReader
@@ -78,6 +78,7 @@
7878
'NiftiImageReader',
7979
'TensorflowImageReader',
8080
'AnnotationFeaturesReader',
81+
'DiskImageFeaturesExtractor',
8182
'WavReader',
8283
'FlacReader',
8384
'DicomReader',

tools/accuracy_checker/accuracy_checker/data_readers/annotation_readers.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@
1414
limitations under the License.
1515
"""
1616

17-
from ..config import ListField, ConfigError
17+
from ..config import ListField, BoolField, ConfigError
1818
from .data_reader import BaseReader, create_ann_identifier_key, AnnotationDataIdentifier
1919
from ..utils import contains_all
2020

21-
2221
class NCFDataReader(BaseReader):
2322
__provider__ = 'ncf_data_reader'
2423

@@ -68,3 +67,38 @@ def _read_list(self, data_id):
6867
def reset(self):
6968
self.subset = range(len(self.data_source))
7069
self.counter = 0
70+
71+
72+
class DiskImageFeaturesExtractor(BaseReader):
73+
__provider__ = 'disk_features_extractor'
74+
75+
@classmethod
76+
def parameters(cls):
77+
parameters = super().parameters()
78+
parameters.update({'input_is_dict_type': BoolField(
79+
optional=True, default=True, description='Model input is dict type.')})
80+
parameters.update({'output_is_dict_type': BoolField(
81+
optional=True, default=True, description='Model output is dict type.')})
82+
return parameters
83+
84+
def configure(self):
85+
self.input_as_dict_type = self.get_value_from_config('input_is_dict_type')
86+
self.output_is_dict_type = self.get_value_from_config('output_is_dict_type')
87+
88+
def read(self, data_id):
89+
assert isinstance(data_id, AnnotationDataIdentifier)
90+
data = data_id.data_id
91+
92+
required_keys = ["keypoints", "descriptors", "image_size", "oris"]
93+
94+
view0 = {
95+
**{k: data[k + "0"] for k in required_keys if k + "0" in data},
96+
}
97+
view1 = {
98+
**{k: data[k + "1"] for k in required_keys if k + "0" in data},
99+
}
100+
101+
return {"image0": view0, "image1": view1}
102+
103+
def _read_list(self, data_id):
104+
return self.read(data_id)

tools/accuracy_checker/accuracy_checker/data_readers/data_reader.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
BaseField, StringField, ConfigValidator, ConfigError, DictField, BoolField, PathField
3030
)
3131

32-
REQUIRES_ANNOTATIONS = ['annotation_features_extractor', ]
32+
REQUIRES_ANNOTATIONS = ['annotation_features_extractor' ,'disk_features_extractor' ]
3333
DOES_NOT_REQUIRED_DATA_SOURCE = REQUIRES_ANNOTATIONS + ['ncf_reader']
3434
DATA_SOURCE_IS_FILE = ['opencv_capture']
3535

@@ -39,6 +39,9 @@ def __init__(self, data, meta=None, identifier=''):
3939
self.identifier = identifier
4040
self.data = data
4141
self.metadata = meta or {}
42+
43+
if self.metadata.get('input_is_dict_type'):
44+
return
4245
if np.isscalar(data):
4346
self.metadata['image_size'] = 1
4447
elif isinstance(data, list) and np.isscalar(data[0]):
@@ -211,6 +214,7 @@ def __init__(self, data_source, config=None, postpone_data_source=False, **kwarg
211214
self.read_dispatcher.register(ParametricImageIdentifier, self._read_parametric_input)
212215
self.read_dispatcher.register(VideoFrameIdentifier, self._read_video_frame)
213216
self.multi_infer = False
217+
self.data_layout = None
214218

215219
self.validate_config(config, data_source)
216220
self.configure()
@@ -318,8 +322,13 @@ def _read_video_frame(self, data_id):
318322
return self.read_dispatcher(data_id.frame)
319323

320324
def read_item(self, data_id):
325+
meta = {
326+
'input_is_dict_type' : self.config.get('input_is_dict_type', False),
327+
'output_is_dict_type' : self.config.get('output_is_dict_type', False),
328+
}
321329
data_rep = DataRepresentation(
322330
self.read_dispatcher(data_id),
331+
meta = meta,
323332
identifier=data_id if not isinstance(data_id, ListIdentifier) else list(data_id.values)
324333
)
325334
if self.multi_infer:

tools/accuracy_checker/accuracy_checker/launcher/input_feeder.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ def separate_data(data, num_splits):
484484

485485
batch_size = len(meta)
486486
template_for_shapes = {}
487+
487488
if meta[0].get('multi_infer', False):
488489
num_splits = calculate_num_splits(batch_data, batch_size)
489490
infers_data = [{} for _ in range(num_splits)]
@@ -504,6 +505,15 @@ def separate_data(data, num_splits):
504505

505506
for layer_name, layer_data in batch_data.items():
506507
layout = self.layouts_mapping.get(layer_name)
508+
if meta[0].get('input_is_dict_type'):
509+
layer_data_preprocessed = self.input_transform_func(
510+
layer_data, layer_name,
511+
layout,
512+
self.precision_mapping.get(layer_name), template
513+
)
514+
batch_data[layer_name] = layer_data_preprocessed
515+
continue
516+
507517
if 'data_layout' in meta[0]:
508518
data_layout = LAYER_LAYOUT_TO_IMAGE_LAYOUT.get(meta[0]['data_layout'])
509519
if layout is None and len(self.default_layout) == len(data_layout):

0 commit comments

Comments
 (0)