From 06b13c6e3bc14efd89674f8bf2dbb6bb37e040bc Mon Sep 17 00:00:00 2001 From: Gitea Date: Thu, 9 Jun 2022 11:48:42 +0200 Subject: [PATCH 1/5] remove python 2 compability --- stitching_tool.py | 237 ++++++++++++++++++++++++++++------------------ 1 file changed, 144 insertions(+), 93 deletions(-) diff --git a/stitching_tool.py b/stitching_tool.py index 9615a6c..8f0d347 100644 --- a/stitching_tool.py +++ b/stitching_tool.py @@ -2,217 +2,268 @@ Command line tool for the stitching package """ -# Python 2/3 compatibility -from __future__ import print_function - import argparse import cv2 as cv import numpy as np from stitching import Stitcher - -from stitching.image_handler import ImageHandler -from stitching.feature_detector import FeatureDetector -from stitching.feature_matcher import FeatureMatcher -from stitching.subsetter import Subsetter -from stitching.camera_estimator import CameraEstimator +from stitching.blender import Blender from stitching.camera_adjuster import CameraAdjuster +from stitching.camera_estimator import CameraEstimator from stitching.camera_wave_corrector import WaveCorrector -from stitching.warper import Warper from stitching.cropper import Cropper -from stitching.exposure_error_compensator import ExposureErrorCompensator # noqa +from stitching.exposure_error_compensator import ExposureErrorCompensator +from stitching.feature_detector import FeatureDetector +from stitching.feature_matcher import FeatureMatcher +from stitching.image_handler import ImageHandler from stitching.seam_finder import SeamFinder -from stitching.blender import Blender +from stitching.subsetter import Subsetter from stitching.timelapser import Timelapser +from stitching.warper import Warper parser = argparse.ArgumentParser( - prog="stitching.py", - description="Rotation model image stitcher" -) -parser.add_argument( - 'img_names', nargs='+', - help="Files to stitch", type=str + prog="stitching.py", description="Rotation model image stitcher" ) +parser.add_argument("img_names", nargs="+", help="Files to stitch", type=str) parser.add_argument( - '--medium_megapix', action='store', + "--medium_megapix", + action="store", default=ImageHandler.DEFAULT_MEDIUM_MEGAPIX, help="Resolution for image registration step. " "The default is %s Mpx" % ImageHandler.DEFAULT_MEDIUM_MEGAPIX, - type=float, dest='medium_megapix' + type=float, + dest="medium_megapix", ) parser.add_argument( - '--detector', action='store', + "--detector", + action="store", default=FeatureDetector.DEFAULT_DETECTOR, help="Type of features used for images matching. " - "The default is '%s'." % FeatureDetector.DEFAULT_DETECTOR, + "The default is '%s'." % FeatureDetector.DEFAULT_DETECTOR, choices=FeatureDetector.DETECTOR_CHOICES.keys(), - type=str, dest='detector' + type=str, + dest="detector", ) parser.add_argument( - '--nfeatures', action='store', + "--nfeatures", + action="store", default=500, - help="Type of features used for images matching. " - "The default is 500.", - type=int, dest='nfeatures' + help="Type of features used for images matching. " "The default is 500.", + type=int, + dest="nfeatures", ) parser.add_argument( - '--matcher_type', action='store', default=FeatureMatcher.DEFAULT_MATCHER, + "--matcher_type", + action="store", + default=FeatureMatcher.DEFAULT_MATCHER, help="Matcher used for pairwise image matching. " - "The default is '%s'." % FeatureMatcher.DEFAULT_MATCHER, + "The default is '%s'." % FeatureMatcher.DEFAULT_MATCHER, choices=FeatureMatcher.MATCHER_CHOICES, - type=str, dest='matcher_type' + type=str, + dest="matcher_type", ) parser.add_argument( - '--range_width', action='store', + "--range_width", + action="store", default=FeatureMatcher.DEFAULT_RANGE_WIDTH, help="uses range_width to limit number of images to match with.", - type=int, dest='range_width' + type=int, + dest="range_width", ) parser.add_argument( - '--try_use_gpu', action='store', default=False, + "--try_use_gpu", + action="store", + default=False, help="Try to use CUDA. The default value is no. " - "All default values are for CPU mode.", - type=bool, dest='try_use_gpu' + "All default values are for CPU mode.", + type=bool, + dest="try_use_gpu", ) parser.add_argument( - '--match_conf', action='store', + "--match_conf", + action="store", help="Confidence for feature matching step. " - "The default is 0.3 for ORB and 0.65 for other feature types.", - type=float, dest='match_conf' + "The default is 0.3 for ORB and 0.65 for other feature types.", + type=float, + dest="match_conf", ) parser.add_argument( - '--confidence_threshold', action='store', + "--confidence_threshold", + action="store", default=Subsetter.DEFAULT_CONFIDENCE_THRESHOLD, help="Threshold for two images are from the same panorama confidence. " - "The default is '%s'." % Subsetter.DEFAULT_CONFIDENCE_THRESHOLD, - type=float, dest='confidence_threshold' + "The default is '%s'." % Subsetter.DEFAULT_CONFIDENCE_THRESHOLD, + type=float, + dest="confidence_threshold", ) parser.add_argument( - '--matches_graph_dot_file', action='store', + "--matches_graph_dot_file", + action="store", default=Subsetter.DEFAULT_MATCHES_GRAPH_DOT_FILE, help="Save matches graph represented in DOT language to file.", - type=str, dest='matches_graph_dot_file' + type=str, + dest="matches_graph_dot_file", ) parser.add_argument( - '--estimator', action='store', + "--estimator", + action="store", default=CameraEstimator.DEFAULT_CAMERA_ESTIMATOR, help="Type of estimator used for transformation estimation. " - "The default is '%s'." % CameraEstimator.DEFAULT_CAMERA_ESTIMATOR, + "The default is '%s'." % CameraEstimator.DEFAULT_CAMERA_ESTIMATOR, choices=CameraEstimator.CAMERA_ESTIMATOR_CHOICES.keys(), - type=str, dest='estimator' + type=str, + dest="estimator", ) parser.add_argument( - '--adjuster', action='store', + "--adjuster", + action="store", default=CameraAdjuster.DEFAULT_CAMERA_ADJUSTER, help="Bundle adjustment cost function. " - "The default is '%s'." % CameraAdjuster.DEFAULT_CAMERA_ADJUSTER, + "The default is '%s'." % CameraAdjuster.DEFAULT_CAMERA_ADJUSTER, choices=CameraAdjuster.CAMERA_ADJUSTER_CHOICES.keys(), - type=str, dest='adjuster' + type=str, + dest="adjuster", ) parser.add_argument( - '--refinement_mask', action='store', + "--refinement_mask", + action="store", default=CameraAdjuster.DEFAULT_REFINEMENT_MASK, help="Set refinement mask for bundle adjustment. It looks like 'x_xxx', " - "where 'x' means refine respective parameter and '_' means don't " - "refine, and has the following format:. " - "The default mask is '%s'. " - "If bundle adjustment doesn't support estimation of selected " - "parameter then the respective flag is ignored." - "" % CameraAdjuster.DEFAULT_REFINEMENT_MASK, - type=str, dest='refinement_mask' + "where 'x' means refine respective parameter and '_' means don't " + "refine, and has the following format:. " + "The default mask is '%s'. " + "If bundle adjustment doesn't support estimation of selected " + "parameter then the respective flag is ignored." + "" % CameraAdjuster.DEFAULT_REFINEMENT_MASK, + type=str, + dest="refinement_mask", ) parser.add_argument( - '--wave_correct_kind', action='store', + "--wave_correct_kind", + action="store", default=WaveCorrector.DEFAULT_WAVE_CORRECTION, help="Perform wave effect correction. " - "The default is '%s'" % WaveCorrector.DEFAULT_WAVE_CORRECTION, + "The default is '%s'" % WaveCorrector.DEFAULT_WAVE_CORRECTION, choices=WaveCorrector.WAVE_CORRECT_CHOICES.keys(), - type=str, dest='wave_correct_kind' + type=str, + dest="wave_correct_kind", ) parser.add_argument( - '--warper_type', action='store', default=Warper.DEFAULT_WARP_TYPE, + "--warper_type", + action="store", + default=Warper.DEFAULT_WARP_TYPE, help="Warp surface type. The default is '%s'." % Warper.DEFAULT_WARP_TYPE, choices=Warper.WARP_TYPE_CHOICES, - type=str, dest='warper_type' + type=str, + dest="warper_type", ) parser.add_argument( - '--low_megapix', action='store', default=ImageHandler.DEFAULT_LOW_MEGAPIX, + "--low_megapix", + action="store", + default=ImageHandler.DEFAULT_LOW_MEGAPIX, help="Resolution for seam estimation and exposure estimation step. " "The default is %s Mpx." % ImageHandler.DEFAULT_LOW_MEGAPIX, - type=float, dest='low_megapix' + type=float, + dest="low_megapix", ) parser.add_argument( - '--crop', action='store', default=Cropper.DEFAULT_CROP, + "--crop", + action="store", + default=Cropper.DEFAULT_CROP, help="Crop black borders around images caused by warping using the " "largest interior rectangle. " "Default is '%s'." % Cropper.DEFAULT_CROP, - type=bool, dest='crop' + type=bool, + dest="crop", ) parser.add_argument( - '--compensator', action='store', + "--compensator", + action="store", default=ExposureErrorCompensator.DEFAULT_COMPENSATOR, help="Exposure compensation method. " - "The default is '%s'." % ExposureErrorCompensator.DEFAULT_COMPENSATOR, + "The default is '%s'." % ExposureErrorCompensator.DEFAULT_COMPENSATOR, choices=ExposureErrorCompensator.COMPENSATOR_CHOICES.keys(), - type=str, dest='compensator' + type=str, + dest="compensator", ) parser.add_argument( - '--nr_feeds', action='store', + "--nr_feeds", + action="store", default=ExposureErrorCompensator.DEFAULT_NR_FEEDS, help="Number of exposure compensation feed.", - type=np.int32, dest='nr_feeds' + type=np.int32, + dest="nr_feeds", ) parser.add_argument( - '--block_size', action='store', + "--block_size", + action="store", default=ExposureErrorCompensator.DEFAULT_BLOCK_SIZE, help="BLock size in pixels used by the exposure compensator. " - "The default is '%s'." % ExposureErrorCompensator.DEFAULT_BLOCK_SIZE, - type=np.int32, dest='block_size' + "The default is '%s'." % ExposureErrorCompensator.DEFAULT_BLOCK_SIZE, + type=np.int32, + dest="block_size", ) parser.add_argument( - '--finder', action='store', default=SeamFinder.DEFAULT_SEAM_FINDER, + "--finder", + action="store", + default=SeamFinder.DEFAULT_SEAM_FINDER, help="Seam estimation method. " - "The default is '%s'." % SeamFinder.DEFAULT_SEAM_FINDER, + "The default is '%s'." % SeamFinder.DEFAULT_SEAM_FINDER, choices=SeamFinder.SEAM_FINDER_CHOICES.keys(), - type=str, dest='finder' + type=str, + dest="finder", ) parser.add_argument( - '--final_megapix', action='store', + "--final_megapix", + action="store", default=ImageHandler.DEFAULT_FINAL_MEGAPIX, help="Resolution for compositing step. Use -1 for original resolution. " - "The default is %s" % ImageHandler.DEFAULT_FINAL_MEGAPIX, - type=float, dest='final_megapix' + "The default is %s" % ImageHandler.DEFAULT_FINAL_MEGAPIX, + type=float, + dest="final_megapix", ) parser.add_argument( - '--blender_type', action='store', default=Blender.DEFAULT_BLENDER, + "--blender_type", + action="store", + default=Blender.DEFAULT_BLENDER, help="Blending method. The default is '%s'." % Blender.DEFAULT_BLENDER, choices=Blender.BLENDER_CHOICES, - type=str, dest='blender_type' + type=str, + dest="blender_type", ) parser.add_argument( - '--blend_strength', action='store', default=Blender.DEFAULT_BLEND_STRENGTH, + "--blend_strength", + action="store", + default=Blender.DEFAULT_BLEND_STRENGTH, help="Blending strength from [0,100] range. " - "The default is '%s'." % Blender.DEFAULT_BLEND_STRENGTH, - type=np.int32, dest='blend_strength' + "The default is '%s'." % Blender.DEFAULT_BLEND_STRENGTH, + type=np.int32, + dest="blend_strength", ) parser.add_argument( - '--timelapse', action='store', default=Timelapser.DEFAULT_TIMELAPSE, + "--timelapse", + action="store", + default=Timelapser.DEFAULT_TIMELAPSE, help="Output warped images separately as frames of a time lapse movie, " - "with 'fixed_' prepended to input file names. " - "The default is '%s'." % Timelapser.DEFAULT_TIMELAPSE, + "with 'fixed_' prepended to input file names. " + "The default is '%s'." % Timelapser.DEFAULT_TIMELAPSE, choices=Timelapser.TIMELAPSE_CHOICES, - type=str, dest='timelapse' + type=str, + dest="timelapse", ) parser.add_argument( - '--output', action='store', default='result.jpg', + "--output", + action="store", + default="result.jpg", help="The default is 'result.jpg'", - type=str, dest='output' + type=str, + dest="output", ) -__doc__ += '\n' + parser.format_help() +__doc__ += "\n" + parser.format_help() -if __name__ == '__main__': +if __name__ == "__main__": args = parser.parse_args() args_dict = vars(args) From f436d51dc238fa1e015b052c71d2bb5e524a8a92 Mon Sep 17 00:00:00 2001 From: Gitea Date: Thu, 9 Jun 2022 12:28:52 +0200 Subject: [PATCH 2/5] install CLI in user data path --- setup.cfg | 5 ++++- stitching/cli/__init__.py | 0 stitching_tool.py => stitching/cli/stitch.py | 11 +++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 stitching/cli/__init__.py rename stitching_tool.py => stitching/cli/stitch.py (98%) diff --git a/setup.cfg b/setup.cfg index 5306583..509dbc1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,9 @@ install_requires = include_package_data = True zip_safe = False +[options.entry_points] +console_scripts = + stitch = stitching.cli.stitch:main + [options.packages.find] -include = stitching exclude = tests diff --git a/stitching/cli/__init__.py b/stitching/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stitching_tool.py b/stitching/cli/stitch.py similarity index 98% rename from stitching_tool.py rename to stitching/cli/stitch.py index 8f0d347..db26e52 100644 --- a/stitching_tool.py +++ b/stitching/cli/stitch.py @@ -22,9 +22,7 @@ from stitching.timelapser import Timelapser from stitching.warper import Warper -parser = argparse.ArgumentParser( - prog="stitching.py", description="Rotation model image stitcher" -) +parser = argparse.ArgumentParser(prog="stitch.py") parser.add_argument("img_names", nargs="+", help="Files to stitch", type=str) parser.add_argument( "--medium_megapix", @@ -263,7 +261,8 @@ __doc__ += "\n" + parser.format_help() -if __name__ == "__main__": + +def main(): args = parser.parse_args() args_dict = vars(args) @@ -283,3 +282,7 @@ cv.imshow(output, preview) cv.waitKey() cv.destroyAllWindows() + + +if __name__ == "__main__": + main() From 211924dd11357f8454cb6bc2c78a5f8bf15e86ca Mon Sep 17 00:00:00 2001 From: Gitea Date: Thu, 9 Jun 2022 12:49:17 +0200 Subject: [PATCH 3/5] improved CLI - made preview optional - store_true, store_false for boolean inputs - removed dublicated 'dest' parameters --- stitching/cli/stitch.py | 62 ++++++++++++++++------------------------- 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/stitching/cli/stitch.py b/stitching/cli/stitch.py index db26e52..949d5b8 100644 --- a/stitching/cli/stitch.py +++ b/stitching/cli/stitch.py @@ -31,7 +31,6 @@ help="Resolution for image registration step. " "The default is %s Mpx" % ImageHandler.DEFAULT_MEDIUM_MEGAPIX, type=float, - dest="medium_megapix", ) parser.add_argument( "--detector", @@ -41,15 +40,13 @@ "The default is '%s'." % FeatureDetector.DEFAULT_DETECTOR, choices=FeatureDetector.DETECTOR_CHOICES.keys(), type=str, - dest="detector", ) parser.add_argument( "--nfeatures", action="store", default=500, - help="Type of features used for images matching. " "The default is 500.", + help="Type of features used for images matching. The default is 500.", type=int, - dest="nfeatures", ) parser.add_argument( "--matcher_type", @@ -59,7 +56,6 @@ "The default is '%s'." % FeatureMatcher.DEFAULT_MATCHER, choices=FeatureMatcher.MATCHER_CHOICES, type=str, - dest="matcher_type", ) parser.add_argument( "--range_width", @@ -67,16 +63,11 @@ default=FeatureMatcher.DEFAULT_RANGE_WIDTH, help="uses range_width to limit number of images to match with.", type=int, - dest="range_width", ) parser.add_argument( "--try_use_gpu", - action="store", - default=False, - help="Try to use CUDA. The default value is no. " - "All default values are for CPU mode.", - type=bool, - dest="try_use_gpu", + action="store_true", + help="Try to use CUDA", ) parser.add_argument( "--match_conf", @@ -84,7 +75,6 @@ help="Confidence for feature matching step. " "The default is 0.3 for ORB and 0.65 for other feature types.", type=float, - dest="match_conf", ) parser.add_argument( "--confidence_threshold", @@ -93,7 +83,6 @@ help="Threshold for two images are from the same panorama confidence. " "The default is '%s'." % Subsetter.DEFAULT_CONFIDENCE_THRESHOLD, type=float, - dest="confidence_threshold", ) parser.add_argument( "--matches_graph_dot_file", @@ -101,7 +90,6 @@ default=Subsetter.DEFAULT_MATCHES_GRAPH_DOT_FILE, help="Save matches graph represented in DOT language to file.", type=str, - dest="matches_graph_dot_file", ) parser.add_argument( "--estimator", @@ -111,7 +99,6 @@ "The default is '%s'." % CameraEstimator.DEFAULT_CAMERA_ESTIMATOR, choices=CameraEstimator.CAMERA_ESTIMATOR_CHOICES.keys(), type=str, - dest="estimator", ) parser.add_argument( "--adjuster", @@ -121,7 +108,6 @@ "The default is '%s'." % CameraAdjuster.DEFAULT_CAMERA_ADJUSTER, choices=CameraAdjuster.CAMERA_ADJUSTER_CHOICES.keys(), type=str, - dest="adjuster", ) parser.add_argument( "--refinement_mask", @@ -135,7 +121,6 @@ "parameter then the respective flag is ignored." "" % CameraAdjuster.DEFAULT_REFINEMENT_MASK, type=str, - dest="refinement_mask", ) parser.add_argument( "--wave_correct_kind", @@ -145,7 +130,6 @@ "The default is '%s'" % WaveCorrector.DEFAULT_WAVE_CORRECTION, choices=WaveCorrector.WAVE_CORRECT_CHOICES.keys(), type=str, - dest="wave_correct_kind", ) parser.add_argument( "--warper_type", @@ -154,7 +138,6 @@ help="Warp surface type. The default is '%s'." % Warper.DEFAULT_WARP_TYPE, choices=Warper.WARP_TYPE_CHOICES, type=str, - dest="warper_type", ) parser.add_argument( "--low_megapix", @@ -163,18 +146,23 @@ help="Resolution for seam estimation and exposure estimation step. " "The default is %s Mpx." % ImageHandler.DEFAULT_LOW_MEGAPIX, type=float, - dest="low_megapix", ) parser.add_argument( "--crop", - action="store", - default=Cropper.DEFAULT_CROP, + action="store_true", help="Crop black borders around images caused by warping using the " "largest interior rectangle. " "Default is '%s'." % Cropper.DEFAULT_CROP, - type=bool, +) +parser.add_argument( + "--no-crop", + action="store_false", + help="Don't Crop black borders around images caused by warping using the " + "largest interior rectangle. " + "Default is '%s'." % (not Cropper.DEFAULT_CROP), dest="crop", ) +parser.set_defaults(crop=Cropper.DEFAULT_CROP) parser.add_argument( "--compensator", action="store", @@ -183,7 +171,6 @@ "The default is '%s'." % ExposureErrorCompensator.DEFAULT_COMPENSATOR, choices=ExposureErrorCompensator.COMPENSATOR_CHOICES.keys(), type=str, - dest="compensator", ) parser.add_argument( "--nr_feeds", @@ -191,7 +178,6 @@ default=ExposureErrorCompensator.DEFAULT_NR_FEEDS, help="Number of exposure compensation feed.", type=np.int32, - dest="nr_feeds", ) parser.add_argument( "--block_size", @@ -200,7 +186,6 @@ help="BLock size in pixels used by the exposure compensator. " "The default is '%s'." % ExposureErrorCompensator.DEFAULT_BLOCK_SIZE, type=np.int32, - dest="block_size", ) parser.add_argument( "--finder", @@ -210,7 +195,6 @@ "The default is '%s'." % SeamFinder.DEFAULT_SEAM_FINDER, choices=SeamFinder.SEAM_FINDER_CHOICES.keys(), type=str, - dest="finder", ) parser.add_argument( "--final_megapix", @@ -219,7 +203,6 @@ help="Resolution for compositing step. Use -1 for original resolution. " "The default is %s" % ImageHandler.DEFAULT_FINAL_MEGAPIX, type=float, - dest="final_megapix", ) parser.add_argument( "--blender_type", @@ -228,7 +211,6 @@ help="Blending method. The default is '%s'." % Blender.DEFAULT_BLENDER, choices=Blender.BLENDER_CHOICES, type=str, - dest="blender_type", ) parser.add_argument( "--blend_strength", @@ -237,7 +219,6 @@ help="Blending strength from [0,100] range. " "The default is '%s'." % Blender.DEFAULT_BLEND_STRENGTH, type=np.int32, - dest="blend_strength", ) parser.add_argument( "--timelapse", @@ -248,7 +229,11 @@ "The default is '%s'." % Timelapser.DEFAULT_TIMELAPSE, choices=Timelapser.TIMELAPSE_CHOICES, type=str, - dest="timelapse", +) +parser.add_argument( + "--preview", + action="store_true", + help="Opens a preview window with the stitched result", ) parser.add_argument( "--output", @@ -256,7 +241,6 @@ default="result.jpg", help="The default is 'result.jpg'", type=str, - dest="output", ) __doc__ += "\n" + parser.format_help() @@ -269,6 +253,7 @@ def main(): # Extract In- and Output img_names = args_dict.pop("img_names") img_names = [cv.samples.findFile(img_name) for img_name in img_names] + preview = args_dict.pop("preview") output = args_dict.pop("output") stitcher = Stitcher(**args_dict) @@ -276,12 +261,13 @@ def main(): cv.imwrite(output, panorama) - zoom_x = 600.0 / panorama.shape[1] - preview = cv.resize(panorama, dsize=None, fx=zoom_x, fy=zoom_x) + if preview: + zoom_x = 600.0 / panorama.shape[1] + preview = cv.resize(panorama, dsize=None, fx=zoom_x, fy=zoom_x) - cv.imshow(output, preview) - cv.waitKey() - cv.destroyAllWindows() + cv.imshow(output, preview) + cv.waitKey() + cv.destroyAllWindows() if __name__ == "__main__": From 095d4a7638bfffa141583b8db43dad12308c560f Mon Sep 17 00:00:00 2001 From: Gitea Date: Thu, 9 Jun 2022 12:54:08 +0200 Subject: [PATCH 4/5] bump version --- stitching/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stitching/__init__.py b/stitching/__init__.py index d80b262..c461c4f 100644 --- a/stitching/__init__.py +++ b/stitching/__init__.py @@ -1,3 +1,3 @@ from .stitcher import Stitcher -__version__ = "0.1.0" +__version__ = "0.2.0" From 87d5c30c082fab225ccbaa3e059dd14b96316543 Mon Sep 17 00:00:00 2001 From: Lukas-Alexander Weber <32765578+lukasalexanderweber@users.noreply.github.com> Date: Thu, 9 Jun 2022 13:02:39 +0200 Subject: [PATCH 5/5] Update README.md --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 657d957..a195298 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,11 @@ panorama = stitcher.stitch(["img1.jpg", "img2.jpg", "img3.jpg"]) ``` -or using the [command line -tool](https://github.com/lukasalexanderweber/stitching/blob/main/stitching_tool.py) +or using the command line interface ([cli](https://github.com/lukasalexanderweber/stitching/tree/main/stitching/cli/stitch.py)) which is available after installation ```bash -python stitching_tool.py -h -python stitching_tool.py img1.jpg img2.jpg img3.jpg +stitch -h +stitch img1.jpg img2.jpg img3.jpg ``` ## Tutorial