각종 논문에서 camera pose를 얻는 기본 방법으로 웬만하면 SfM을 사용하는데 그 때마다 SfM == COLMAP으로 생각하는게 당연할 만큼 COLMAP은 자주 접하게 되는 툴이다.
하지만 COLMAP은 기본적으로 c/c++ 기반으로 되어있어서 python 위주로 코딩할 때 사용성이 그렇게 좋은 편은 아니고 독자적인 포맷도 맞춰줘야 하기 때문에 뚝딱 쓰기엔 조금 시간이 걸린다. 더불어 SIFT 이하 feature 만을 사용하는 COLMAP이기 때문에 더 좋은 deep feature가 있어도 이를 COLMAP에 내장시켜 사용하는 것은 더 오래 걸린다.
pycolmap이 있어서 python binding된 형태로 사용할 수 있지만, pycolmap 역시 deep feature를 기반으로 동작하도록 수정하는 것은 만만치 않다.
superpoint나 LoFTR와 같은 SIFT를 한참 앞선 feature들을 이용해 SfM을 돌리고 싶어서 검색을 하다가 hloc 이라는 것을 찾아냈는데, python으로 구현되어 있어서 사용성이 뛰어날 뿐만 아니라 다양한 deep feature들도 지원되고 COLMAP이 뒤에 붙어있는 형태다.
https://github.com/cvg/Hierarchical-Localization
본래 도시 단위의 대규모 localization을 성공적으로 하기 위해 image retrieval부터 SfM, SfM 기반 localization까지 다 담아둔 툴박스인데 localization만 제거하면 그냥 deep feature 기반 SfM이기 때문에 pycolmap보다 모든 면에서 뛰어난 것 같다.
deep feature 뿐만 아니라 deep matching까지 내장되어 있고, image pair를 생성할 때 유용한 image retrieval network들도 지원돼서 성능도 COLMAP보다 당연히 뛰어나다.
Usage
hloc github README가 워낙 잘되어 있기 때문에 별도의 설명이 크게 필요가 없지만, 약간의 팁을 더해서 작성해보고자 한다.
1) IO preparation
'''
root
|- images
|- outputs
'''
def parse_config():
parser = argparse.ArgumentParser("SfM Arguments")
parser.add_argument("--root", type=str, default="ROOT_DIRECTORY", help="root directory")
args = parser.parse_args()
return args
먼저 CLI 입력은 root folder만 받도록 짰다. 하위에 images라는 폴더를 갖고 그 안에 이미지들이 있는 구조다. 결과 파일들은 outputs 폴더에 생성될 것
args = parse_config()
root = Path(args.root)
### input path
img_dir = root / "images"
image_list = sorted([str(p.relative_to(img_dir)) for p in img_dir.iterdir()])
### output path
save_dir = root / "outputs"
features_path = save_dir / "feature.h5"
descriptors_path = save_dir / "descriptor.h5"
matches_path = save_dir / "matches.h5"
sfm_pairs_path = save_dir / 'pairs-sfm.txt'
sfm_dir = save_dir / "sfm"
SfM 과정에서 생기는 값들, feature/matches 그리고 image retrieval 기능을 이용할 것이라면 필요한 descriptor 위치를 지정해준다.
2) deep feature/matching, descriptor configuration
# list the standard configurations available
print(f'Configs for feature extractors:\n{pformat(extract_features.confs)}')
print(f'Configs for feature matchers:\n{pformat(match_features.confs)}')
# pick one of the configurations for image retrieval, local feature extraction, and matching
# you can also simply write your own here!
retrieval_conf = extract_features.confs['netvlad']
feature_conf = extract_features.confs['superpoint_max']
matcher_conf = match_features.confs['superpoint+lightglue']
위 print 결과들을 보면 feature/matching 각각 어떤 network를 사용할 수 있는지 지원되는 목록이 뜬다. 그 중에 사용하고 싶은 이름을 아래 .confs[key] 안에 key형태로 넣어주면 된다. 위에 적어둔 조합은 내가 자주 쓰는 조합이다.
3) feature extraction
## Extract local features for database and query images
if not os.path.exists(features_path):
extract_features.main(feature_conf, img_dir, save_dir, image_list=image_list, feature_path=features_path)
위와 같이 호출만 해주면 알아서 지정된 deep feature를 뽑아서 feature_path에 저장한다. 단지 매번 실행마다 다시 뽑지 않도록 이미 존재할 경우 스킵하도록만 해주면 좋음.
4) feature matching
여기선 몇가지 경우의 수가 있다.1) matching을 시도할 이미지 pair 즉, covisiblity를 이미 알고 있는 경우
2) covisibility를 몰라서 retrieval network가 찾아주는 pair를 사용하고 싶은 경우
3) 이미지가 순서대로 정렬되어 있어서 그냥 이웃 이미지 N장을 pair로 사용하고 싶은 경우
여기서 1)은 웬만하면 모르고 있을테니 여기에 기록하지 않는다. 만약 알고 있다면 가장 쉬운 경우이므로 hloc 기본 제공 예제를 사용하면 된다.
먼저 2) 경우, 앞서 어떤 retrieval network를 사용할지 configuration을 정했으므로, 이를 이용해 descriptor를 뽑아주면 된다.
if not os.path.exists(descriptors_path):
extract_features.main(retrieval_conf, img_dir, save_dir, image_list=image_list, feature_path=descriptors_path)
pairs_from_retrieval.main(descriptors_path, sfm_pairs_path, num_matched=20)
extract_features를 그대로 쓰기 때문에 문법적으로 크게 다른 것이 없다. num_matched만 조정하면서 이미지 당 총 몇 장의 pair를 생성할 것인지만 정해주면 된다.
3)의 경우, 코드가 따로 제공되지 않는다. 그래서 직접 작성해야 한다. 내가 작성한 코드는 아래와 같다.
'''
hloc/pairs_from_neighbors.py
'''
import collections.abc as collections
from pathlib import Path
from typing import Optional, Union, List
from . import logger
from .utils.parsers import parse_image_lists
def main(
output: Path,
image_list: Optional[Union[Path, List[str]]] = None,
features: Optional[Path] = None,
ref_list: Optional[Union[Path, List[str]]] = None,
ref_features: Optional[Path] = None,
num_neighbors=20):
'''
output : sfm_pair save path, necessary
image_list : all images paths
features : not necessary
ref_list : not necessary
ref_featrues : not necessary
'''
if image_list is not None:
if isinstance(image_list, (str, Path)):
names_qry = parse_image_lists(image_list)
names_ref = names_qry.copy()
elif isinstance(image_list, collections.Iterable):
names_qry = list(image_list)
names_ref = names_qry.copy()
else:
raise ValueError(f'Unknown type for image list: {image_list}')
elif features is not None:
raise ValueError('features is not supported. Remove it from arguments')
else:
raise ValueError('Provide a list of images.')
if ref_list is not None:
raise ValueError('ref_list is not supported. Remove it from arguments.')
elif ref_features is not None:
raise ValueError('ref_features is not supported. Remove it from arguments.')
pairs = []
num_images = len(names_qry)
num_each_side = int(num_neighbors/2)
for i in range(len(names_qry)):
for j in range(max(i-num_each_side, 0), min(i+num_each_side+1, num_images)):
if i == j: # pass self matching
continue
n1 = names_qry[i]
n2 = names_ref[j]
pairs.append((n1, n2))
logger.info(f"Found {len(pairs)} pairs.")
with open(output, 'w') as f:
f.write('\n'.join(' '.join([i, j]) for i, j in pairs))
이 스크립트를 돌리면 이미지 전후로 N장 이미지들을 pair로 생성한다. 주의점은 첫번째 이미지나 마지막 이미지처럼 전/후 중 한 방향 neighbor가 없을 경우 굳이 찾지 않고 버린다. 따라서 N장 이하의 pair가 생성되는 이미지들도 있다.
if not os.path.exists(sfm_pairs_path):
pairs_from_neighbors.main(sfm_pairs_path, image_list=image_list)
사용은 그냥 이렇게 하면 되겠다.
if not os.path.exists(matches_path):
match_features.main(matcher_conf, sfm_pairs_path, features=features_path, matches=matches_path)
pair 생성까지 종료되면 위 함수를 호출해주면 matching까지 끝난다! 이 작업을 pair 수에 비례해서 시간이 꽤 오래 걸리므로 시간 많을 때 돌려야 한다.
5) reconstruction
예제보면 triangulation.main()을 사용하는 경우도 있는데 아래와 같이 reconstruction.main() 쓰는 것이 더 좋다.
reconstruction = reconstruction.main(sfm_dir, img_dir, sfm_pairs_path, features_path, matches_path, image_list=image_list)
끝!
KNOWHOW
1) known intrinsic
간혹 카메라를 먼저 calibration해두어서 intrinsic은 SfM 과정에서 배제하고 싶을 수도 있다. 이 때는 간단하게 option 하나만 추가해주면 된다.
COLMAP에서 지원하는 카메라는 PINHOLE, SIMPLE_PINHOLE, RADIAL, SIMPLE_RADIAL, OPENCV 등이 있는데 각각 파라미터를 알고 있다면 dictionary 형태로 먼저 정리해둔다. 예를 들면,
fx = 550.84393311
fy = 541.83520508
cx = 316.54866584
cy = 159.00055477
intrinsic = dict(camera_model='PINHOLE', camera_params=','.join(map(str, (fx, fy, cx, cy))))
camera_model에 따라 파라미터 수는 다를텐데 그냥 이어붙여두면 된다.
reconstruction = reconstruction.main(sfm_dir, img_dir, sfm_pairs_path, features_path, matches_path, image_list=image_list, image_options=intrinsic)
그리곤 reconstruction.main() 호출 시 argument로 image_options=intrinsic을 추가해주면 끝이다.
2) feature descriptor free SfM
신기하게도 descriptor가 존재하지 않는 feature extraction/matching 혼합 네트워크인 LoFTR도 지원이 된다.
이 경우 기존 extract_feature.main()을 사용하지 않고 match_dense.main()이라는 새로운 형태로 구현되어 있다.
extract_feature.main() + match_features.main() => match_dense.main() 하나로 통일되어 있다.
그도 그럴 것이 descriptor없이 feature track을 생성하는 방식은 완전이 다르기 때문이다.
이 점만 유의한 상태로 hloc/pipelines/Aachen_v1_1/pipline_loftr.py를 그대로 따라가면 된다.
Update
위 hloc repository에 올라온 Dockerfile이 버전 이슈가 조금 있는 것 같다. 특히 python3.8로 고정되어 있는 상태인데 ubuntu22.04 이상을 쓴다거나 할 때 오류가 나고, 일부 dependency가 빠져있는 듯 하다.
colmap base image가 업데이트 되어서 그럴 수도 있고 위에 올려둔 Dockerfile은 ubuntu22.04, python3.10에서 만든 것.
공식 dockerfile을 쓰면 python3.8-distutils 오류와 git 미설치, Timezone 설정 문제 때문에 에러가 난다. 위 파일 사용을 추천함.
'Knowhow > Vision' 카테고리의 다른 글
Double sphere 모델 projection-failed region (0) | 2024.02.28 |
---|---|
Fisheye 카메라 모델도 solvePnP 이용해서 자세 초기화/추정하는 방법 (0) | 2024.02.28 |
RealityCapture camera coordinate to opencv(vision) camera coordinate (0) | 2023.12.06 |
Open3d를 이용한 디버깅용 camera, bbox, origin visualization (0) | 2023.12.06 |
Sphere 상에서 normal vector uniform sampling (0) | 2023.11.07 |