basab1142 commited on
Commit
165f2ce
·
verified ·
1 Parent(s): a36dc4c
Files changed (3) hide show
  1. app.py +412 -0
  2. estimate_homography.py +119 -0
  3. requirements.txt +7 -0
app.py ADDED
@@ -0,0 +1,412 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import cv2
3
+ import numpy as np
4
+ from PIL import Image
5
+ import time
6
+ from streamlit_drawable_canvas import st_canvas
7
+ import matplotlib.pylab as plt
8
+ from estimate_homography import calculate_homography, fit_image_in_target_space
9
+
10
+ stitched_image_rgb, stitched_result = None, None
11
+
12
+ # Function to load an image from uploaded file
13
+ def load_image(uploaded_file):
14
+ img = cv2.imdecode(np.frombuffer(uploaded_file.read(), np.uint8), cv2.IMREAD_GRAYSCALE)
15
+ return img
16
+
17
+ # Function to compute stereo vision and disparity map
18
+ def compute_stereo_vision(img1, img2):
19
+ # Feature Detection and Matching using ORB (ORB is a good alternative for uncalibrated cameras)
20
+ orb = cv2.ORB_create() # ORB is a good alternative to SIFT for uncalibrated cameras
21
+ kp1, des1 = orb.detectAndCompute(img1, None)
22
+ kp2, des2 = orb.detectAndCompute(img2, None)
23
+
24
+ # BFMatcher with default params
25
+ bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
26
+ matches = bf.match(des1, des2)
27
+
28
+ # Sort matches by distance
29
+ matches = sorted(matches, key=lambda x: x.distance)
30
+
31
+ # Estimate the Fundamental Matrix
32
+ pts1 = np.array([kp1[m.queryIdx].pt for m in matches])
33
+ pts2 = np.array([kp2[m.trainIdx].pt for m in matches])
34
+
35
+ # Fundamental matrix using RANSAC to reject outliers
36
+ F, mask = cv2.findFundamentalMat(pts1, pts2, cv2.FM_RANSAC)
37
+
38
+ # Estimate the Camera Pose (Rotation and Translation)
39
+ K = np.eye(3) # Assuming no camera calibration
40
+ E = K.T @ F @ K # Essential matrix
41
+ _, R, T, _ = cv2.recoverPose(E, pts1, pts2)
42
+
43
+ # Stereo Rectification
44
+ stereo_rectify = cv2.stereoRectify(K, None, K, None, img1.shape[::-1], R, T, alpha=0)
45
+ left_map_x, left_map_y = cv2.initUndistortRectifyMap(K, None, R, K, img1.shape[::-1], cv2.CV_32F)
46
+ right_map_x, right_map_y = cv2.initUndistortRectifyMap(K, None, R, K, img2.shape[::-1], cv2.CV_32F)
47
+
48
+ # Apply the rectification transformations to the images
49
+ img1_rectified = cv2.remap(img1, left_map_x, left_map_y, interpolation=cv2.INTER_LINEAR)
50
+ img2_rectified = cv2.remap(img2, right_map_x, right_map_y, interpolation=cv2.INTER_LINEAR)
51
+
52
+ # Resize img2_rectified to match img1_rectified size (if necessary)
53
+ if img1_rectified.shape != img2_rectified.shape:
54
+ img2_rectified = cv2.resize(img2_rectified, (img1_rectified.shape[1], img1_rectified.shape[0]))
55
+
56
+ # Disparity Map Computation using StereoBM
57
+ stereo = cv2.StereoBM_create(numDisparities=16, blockSize=15)
58
+ disparity = stereo.compute(img1_rectified, img2_rectified)
59
+
60
+ return disparity, img1_rectified, img2_rectified
61
+
62
+
63
+ def run_point_est(world_pts, img_pts, img):
64
+ if isinstance(img_pts, list):
65
+ img_pts = np.array(img_pts)
66
+
67
+ if isinstance(world_pts, list):
68
+ world_pts = np.array(world_pts)
69
+
70
+ # Plot the original image with marked points
71
+ st.write("Original Image with Points")
72
+ plt.figure()
73
+ plt.imshow(img)
74
+ plt.scatter(img_pts[:, 0], img_pts[:, 1], color='red')
75
+ plt.axis("off")
76
+ plt.title("Original Image with img points marked in red")
77
+ st.pyplot(plt)
78
+
79
+ H = calculate_homography(img_pts, world_pts) # img_pts = H * world_pts
80
+
81
+ #### Cross check ####
82
+ t_one = np.ones((img_pts.shape[0], 1))
83
+ t_out_pts = np.concatenate((world_pts, t_one), axis=1)
84
+ x = np.matmul(H, t_out_pts.T)
85
+ x = x / x[-1, :]
86
+
87
+ st.write("Given Image Points:", img_pts)
88
+ st.write("Calculated Image Points:", x.T)
89
+ st.write("Homography Matrix (OpenCV):", cv2.findHomography(world_pts, img_pts)[0])
90
+ st.write("Calculated Homography Matrix:", H)
91
+
92
+ #####################
93
+ h, w, _ = img.shape
94
+ corners_img = np.array([[0, 0], [w, 0], [w, h], [0, h]])
95
+ H_inv = np.linalg.inv(H)
96
+ t_out_pts = np.concatenate((corners_img, t_one), axis=1)
97
+ world_crd_corners = np.matmul(H_inv, t_out_pts.T)
98
+ world_crd_corners = world_crd_corners / world_crd_corners[-1, :] # Normalize
99
+
100
+ min_crd = np.amin(world_crd_corners.T, axis=0)
101
+ max_crd = np.amax(world_crd_corners.T, axis=0)
102
+
103
+ offset = min_crd.astype(np.int64)
104
+ offset[2] = 0
105
+
106
+ width_world = np.ceil(max_crd - min_crd)[0] + 1
107
+ height_world = np.ceil(max_crd - min_crd)[1] + 1
108
+
109
+ world_img = np.zeros((int(height_world), int(width_world), 3), dtype=np.uint8)
110
+ mask = np.ones((int(height_world), int(width_world)))
111
+
112
+ out = fit_image_in_target_space(img, world_img, mask, H, offset)
113
+
114
+ st.write("Corrected Image")
115
+ plt.figure()
116
+ plt.imshow(out)
117
+ plt.axis("off")
118
+ plt.title("Corrected Image with Point Point Correspondence")
119
+ st.pyplot(plt)
120
+
121
+
122
+ # Function to stitch images
123
+ def stitch_images(images):
124
+ stitcher = cv2.Stitcher_create() if cv2.__version__.startswith('4') else cv2.createStitcher()
125
+ status, stitched_image = stitcher.stitch(images)
126
+ if status == cv2.Stitcher_OK:
127
+ return stitched_image, status
128
+ else:
129
+ return None, status
130
+
131
+ # Function to match features
132
+ def match_features(images):
133
+ if len(images) < 2:
134
+ return None, "At least two images are required for feature matching."
135
+
136
+ gray1 = cv2.cvtColor(images[0], cv2.COLOR_BGR2GRAY)
137
+ gray2 = cv2.cvtColor(images[1], cv2.COLOR_BGR2GRAY)
138
+
139
+ sift = cv2.SIFT_create()
140
+ keypoints1, descriptors1 = sift.detectAndCompute(gray1, None)
141
+ keypoints2, descriptors2 = sift.detectAndCompute(gray2, None)
142
+
143
+ bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=True)
144
+ matches = bf.match(descriptors1, descriptors2)
145
+ matches = sorted(matches, key=lambda x: x.distance)
146
+
147
+ matched_image = cv2.drawMatches(images[0], keypoints1, images[1], keypoints2, matches[:50], None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
148
+ return matched_image, None
149
+
150
+ # Function to cartoonify an image
151
+ def cartoonify_image(image):
152
+ # Convert to grayscale
153
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
154
+
155
+ gray_blur = cv2.medianBlur(gray, 7)
156
+
157
+ edges = cv2.adaptiveThreshold(
158
+ gray_blur, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 10
159
+ )
160
+
161
+ color = cv2.bilateralFilter(image, 9, 250, 250)
162
+
163
+ cartoon = cv2.bitwise_and(color, color, mask=edges)
164
+
165
+ return cartoon
166
+
167
+ # Streamlit layout and UI
168
+ st.set_page_config(page_title="Image Stitching and Feature Matching", layout="wide")
169
+ st.title("Image Stitching and Feature Matching Application")
170
+
171
+ # State to store captured images
172
+ if "captured_images" not in st.session_state:
173
+ st.session_state["captured_images"] = []
174
+
175
+ if "stitched_image" not in st.session_state:
176
+ st.session_state["stitched_image"] = None
177
+ # Sidebar for displaying captured images
178
+ st.sidebar.header("Captured Images")
179
+ if st.session_state["captured_images"]:
180
+ placeholder = st.sidebar.empty()
181
+ with placeholder.container():
182
+ for i, img in enumerate(st.session_state["captured_images"]):
183
+ img_thumbnail = cv2.resize(img, (100, 100))
184
+ st.image(cv2.cvtColor(img_thumbnail, cv2.COLOR_BGR2RGB), caption=f"Image {i+1}", use_container_width =False)
185
+ if st.button(f"Delete Image {i+1}", key=f"delete_{i}"):
186
+ st.session_state["captured_images"].pop(i)
187
+ placeholder.empty() # Clear and refresh the sidebar
188
+ break
189
+
190
+ # Capture the image from camera input
191
+ st.header("Upload or Capture Images")
192
+ uploaded_files = st.file_uploader("Upload images", type=["jpg", "jpeg", "png"], accept_multiple_files=True)
193
+ captured_image = st.camera_input("Take a picture using your camera")
194
+
195
+ if st.button("Add Captured Image"):
196
+ if captured_image:
197
+ captured_image_array = cv2.cvtColor(np.array(Image.open(captured_image)), cv2.COLOR_RGB2BGR)
198
+ st.session_state["captured_images"].append(captured_image_array)
199
+ st.success(f"Captured image {len(st.session_state['captured_images'])} added!")
200
+
201
+ # Combine uploaded and captured images
202
+ images = [cv2.cvtColor(np.array(Image.open(file)), cv2.COLOR_RGB2BGR) for file in uploaded_files]
203
+ images.extend(st.session_state["captured_images"])
204
+
205
+ st.write(f"Total images: {len(images)}")
206
+
207
+ # Placeholder for dynamic updates
208
+ loading_placeholder = st.empty()
209
+
210
+ # Function to show the loading animation
211
+ def show_loading_bar(placeholder):
212
+ with placeholder:
213
+ st.write("Processing images... Please wait.")
214
+ time.sleep(2)
215
+
216
+ if st.button("Stitch Images"):
217
+ if len(images) < 2:
218
+ st.error("Please provide at least two images for stitching.")
219
+ else:
220
+ show_loading_bar(loading_placeholder)
221
+ stitched_result, status = stitch_images(images)
222
+ loading_placeholder.empty()
223
+ if stitched_result is not None:
224
+ stitched_image_rgb = cv2.cvtColor(stitched_result, cv2.COLOR_BGR2RGB)
225
+ st.image(stitched_image_rgb, caption="Stitched Image", use_container_width=True)
226
+ st.session_state["stitched_image"] = stitched_image_rgb
227
+ st.success("Stitching completed successfully!")
228
+ else:
229
+ st.error(f"Stitching failed with status: {status}.")
230
+ if st.button("Show Matching Features"):
231
+ if len(images) < 2:
232
+ st.error("Please provide at least two images for feature matching.")
233
+ else:
234
+ show_loading_bar(loading_placeholder)
235
+ matched_image, error = match_features(images)
236
+ loading_placeholder.empty()
237
+ if matched_image is not None:
238
+ matched_image_rgb = cv2.cvtColor(matched_image, cv2.COLOR_BGR2RGB)
239
+ st.image(matched_image_rgb, caption="Feature Matching Visualization", use_container_width=True)
240
+ st.success("Feature matching completed successfully!")
241
+ else:
242
+ st.error(error)
243
+
244
+ if st.session_state["stitched_image"] is not None:
245
+ st.header("Homography Transformation on Stitched Image")
246
+
247
+ st.write("### Select Points on Stitched Image")
248
+ stitched_image = st.session_state["stitched_image"]
249
+ image = Image.fromarray(cv2.cvtColor(stitched_image, cv2.COLOR_BGR2RGB))
250
+
251
+ canvas_result = st_canvas(
252
+ fill_color="rgba(255, 0, 0, 0.3)",
253
+ stroke_width=3,
254
+ background_image=image,
255
+ update_streamlit=True,
256
+ drawing_mode="point",
257
+ height=image.height,
258
+ width=image.width,
259
+ key="canvas",
260
+ )
261
+
262
+ img_pts = []
263
+
264
+ if canvas_result.json_data is not None:
265
+ for obj in canvas_result.json_data["objects"]:
266
+ if obj["type"] == "circle":
267
+ x = obj["left"] + obj["width"] / 2
268
+ y = obj["top"] + obj["height"] / 2
269
+ img_pts.append([int(x), int(y)])
270
+
271
+ if img_pts:
272
+ st.write("### Selected Image Points")
273
+ st.write(img_pts)
274
+
275
+ st.write("### Enter Corresponding World Points")
276
+ world_pts = st.text_area(
277
+ "Enter world points as a list of tuples (e.g., [(0, 0), (300, 0), (0, 400), (300, 400)])",
278
+ value="[(0, 0), (300, 0), (0, 400), (300, 400)]",
279
+ )
280
+
281
+ if st.button("Run Homography Transformation"):
282
+ try:
283
+ world_pts = eval(world_pts)
284
+ if len(world_pts) != len(img_pts):
285
+ st.error("The number of world points must match the number of image points.")
286
+ else:
287
+ run_point_est(world_pts, img_pts, stitched_image)
288
+ except Exception as e:
289
+ st.error(f"Error: {e}")
290
+
291
+
292
+ if "stitched_image" in st.session_state:
293
+ st.header("Cartoonify & Do Homography on Your Stitched Image")
294
+ if st.button("Cartoonify Stitched Image"):
295
+ cartoon = cartoonify_image(cv2.cvtColor(st.session_state["stitched_image"], cv2.COLOR_RGB2BGR))
296
+ st.image(cv2.cvtColor(cartoon, cv2.COLOR_BGR2RGB), caption="Cartoonified Image", use_container_width=True)
297
+ st.success("Cartoonification completed successfully!")
298
+
299
+ # Upload images
300
+ st.subheader("Upload Left and Right Images")
301
+ left_image_file = st.file_uploader("Choose the Left Image", type=["jpg", "png", "jpeg"])
302
+ right_image_file = st.file_uploader("Choose the Right Image", type=["jpg", "png", "jpeg"])
303
+
304
+ # Check if both images are uploaded
305
+ if left_image_file and right_image_file:
306
+ # Load the uploaded images
307
+ img1 = load_image(left_image_file)
308
+ img2 = load_image(right_image_file)
309
+
310
+ # Display the uploaded images
311
+ st.image(img1, caption="Left Image", use_container_width =True)
312
+ st.image(img2, caption="Right Image", use_container_width =True)
313
+
314
+ # Compute the stereo vision and disparity map
315
+ disparity, img1_rectified, img2_rectified = compute_stereo_vision(img1, img2)
316
+
317
+ # Display the rectified images
318
+ # st.subheader("Rectified Left Image")
319
+ # st.image(img1_rectified, caption="Rectified Left Image", use_container_width =True)
320
+
321
+ # st.subheader("Rectified Right Image")
322
+ # st.image(img2_rectified, caption="Rectified Right Image", use_container_width =True)
323
+
324
+ # Show the disparity map
325
+ fig, ax = plt.subplots()
326
+ st.subheader("Disparity Map")
327
+ plt.imshow(disparity, cmap='gray')
328
+ plt.title("Disparity Map")
329
+ plt.colorbar()
330
+ st.pyplot(fig)
331
+
332
+ # # Optionally: Display an anaglyph or combined view of the images
333
+ # anaglyph = cv2.merge([img1_rectified, np.zeros_like(img1_rectified), img2_rectified])
334
+ # st.subheader("Anaglyph Stereo View")
335
+ # st.image(anaglyph, caption="Anaglyph Stereo View", use_container_width =True)
336
+
337
+
338
+
339
+ # if "img_pts" not in st.session_state:
340
+ # st.session_state["img_pts"] = []
341
+
342
+ # if "world_pts" not in st.session_state:
343
+ # st.session_state["world_pts"] = []
344
+
345
+ # if "homography_ready" not in st.session_state:
346
+ # st.session_state["homography_ready"] = False
347
+
348
+ # if st.button('Homography Transformation'):
349
+ # if st.session_state["stitched_image"] is not None:
350
+ # st.write("### Select Points on Stitched Image")
351
+ # stitched_image = st.session_state["stitched_image"]
352
+ # image = Image.fromarray(cv2.cvtColor(stitched_image, cv2.COLOR_BGR2RGB))
353
+
354
+ # # Display canvas for selecting points
355
+ # canvas_result = st_canvas(
356
+ # fill_color="rgba(255, 0, 0, 0.3)",
357
+ # stroke_width=3,
358
+ # background_image=image,
359
+ # update_streamlit=True,
360
+ # drawing_mode="point",
361
+ # height=image.height,
362
+ # width=image.width,
363
+ # key="canvas",
364
+ # )
365
+
366
+ # # Collect selected points
367
+ # if canvas_result.json_data is not None:
368
+ # img_pts_temp = []
369
+ # for obj in canvas_result.json_data["objects"]:
370
+ # if obj["type"] == "circle":
371
+ # x = obj["left"] + obj["width"] / 2
372
+ # y = obj["top"] + obj["height"] / 2
373
+ # img_pts_temp.append([int(x), int(y)])
374
+
375
+ # # Only update points if there are new ones
376
+ # if img_pts_temp:
377
+ # st.session_state["img_pts"] = img_pts_temp
378
+
379
+ # # Display the selected points
380
+ # if st.session_state["img_pts"]:
381
+ # st.write("### Selected Image Points")
382
+ # st.write(st.session_state["img_pts"])
383
+
384
+ # # Input world points
385
+ # world_pts_input = st.text_area(
386
+ # "Enter world points as a list of tuples (e.g., [(0, 0), (300, 0), (0, 400), (300, 400)])",
387
+ # value="[(0, 0), (300, 0), (0, 400), (300, 400)]",
388
+ # )
389
+
390
+ # if st.button("Confirm Points and Run Homography"):
391
+ # try:
392
+ # st.session_state["world_pts"] = eval(world_pts_input)
393
+ # if len(st.session_state["world_pts"]) != len(st.session_state["img_pts"]):
394
+ # st.error("The number of world points must match the number of image points.")
395
+ # else:
396
+ # st.session_state["homography_ready"] = True
397
+ # st.success("Points confirmed! Ready for homography transformation.")
398
+ # except Exception as e:
399
+ # st.error(f"Error parsing world points: {e}")
400
+
401
+ # # Perform homography transformation
402
+ # if st.session_state.get("homography_ready"):
403
+ # st.write("### Running Homography Transformation...")
404
+ # try:
405
+ # run_point_est(
406
+ # st.session_state["world_pts"],
407
+ # st.session_state["img_pts"],
408
+ # st.session_state["stitched_image"],
409
+ # )
410
+ # st.session_state["homography_ready"] = False # Reset the flag after execution
411
+ # except Exception as e:
412
+ # st.error(f"Error during homography transformation: {e}")
estimate_homography.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import numpy as np
3
+
4
+ def calculate_homography(in_pts, out_pts):
5
+ """
6
+ Calculates the homography matrix H such that in_pts = H * out_pts.
7
+ :param in_pts: Source points as a numpy array.
8
+ :param out_pts: Destination points as a numpy array.
9
+ :return: Homography matrix H.
10
+ """
11
+ if isinstance(in_pts, list):
12
+ in_pts = np.array(in_pts)
13
+
14
+ if isinstance(out_pts, list):
15
+ out_pts = np.array(out_pts)
16
+
17
+ mat_A, mat_b = build_sys_equations(in_pts, out_pts)
18
+ H = np.matmul(np.linalg.pinv(mat_A), mat_b)
19
+ H = np.reshape(np.hstack((H, 1)), (3, 3))
20
+ return H
21
+
22
+ def build_sys_equations(in_pts, out_pts):
23
+ """
24
+ Builds the system of equations for homography calculation.
25
+ :param in_pts: Array of input points.
26
+ :param out_pts: Array of output points.
27
+ :return: Matrix A and vector b.
28
+ """
29
+ mat_A = np.zeros((np.size(in_pts), 8))
30
+ mat_b = in_pts.ravel()
31
+
32
+ i = 0
33
+ for x, y in out_pts:
34
+ # x row
35
+ mat_A[i][0:3] = [x, y, 1]
36
+ mat_A[i][-2:] = [-x * mat_b[i], -y * mat_b[i]]
37
+
38
+ # y row
39
+ mat_A[i + 1][-5:] = [x, y, 1, -x * mat_b[i + 1], -y * mat_b[i + 1]]
40
+
41
+ i += 2
42
+
43
+ return mat_A, mat_b
44
+
45
+ def fit_image_in_target_space(img_src, img_dst, mask, H, offset=np.array([0, 0, 0])):
46
+ """
47
+ Warps img_src into img_dst using the homography matrix H.
48
+ :param img_src: Source image.
49
+ :param img_dst: Target image.
50
+ :param mask: Mask for the destination region.
51
+ :param H: Homography matrix.
52
+ :param offset: Offset correction array [x_offset, y_offset, 0].
53
+ :return: Transformed image.
54
+ """
55
+ pts = get_pixel_coord(mask) # Get all pixel coordinates in the mask region.
56
+ pts = pts + offset # Apply offset correction.
57
+ out_src = np.matmul(H, pts.T)
58
+ out_src = out_src / out_src[-1, :] # Normalize to homogenous coordinates.
59
+
60
+ out_src = out_src[0:2, :].T # Extract x, y coordinates.
61
+ pts = pts[:, 0:2].astype(np.int64) # Target points in destination image.
62
+
63
+ h, w, _ = img_src.shape
64
+ img_dst = get_pixel_val(img_dst, img_src, pts, out_src, offset)
65
+ return img_dst
66
+
67
+ def get_pixel_coord(mask):
68
+ """
69
+ Extracts x, y coordinates of white pixels in a binary mask.
70
+ :param mask: Binary mask.
71
+ :return: Homogenous coordinates of mask pixels.
72
+ """
73
+ y, x = np.where(mask)
74
+ pts = np.concatenate((x[:, np.newaxis], y[:, np.newaxis], np.ones((x.size, 1))), axis=1)
75
+ return pts
76
+
77
+ def get_pixel_val(img_dst, img_src, pts, out_src, offset):
78
+ """
79
+ Performs bilinear interpolation to fetch pixel values.
80
+ :param img_dst: Destination image.
81
+ :param img_src: Source image.
82
+ :param pts: Points in the destination image.
83
+ :param out_src: Corresponding points in the source image.
84
+ :param offset: Offset correction.
85
+ :return: Updated destination image.
86
+ """
87
+ h, w, _ = img_src.shape
88
+ tl = np.floor(out_src[:, ::-1]).astype(np.int64)
89
+ br = np.ceil(out_src[:, ::-1]).astype(np.int64)
90
+
91
+ pts = pts - offset[:2]
92
+ valid_mask = ~np.logical_or.reduce(
93
+ (np.any(tl < 0, axis=1), np.any(br < 0, axis=1), tl[:, 0] >= h - 1, tl[:, 1] >= w - 1, br[:, 0] >= h - 1, br[:, 1] >= w - 1)
94
+ )
95
+ pts = pts[valid_mask]
96
+ out_src = out_src[valid_mask]
97
+ tl = tl[valid_mask]
98
+ br = br[valid_mask]
99
+
100
+ tr = np.concatenate((tl[:, 0:1], br[:, 1:2]), axis=1)
101
+ bl = np.concatenate((br[:, 0:1], tl[:, 1:2]), axis=1)
102
+
103
+ weight = np.zeros((out_src.shape[0], 4))
104
+ weight[:, 0] = np.linalg.norm(tl - out_src[:, ::-1], axis=1)
105
+ weight[:, 1] = np.linalg.norm(tr - out_src[:, ::-1], axis=1)
106
+ weight[:, 2] = np.linalg.norm(bl - out_src[:, ::-1], axis=1)
107
+ weight[:, 3] = np.linalg.norm(br - out_src[:, ::-1], axis=1)
108
+
109
+ weight[weight == 0] = 1
110
+ weight = 1 / weight
111
+ weight /= np.sum(weight, axis=1, keepdims=True)
112
+
113
+ img_dst[pts[:, 1], pts[:, 0], :] = (
114
+ img_src[tl[:, 0], tl[:, 1], :] * weight[:, 0:1]
115
+ + img_src[tr[:, 0], tr[:, 1], :] * weight[:, 1:2]
116
+ + img_src[bl[:, 0], bl[:, 1], :] * weight[:, 2:3]
117
+ + img_src[br[:, 0], br[:, 1], :] * weight[:, 3:4]
118
+ )
119
+ return img_dst
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ opencv-python-contrib
3
+ opencv-python-headless
4
+ matplotlib
5
+ pillow
6
+ plotly
7
+ streamlit_drawable_canvas