Skip to content

Commit 78dd615

Browse files
Merge pull request #1 from lukasalexanderweber/pip_package
prepare pip package
2 parents 159cba9 + c9b59e5 commit 78dd615

29 files changed

Lines changed: 2427 additions & 1 deletion

README.md

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,71 @@
11
# stitching
2-
Fast and Robust Image Stitching
2+
3+
A Python package for fast and robust Image Stitching. A modularized and continuing work based on opencv's [stitching module](https://github.com/opencv/opencv/tree/4.x/modules/stitching) and the [stitching_detailed.py](https://github.com/opencv/opencv/blob/4.x/samples/python/stitching_detailed.py) python command line tool.
4+
5+
![inputs](https://github.com/lukasalexanderweber/stitching_tutorial/blob/master/docs/static_files/inputs.png?raw=true)
6+
7+
![result](https://github.com/lukasalexanderweber/stitching_tutorial/blob/master/docs/static_files/panorama.png?raw=true)
8+
9+
## Installation
10+
11+
Use pip to install stitching from [PyPI](https://pypi.org/project/stitching/).
12+
13+
```bash
14+
pip install stitching
15+
```
16+
17+
## Usage
18+
19+
```python
20+
import stitching
21+
22+
stitcher = stitching.Stitcher()
23+
panorama = stitcher.stitch(["img1.jpg", "img2.jpg", "img3.jpg"])
24+
25+
```
26+
27+
or using the [command line tool](https://github.com/lukasalexanderweber/stitching/blob/main/stitching_tool.py)
28+
29+
30+
```
31+
python stitching_tool.py -h
32+
python stitching_tool.py img1.jpg img2.jpg img3.jpg
33+
```
34+
35+
## Tutorial
36+
37+
This package provides utility functions to deeply analyse what's happening behind the stitching. A tutorial was created as [Jupyter Notebook](https://github.com/lukasalexanderweber/stitching_tutorial). The preview is [here](https://github.com/lukasalexanderweber/stitching_tutorial/blob/master/docs/Stitching%20Tutorial.md).
38+
39+
You can e.g. visualize the RANSAC matches between the images or the seam lines where the images are blended:
40+
41+
![matches1](https://github.com/lukasalexanderweber/stitching_tutorial/blob/master/docs/static_files/matches1.png?raw=true)
42+
![matches2](https://github.com/lukasalexanderweber/stitching_tutorial/blob/master/docs/static_files/matches2.png?raw=true)
43+
![seams1](https://github.com/lukasalexanderweber/stitching_tutorial/blob/master/docs/static_files/seams1.png?raw=true)
44+
![seams2](https://github.com/lukasalexanderweber/stitching_tutorial/blob/master/docs/static_files/seams2.png?raw=true)
45+
46+
## Literature
47+
48+
This package was developed and used for our paper 'Automatic stitching of fragmented construction plans of hydraulic structures' https://doi.org/10.1002/bate.202200010
49+
50+
## Contributing
51+
52+
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
53+
54+
Please make sure to update tests as appropriate.
55+
56+
Run tests using
57+
58+
```bash
59+
python -m unittest
60+
```
61+
62+
Build with
63+
64+
```bash
65+
python -m build
66+
```
67+
68+
## License
69+
70+
[Apache License 2.0](https://github.com/lukasalexanderweber/lir/blob/main/LICENSE)
71+

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["setuptools>=42"]
3+
build-backend = "setuptools.build_meta"

setup.cfg

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[metadata]
2+
name = stitching
3+
version = attr: stitching.__version__
4+
description = A Python package for fast and robust Image Stitching
5+
long_description = file: README.md
6+
long_description_content_type = text/markdown
7+
author = Lukas Weber
8+
author_email = l.a.weber@outlook.de
9+
license = Apache License 2.0
10+
license_file = LICENSE
11+
platforms = any
12+
classifiers =
13+
Programming Language :: Python :: 3 :: Only
14+
project_urls =
15+
Source = https://github.com/lukasalexanderweber/stitching
16+
Bug Tracker = https://github.com/lukasalexanderweber/stitching/issues
17+
18+
[options]
19+
packages = find:
20+
install_requires =
21+
opencv-python>=4.0.1
22+
largestinteriorrectangle
23+
include_package_data = True
24+
zip_safe = False
25+
26+
[options.packages.find]
27+
include = stitching
28+
exclude = tests

stitching/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .stitcher import Stitcher
2+
3+
__version__ = "0.0.1"

stitching/blender.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import cv2 as cv
2+
import numpy as np
3+
4+
5+
class Blender:
6+
"""https://docs.opencv.org/4.x/d6/d4a/classcv_1_1detail_1_1Blender.html""" # noqa
7+
8+
BLENDER_CHOICES = ('multiband', 'feather', 'no',)
9+
DEFAULT_BLENDER = 'multiband'
10+
DEFAULT_BLEND_STRENGTH = 5
11+
12+
def __init__(self, blender_type=DEFAULT_BLENDER,
13+
blend_strength=DEFAULT_BLEND_STRENGTH):
14+
self.blender_type = blender_type
15+
self.blend_strength = blend_strength
16+
self.blender = None
17+
18+
def prepare(self, corners, sizes):
19+
dst_sz = cv.detail.resultRoi(corners=corners, sizes=sizes)
20+
blend_width = (np.sqrt(dst_sz[2] * dst_sz[3]) *
21+
self.blend_strength / 100)
22+
23+
if self.blender_type == 'no' or blend_width < 1:
24+
self.blender = cv.detail.Blender_createDefault(
25+
cv.detail.Blender_NO
26+
)
27+
28+
elif self.blender_type == "multiband":
29+
self.blender = cv.detail_MultiBandBlender()
30+
self.blender.setNumBands(int((np.log(blend_width) /
31+
np.log(2.) - 1.)))
32+
33+
elif self.blender_type == "feather":
34+
self.blender = cv.detail_FeatherBlender()
35+
self.blender.setSharpness(1. / blend_width)
36+
37+
self.blender.prepare(dst_sz)
38+
39+
def feed(self, img, mask, corner):
40+
self.blender.feed(cv.UMat(img.astype(np.int16)), mask, corner)
41+
42+
def blend(self):
43+
result = None
44+
result_mask = None
45+
result, result_mask = self.blender.blend(result, result_mask)
46+
result = cv.convertScaleAbs(result)
47+
return result, result_mask
48+
49+
@classmethod
50+
def create_panorama(cls, imgs, masks, corners, sizes):
51+
blender = cls("no")
52+
blender.prepare(corners, sizes)
53+
for img, mask, corner in zip(imgs, masks, corners):
54+
blender.feed(img, mask, corner)
55+
return blender.blend()

stitching/camera_adjuster.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from collections import OrderedDict
2+
import cv2 as cv
3+
import numpy as np
4+
5+
from .stitching_error import StitchingError
6+
7+
8+
class CameraAdjuster:
9+
"""https://docs.opencv.org/4.x/d5/d56/classcv_1_1detail_1_1BundleAdjusterBase.html""" # noqa
10+
11+
CAMERA_ADJUSTER_CHOICES = OrderedDict()
12+
CAMERA_ADJUSTER_CHOICES['ray'] = cv.detail_BundleAdjusterRay
13+
CAMERA_ADJUSTER_CHOICES['reproj'] = cv.detail_BundleAdjusterReproj
14+
CAMERA_ADJUSTER_CHOICES['affine'] = cv.detail_BundleAdjusterAffinePartial
15+
CAMERA_ADJUSTER_CHOICES['no'] = cv.detail_NoBundleAdjuster
16+
17+
DEFAULT_CAMERA_ADJUSTER = list(CAMERA_ADJUSTER_CHOICES.keys())[0]
18+
DEFAULT_REFINEMENT_MASK = "xxxxx"
19+
20+
def __init__(self,
21+
adjuster=DEFAULT_CAMERA_ADJUSTER,
22+
refinement_mask=DEFAULT_REFINEMENT_MASK):
23+
24+
self.adjuster = CameraAdjuster.CAMERA_ADJUSTER_CHOICES[adjuster]()
25+
self.set_refinement_mask(refinement_mask)
26+
self.adjuster.setConfThresh(1)
27+
28+
def set_refinement_mask(self, refinement_mask):
29+
mask_matrix = np.zeros((3, 3), np.uint8)
30+
if refinement_mask[0] == 'x':
31+
mask_matrix[0, 0] = 1
32+
if refinement_mask[1] == 'x':
33+
mask_matrix[0, 1] = 1
34+
if refinement_mask[2] == 'x':
35+
mask_matrix[0, 2] = 1
36+
if refinement_mask[3] == 'x':
37+
mask_matrix[1, 1] = 1
38+
if refinement_mask[4] == 'x':
39+
mask_matrix[1, 2] = 1
40+
self.adjuster.setRefinementMask(mask_matrix)
41+
42+
def adjust(self, features, pairwise_matches, estimated_cameras):
43+
b, cameras = self.adjuster.apply(features,
44+
pairwise_matches,
45+
estimated_cameras)
46+
if not b:
47+
raise StitchingError("Camera parameters adjusting failed.")
48+
49+
return cameras

stitching/camera_estimator.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from collections import OrderedDict
2+
import cv2 as cv
3+
import numpy as np
4+
5+
from .stitching_error import StitchingError
6+
7+
8+
class CameraEstimator:
9+
"""https://docs.opencv.org/4.x/df/d15/classcv_1_1detail_1_1Estimator.html""" # noqa
10+
11+
CAMERA_ESTIMATOR_CHOICES = OrderedDict()
12+
CAMERA_ESTIMATOR_CHOICES['homography'] = cv.detail_HomographyBasedEstimator
13+
CAMERA_ESTIMATOR_CHOICES['affine'] = cv.detail_AffineBasedEstimator
14+
15+
DEFAULT_CAMERA_ESTIMATOR = list(CAMERA_ESTIMATOR_CHOICES.keys())[0]
16+
17+
def __init__(self, estimator=DEFAULT_CAMERA_ESTIMATOR, **kwargs):
18+
self.estimator = CameraEstimator.CAMERA_ESTIMATOR_CHOICES[estimator](
19+
**kwargs
20+
)
21+
22+
def estimate(self, features, pairwise_matches):
23+
b, cameras = self.estimator.apply(features, pairwise_matches, None)
24+
if not b:
25+
raise StitchingError("Homography estimation failed.")
26+
for cam in cameras:
27+
cam.R = cam.R.astype(np.float32)
28+
return cameras

stitching/camera_wave_corrector.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from collections import OrderedDict
2+
import cv2 as cv
3+
import numpy as np
4+
5+
6+
class WaveCorrector:
7+
"""https://docs.opencv.org/4.x/d7/d74/group__stitching__rotation.html#ga8faf9588aebd5aeb6f8c649c82beb1fb""" # noqa
8+
9+
WAVE_CORRECT_CHOICES = OrderedDict()
10+
WAVE_CORRECT_CHOICES['horiz'] = cv.detail.WAVE_CORRECT_HORIZ
11+
WAVE_CORRECT_CHOICES['vert'] = cv.detail.WAVE_CORRECT_VERT
12+
WAVE_CORRECT_CHOICES['auto'] = cv.detail.WAVE_CORRECT_AUTO
13+
WAVE_CORRECT_CHOICES['no'] = None
14+
15+
DEFAULT_WAVE_CORRECTION = list(WAVE_CORRECT_CHOICES.keys())[0]
16+
17+
def __init__(self, wave_correct_kind=DEFAULT_WAVE_CORRECTION):
18+
self.wave_correct_kind = WaveCorrector.WAVE_CORRECT_CHOICES[
19+
wave_correct_kind
20+
]
21+
22+
def correct(self, cameras):
23+
if self.wave_correct_kind is not None:
24+
rmats = [np.copy(cam.R) for cam in cameras]
25+
rmats = cv.detail.waveCorrect(rmats, self.wave_correct_kind)
26+
for idx, cam in enumerate(cameras):
27+
cam.R = rmats[idx]
28+
return cameras
29+
return cameras

0 commit comments

Comments
 (0)