diff --git a/stitching/download_imgs.py b/stitching/download_imgs.py new file mode 100644 index 0000000000000000000000000000000000000000..b4aea9b6b0818c68286812ef77e71a095c81bc97 --- /dev/null +++ b/stitching/download_imgs.py @@ -0,0 +1,18 @@ +import requests +import pathlib + + +def dl_at_qual(pct): + for num in range(9, 51): + path = pathlib.Path(f'imgs/danube_{pct}_pct/{num}.jpg') + if path.exists(): + return + else: + if num == 9: + num = '09' + r = requests.get(f'https://iiif.onb.ac.at/images/REPO/7502199/000000{num}.jpg/full/pct:{pct}/0/native.jpg') + open(path, 'wb').write(r.content) + + +if __name__ == '__main__': + dl_at_qual(10) diff --git a/stitching/expected_matches.txt b/stitching/expected_matches.txt new file mode 100644 index 0000000000000000000000000000000000000000..150bc3576989f272fc7d90ccf92cae7b2bc96415 --- /dev/null +++ b/stitching/expected_matches.txt @@ -0,0 +1,37 @@ +[['9.jpg', '10.jpg'], +['10.jpg', '11.jpg'], +['11.jpg', '12.jpg'], +['12.jpg', '13.jpg'], +['13.jpg', '14.jpg'], +['14.jpg', '15.jpg'], +['15.jpg', '16.jpg'], +['16.jpg', '17.jpg'], +['17.jpg', '18.jpg'], +['18.jpg', '19.jpg'], +['19.jpg', '20.jpg'], +['20.jpg', '21.jpg'], +['21.jpg', '22.jpg'], +['22.jpg', '23.jpg'], +['23.jpg', '24.jpg'], +['24.jpg', '25.jpg'], +['25.jpg', '26.jpg'], +['26.jpg', '27.jpg'], +['27.jpg', '28.jpg'], +['28.jpg', '29.jpg'], +['29.jpg', '30.jpg'], +['30.jpg', '31.jpg'], +['31.jpg', '32.jpg'], +['32.jpg', '33.jpg'], +['33.jpg', '34.jpg'], +['34.jpg', '35.jpg'], +['42.jpg', '43.jpg'], +['43.jpg', '44.jpg'], +['44.jpg', '45.jpg'], +['45.jpg', '46.jpg'], +['46.jpg', '47.jpg'], +['47.jpg', '48.jpg'], +['48.jpg', '49.jpg'], +['49.jpg', '50.jpg'], +['36.jpg', '37.jpg'], +['39.jpg', '40.jpg'], +['40.jpg', '41.jpg']] \ No newline at end of file diff --git a/stitching/plots/avg_conf.png b/stitching/plots/avg_conf.png new file mode 100644 index 0000000000000000000000000000000000000000..17c49e4c31776ce1f7b6b360e2824ae394f6cf82 Binary files /dev/null and b/stitching/plots/avg_conf.png differ diff --git a/stitching/scan.py b/stitching/scan.py index 904e7ef27c5f13b90b4e6a6a60cbec41a689b27c..726a5eac4ca780f0aa16458b8a8745df7a1aaf4a 100644 --- a/stitching/scan.py +++ b/stitching/scan.py @@ -7,6 +7,7 @@ Adapted from https://raw.githubusercontent.com/opencv/opencv/master/samples/pyth import cv2 as cv import numpy as np import matplotlib.pyplot as plt +import json def resize_image(img, scale): @@ -130,6 +131,7 @@ def get_warped_masks_and_images(imgs, cameras, warp_scale, seam_aspect): K[1, 1] *= swa K[1, 2] *= swa corner, image_wp = warper.warp(imgs[idx], K, cameras[idx].R, cv.INTER_LINEAR, cv.BORDER_REFLECT) + # corner, image_wp = warper.warp(imgs[idx], K, cameras[idx].R, cv.INTER_NEAREST, cv.BORDER_CONSTANT) corners.append(corner) images_warped.append(image_wp) p, mask_wp = warper.warp(masks[idx], K, cameras[idx].R, cv.INTER_NEAREST, cv.BORDER_CONSTANT) @@ -186,16 +188,17 @@ def blend_images(corners, imgs, seam_msks, msks_w, comp): def get_reg_data(imgs, img_names, reg_megapix=0.6): - reg_scale = get_scale(imgs[0], reg_megapix) - imgs_med_res = [resize_image(img, reg_scale) for img in imgs] + if reg_megapix == -1: + imgs_med_res = imgs + else: + reg_scale = get_scale(imgs[0], reg_megapix) + imgs_med_res = [resize_image(img, reg_scale) for img in imgs] features = find_features(imgs_med_res) - matches = match_features(features) - # for match in matches: - # print(match.num_inliers, match.confidence, match.src_img_idx, match.dst_img_idx) + matches = match_features(features, match_conf=0.35) indices, img_names_subset, img_subset, features_subset, matches_subset = get_biggest_subset( - img_names, imgs_med_res, features, matches, conf_thresh=0.3, match_conf=0.3) + img_names, imgs_med_res, features, matches, conf_thresh=0.6, match_conf=0.35) - graph = get_matches_graph(img_names_subset, matches_subset, conf_thresh=0.3) + graph = get_matches_graph(img_names_subset, matches_subset, conf_thresh=0.6) cameras = estimate_cameras(features_subset, matches_subset) cameras = refine_cameras(features_subset, matches_subset, cameras, 'xxxxx') return indices, graph, cameras @@ -203,8 +206,14 @@ def get_reg_data(imgs, img_names, reg_megapix=0.6): def prepare_reg_data(imgs, indices, reg_megapix=0.6, seam_megapix=0.1): imgs_subset = [imgs[i[0]] for i in indices] - reg_scale = get_scale(imgs[0], megapix=reg_megapix) - seam_scale = get_scale(imgs[0], megapix=seam_megapix) + if reg_megapix == -1: + reg_scale = 1 + else: + reg_scale = get_scale(imgs[0], megapix=reg_megapix) + if seam_megapix == -1: + seam_scale = 1 + else: + seam_scale = get_scale(imgs[0], megapix=seam_megapix) return imgs_subset, reg_scale, seam_scale @@ -225,21 +234,195 @@ def full_stitching_pipeline(imgs, img_names): indices, graph, cameras = get_reg_data(imgs, img_names, reg_megapix=0.6) imgs_subset, reg_scale, seam_scale = prepare_reg_data(imgs, indices, reg_megapix=0.6, seam_megapix=0.1) result, result_mask = compose_from_reg_data(imgs_subset, cameras, reg_scale, seam_scale) - # print(graph) return result +def debug_stitching_pipeline(imgs, img_names): + features = find_features(imgs) + matches = match_features(features, match_conf=0.3) + # for match in matches: + # print(match.getInliers()) + # print(match.getMatches()) + # print(match.confidence) + # print(match.src_img_idx, match.dst_img_idx) + # print(match.num_inliers) + # print(match.H) + graph = get_matches_graph(img_names, matches, conf_thresh=0.3) + print(graph) + cameras = estimate_cameras(features, matches) + # cameras = refine_cameras(features, matches, cameras, 'xxxxx') + for cam in cameras: + print(cam.R) + corners, images_w, masks_w = get_warped_masks_and_images(imgs, cameras, 1, 1) + print(corners) + corners = [(-3788, -362), (-1872, -178), (0, 0), (1889, -1135), (3450, -2492)] + # corners = [(0, 0), (1947, -556)] #- (2914, 2736) + (2879, 2733) = (-35, -3), (2305, 823) - (2335, 826) = (-30, -3) + # corners = [(0, 0), (1912, -559)] + # corners = [(0, 0), (1583, -88)] + # cv.imwrite('32_w.jpg', images_w[0]) + # cv.imwrite('32_m_w.jpg', masks_w[0]) + # cv.imwrite('33_w.jpg', images_w[1]) + # cv.imwrite('33_m_w.jpg', masks_w[1]) + seam_masks = find_seams(corners, images_w, masks_w) + cv.imwrite('24_sm.jpg', seam_masks[0]) + cv.imwrite('25_sm.jpg', seam_masks[1]) + sizes = get_sizes(images_w) + blender = cv.detail_MultiBandBlender() + dst_sz = cv.detail.resultRoi(corners=corners, sizes=sizes) + blend_strength = 5 + blend_width = np.sqrt(dst_sz[2] * dst_sz[3]) * blend_strength / 100 + blender.setNumBands((np.log(blend_width) / np.log(2.) - 1.).astype(np.int32)) + blender.prepare(dst_sz) + # comp = estimate_exposure_compensation(corners, images_w, masks_w) + for idx in range(len(images_w)): + # comp.apply(idx, corners[idx], images_w[idx], masks_w[idx]) + img_s = images_w[idx].astype(np.int16) + blender.feed(cv.UMat(img_s), seam_masks[idx], corners[idx]) + result, result_mask = blender.blend(None, None) + cv.imwrite('test.jpg', result) + cv.imwrite('test_m.jpg', result_mask) + + +def feature_testing(img_names, comp, match_conf=0.3): + features = [] + feat_finders = [cv.AKAZE_create(), cv.BRISK_create(), cv.KAZE_create(), cv.ORB_create(), cv.SIFT_create()] + for name in img_names: + img = cv.imread(name) + feat_lis = [] + for finder in feat_finders: + feat = cv.detail.computeImageFeatures2(finder, img) + feat_lis.append(feat) + features.append(feat_lis) + features_transposed = list(map(list, zip(*features))) + matches_lis = [match_features(feat, match_conf=match_conf) for feat in features_transposed] + H_lis = [] + for m in matches_lis: + c = [] + for info in m: + if info.src_img_idx + 1 == info.dst_img_idx: + c.append(info) + H_lis.append(c) + # H_transposed = list(map(list, zip(*H_lis))) + # plt.plot(list(range(len(img_names) - 1)), confidences_transposed) + # plt.title(f'Confidences of feature detection algorithms at match_conf={match_conf}') + # plt.legend(['AKAZE', 'BRISK', 'KAZE', 'ORB', 'SIFT']) + # plt.savefig(f'comp_{comp}_match_conf_{match_conf}.png') + # plt.clf() + return H_lis + + +def match_sift_features(img_names, match_conf=0.4): + features = [] + feat_finder = cv.SIFT_create() + for name in img_names: + img = cv.imread(name) + feat = cv.detail.computeImageFeatures2(feat_finder, img) + features.append(feat) + matches = match_features(features, match_conf=match_conf) + overlap_matches = [] + for match in matches: + if match.src_img_idx + 1 == match.dst_img_idx: + overlap_matches.append(match) + return overlap_matches + + +def extract_from_R_mat(R_mat): + scale = np.sqrt(R_mat[0, 0] ** 2 + R_mat[0, 1] ** 2) + angle = np.arcsin(R_mat[1, 0] / scale) * 180 / np.pi + t_x = R_mat[0, 2] + t_y = R_mat[1, 2] + return angle, scale, t_x, t_y + + +def truncate_path(s): + return s.replace('imgs/danube_10_pct/', '') + + +def get_match_details(img_names, matches_lis): + details_lis = [] + for match in matches_lis: + details = {} + angle, scale, x, y = extract_from_R_mat(match.H) + src_idx = match.src_img_idx + dst_idx = match.dst_img_idx + details['src_name'] = truncate_path(img_names[src_idx]) + details['src_shape'] = get_img_shape(img_names[src_idx]) + details['dst_name'] = truncate_path(img_names[dst_idx]) + details['dst_shape'] = get_img_shape(img_names[dst_idx]) + details['angle'] = angle + details['scale'] = scale + details['x'] = x + details['y'] = y + details_lis.append(details) + return details_lis + + +def get_img_shape(img_name): + img = cv.imread(img_name) + return img.shape[1], img.shape[0] + + if __name__ == '__main__': - # img_names = [f'pano_{i}.jpg' for i in range(1, 4)] - # img_names = ['left.png', 'right.png'] - # img_names = ['left2.png', 'right2.png'] - img_names = ['wien_low1.jpg', 'prague2.jpg'] - # img_names = ['prague1.jpg', 'prague2.jpg'] - imgs = [cv.imread(f'imgs/{name}') for name in img_names] + path = 'imgs/danube_10_pct' + comp_1 = [f'{path}/{i}.jpg' for i in range(9, 36)] + comp_2 = [f'{path}/{i}.jpg' for i in range(42, 51)] + comp_3 = [f'{path}/39.jpg', f'{path}/40.jpg', f'{path}/41.jpg'] + comp_4 = [f'{path}/36.jpg', f'{path}/37.jpg'] + comp_5 = [f'{path}/38.jpg'] + img_names = [f'imgs/danube_10_pct/{i}.jpg' for i in range(9, 51)] + # imgs = [cv.imread(f'imgs/{name}') for name in img_names] + debug_mode = True try: - result = full_stitching_pipeline(imgs, img_names) - out_name = 'imgs_out/wien_test.jpg' - cv.imwrite(out_name, result) - print('Stitched image saved as ' + out_name) + if debug_mode: + # test_scale = ['imgs/danube_10_pct/13.jpg', 'imgs/danube_10_pct/14_scaled_down.jpg'] + all_matches = [match_sift_features(eval(f'comp_{i}'), match_conf=0.4) for i in range(1, 5)] + match_details = [get_match_details(eval(f'comp_{i+1}'), all_matches[i]) for i in range(0, 4)] + components = [{'component': i+1, 'data': match_details[i]} for i in range(0, 4)] + for comp in components: + print(comp) + with open('stitch_data.json', 'w') as fp: + json.dump(components, fp) + # with open('stitch_data.json', 'r') as fp: + # components = json.load(fp) + # H_2 = feature_testing(comp_2, 2, match_conf=0.4) + # H_3 = feature_testing(comp_3, 3, match_conf=0.4) + # H_4 = feature_testing(comp_4, 4, match_conf=0.4) + # conf_1 = feature_testing(comp_1, 1, match_conf=0.8) + # conf_2 = feature_testing(comp_2, 2, match_conf=0.8) + # conf_3 = feature_testing(comp_3, 3, match_conf=0.8) + # conf_4 = feature_testing(comp_4, 4, match_conf=0.8) + # with open('confs_match_conf_0.8.csv', 'w') as file: + # json.dump([conf_1, conf_2, conf_3, conf_4], file) + # confs = [] + # for i in range(2, 9): + # with open(f'confs_match_conf_0.{i}.csv', 'r') as file: + # lis = json.load(file) + # flat_lis = [item for sublist in lis for item in sublist] + # avg_conf = np.mean(flat_lis, axis=0) + # confs.append(avg_conf) + # plt.plot(np.arange(0.2, 0.8, 0.1), confs) + # plt.legend(['AKAZE', 'BRISK', 'KAZE', 'ORB', 'SIFT']) + # plt.title('Average confidence for different match_conf') + # plt.savefig('avg_conf.png') + # orb = cv.ORB.create() + # crop1 = imgs[0][0:2765, 1920:3246] + # crop2 = imgs[1][0:3089, 0:1170] + # imgs = [crop1, crop2] + # debug_stitching_pipeline(imgs, img_names) + # kp1, des1 = orb.detectAndCompute(imgs[0], None) + # kp2, des2 = orb.detectAndCompute(imgs[1], None) + # feat1 = cv.detail.computeImageFeatures2(orb, imgs[0]) + # feat2 = cv.detail.computeImageFeatures2(orb, imgs[1]) + # afm = cv.detail_AffineBestOf2NearestMatcher.create() + # matches_info = afm.apply(feat1, feat2) + # matches = matches_info.getMatches() + # matches = sorted(matches, key=lambda x: x.distance) + # img = cv.drawMatches(imgs[0], kp1, imgs[1], kp2, matches[:20], None) + # cv.imwrite('imgs_out/matches3.jpg', img) + else: + imgs = [cv.imread(name) for name in img_names] + result = full_stitching_pipeline(imgs, img_names) + res_name = 'area.jpg' + cv.imwrite(res_name, result) except SystemExit: print('program execution stopped!') diff --git a/stitching/stitch_data.json b/stitching/stitch_data.json new file mode 100644 index 0000000000000000000000000000000000000000..9761973c17d98e818b0b4a4161d8f4840d304bd6 --- /dev/null +++ b/stitching/stitch_data.json @@ -0,0 +1 @@ +[{"component": 1, "data": [{"src_name": "9.jpg", "src_shape": [845, 627], "dst_name": "10.jpg", "dst_shape": [916, 630], "angle": -0.08323251825809101, "scale": 1.0012847517044934, "x": -725.8677796027105, "y": 1.327231526027312}, {"src_name": "10.jpg", "src_shape": [916, 630], "dst_name": "11.jpg", "dst_shape": [908, 758], "angle": -0.3874085856970733, "scale": 0.9993879049233608, "x": -727.5777707043392, "y": 4.190702764230509}, {"src_name": "11.jpg", "src_shape": [908, 758], "dst_name": "12.jpg", "dst_shape": [884, 756], "angle": 0.07714987376645693, "scale": 1.0001919853845924, "x": -752.6976600173008, "y": -2.722047131190443}, {"src_name": "12.jpg", "src_shape": [884, 756], "dst_name": "13.jpg", "dst_shape": [924, 703], "angle": -0.04060580582383663, "scale": 1.0004665056559117, "x": -770.8630238226102, "y": 71.61996380936549}, {"src_name": "13.jpg", "src_shape": [924, 703], "dst_name": "14.jpg", "dst_shape": [1179, 824], "angle": 19.03415666322512, "scale": 0.9966014463696012, "x": -514.9510007319319, "y": -124.7481107560606}, {"src_name": "14.jpg", "src_shape": [1179, 824], "dst_name": "15.jpg", "dst_shape": [1176, 723], "angle": 12.080696657848115, "scale": 1.0011571532839152, "x": -818.5039133607494, "y": -158.31696770299678}, {"src_name": "15.jpg", "src_shape": [1176, 723], "dst_name": "16.jpg", "dst_shape": [937, 640], "angle": 28.592036680489468, "scale": 1.0017053478189943, "x": -580.5875386714158, "y": -473.35757729772814}, {"src_name": "16.jpg", "src_shape": [937, 640], "dst_name": "17.jpg", "dst_shape": [1198, 741], "angle": -7.945887553718481, "scale": 0.998128975245677, "x": -804.3304803657236, "y": 204.68126293641077}, {"src_name": "17.jpg", "src_shape": [1198, 741], "dst_name": "18.jpg", "dst_shape": [945, 635], "angle": -29.61161267387781, "scale": 1.0013304876178608, "x": -929.3256677685242, "y": 510.1955368303441}, {"src_name": "18.jpg", "src_shape": [945, 635], "dst_name": "19.jpg", "dst_shape": [1138, 702], "angle": -4.556207737313075, "scale": 0.9997737292558313, "x": -789.9232782713025, "y": 126.25049426119003}, {"src_name": "19.jpg", "src_shape": [1138, 702], "dst_name": "20.jpg", "dst_shape": [1053, 640], "angle": -21.63309606966946, "scale": 0.9957128541702706, "x": -877.5442565359942, "y": 371.4400443436195}, {"src_name": "20.jpg", "src_shape": [1053, 640], "dst_name": "21.jpg", "dst_shape": [1116, 674], "angle": 2.879875469659923, "scale": 0.9998627526693865, "x": -761.7302014882371, "y": -46.72425088925411}, {"src_name": "21.jpg", "src_shape": [1116, 674], "dst_name": "22.jpg", "dst_shape": [1026, 741], "angle": 15.501171289342592, "scale": 0.9999075279205494, "x": -745.2862873407202, "y": -166.09871117818025}, {"src_name": "22.jpg", "src_shape": [1026, 741], "dst_name": "23.jpg", "dst_shape": [1259, 777], "angle": -5.58797413363355, "scale": 0.999133293150094, "x": -647.2823465060284, "y": 106.21263420892734}, {"src_name": "23.jpg", "src_shape": [1259, 777], "dst_name": "24.jpg", "dst_shape": [1072, 633], "angle": -20.63080212766057, "scale": 0.9991346403831461, "x": -1051.9550572768376, "y": 338.61525237180757}, {"src_name": "24.jpg", "src_shape": [1072, 633], "dst_name": "25.jpg", "dst_shape": [977, 627], "angle": -0.03823885067939719, "scale": 0.9999754255389337, "x": -768.5489616265443, "y": -1.0028453153164518}, {"src_name": "25.jpg", "src_shape": [977, 627], "dst_name": "26.jpg", "dst_shape": [1322, 702], "angle": 5.354330694304631, "scale": 0.9993709620252941, "x": -690.827078455422, "y": -71.3667599052535}, {"src_name": "26.jpg", "src_shape": [1322, 702], "dst_name": "27.jpg", "dst_shape": [1007, 632], "angle": 40.197091993384845, "scale": 0.9989990990456168, "x": -445.1440206705711, "y": -632.2760304280525}, {"src_name": "27.jpg", "src_shape": [1007, 632], "dst_name": "28.jpg", "dst_shape": [1020, 627], "angle": -0.07742190085639233, "scale": 0.9993720080141424, "x": -834.6668719016778, "y": -1.7376411784221855}, {"src_name": "28.jpg", "src_shape": [1020, 627], "dst_name": "29.jpg", "dst_shape": [960, 627], "angle": 0.08381344975005875, "scale": 1.0002555829919488, "x": -704.108783857091, "y": -0.7416929236555762}, {"src_name": "29.jpg", "src_shape": [960, 627], "dst_name": "30.jpg", "dst_shape": [947, 627], "angle": -0.07713193723763126, "scale": 0.9993186556532744, "x": -781.8471778231437, "y": -0.45372213170232434}, {"src_name": "30.jpg", "src_shape": [947, 627], "dst_name": "31.jpg", "dst_shape": [1388, 739], "angle": -0.5483236647987095, "scale": 0.9994769073858493, "x": -809.7688783926799, "y": 9.486127607310602}, {"src_name": "31.jpg", "src_shape": [1388, 739], "dst_name": "32.jpg", "dst_shape": [1032, 679], "angle": -89.86274191480732, "scale": 0.9998071486991533, "x": -510.87536291751366, "y": 1385.9836340266602}, {"src_name": "32.jpg", "src_shape": [1032, 679], "dst_name": "33.jpg", "dst_shape": [1030, 634], "angle": -14.493348979068939, "scale": 1.001578824314171, "x": -757.2064029176969, "y": 232.79422793291977}, {"src_name": "33.jpg", "src_shape": [1030, 634], "dst_name": "34.jpg", "dst_shape": [932, 629], "angle": -0.16731881941073357, "scale": 1.0008225355013083, "x": -846.0615251668322, "y": 3.1709044191330316}, {"src_name": "34.jpg", "src_shape": [932, 629], "dst_name": "35.jpg", "dst_shape": [891, 627], "angle": 0.10905914327918778, "scale": 0.9984874307290723, "x": -719.2575072074923, "y": -0.5041634588358995}]}, {"component": 2, "data": [{"src_name": "42.jpg", "src_shape": [1083, 624], "dst_name": "43.jpg", "dst_shape": [949, 622], "angle": 0.2893821083225323, "scale": 0.9995359432497662, "x": -783.5510704757684, "y": -6.320499726540566}, {"src_name": "43.jpg", "src_shape": [949, 622], "dst_name": "44.jpg", "dst_shape": [1047, 684], "angle": -1.5083426303790213, "scale": 0.9992216476694503, "x": -782.9084950985991, "y": 35.904104956761635}, {"src_name": "44.jpg", "src_shape": [1047, 684], "dst_name": "45.jpg", "dst_shape": [1151, 851], "angle": -16.3281881212274, "scale": 1.0001136442280363, "x": -711.0771906589666, "y": 266.8568160409473}, {"src_name": "45.jpg", "src_shape": [1151, 851], "dst_name": "46.jpg", "dst_shape": [1300, 923], "angle": 18.139198557216538, "scale": 1.0008667495612404, "x": -531.3371856145068, "y": -244.93581461312954}, {"src_name": "46.jpg", "src_shape": [1300, 923], "dst_name": "47.jpg", "dst_shape": [1330, 1135], "angle": -6.264699513389657, "scale": 0.9993744102925644, "x": -1015.8542832900469, "y": 371.01288964629526}, {"src_name": "47.jpg", "src_shape": [1330, 1135], "dst_name": "48.jpg", "dst_shape": [1299, 1107], "angle": 13.852251752896482, "scale": 1.0006375150035631, "x": -762.5982928801842, "y": -307.78195866642574}, {"src_name": "48.jpg", "src_shape": [1299, 1107], "dst_name": "49.jpg", "dst_shape": [932, 1235], "angle": 16.445423280814605, "scale": 1.0068292749333456, "x": -728.6673781812307, "y": -260.9078565606926}, {"src_name": "49.jpg", "src_shape": [932, 1235], "dst_name": "50.jpg", "dst_shape": [817, 1235], "angle": -0.1245182696481108, "scale": 1.0003616504118136, "x": -779.5992511948398, "y": 3.3677247076398014}]}, {"component": 3, "data": [{"src_name": "39.jpg", "src_shape": [1121, 1352], "dst_name": "40.jpg", "dst_shape": [920, 1550], "angle": -0.07760783438284052, "scale": 1.0001539718062533, "x": -959.8032830385117, "y": 262.9132319512283}, {"src_name": "40.jpg", "src_shape": [920, 1550], "dst_name": "41.jpg", "dst_shape": [945, 1559], "angle": -0.49981482199990906, "scale": 0.9996924085885643, "x": -639.2482216447894, "y": 10.770331317759712}]}, {"component": 4, "data": [{"src_name": "36.jpg", "src_shape": [1045, 825], "dst_name": "37.jpg", "dst_shape": [1307, 1006], "angle": -0.015660440215784714, "scale": 1.0005636035214283, "x": -630.694768562902, "y": 179.27784988325527}]}] \ No newline at end of file