Skip to content

Commit 84d75b7

Browse files
committed
created external_sequence for Run30 LV project and updated Readme
1 parent 45f19e3 commit 84d75b7

11 files changed

+399
-59
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,11 @@ Ask for help on our mailing list:
4848

4949

5050

51+
## Working with plugins
52+
53+
Plugins is a system of extensions to PyPTV without the need to change the GUI
54+
55+
1. copy the `external_sequence_list.txt` and `external_tracker_list.txt` to the working folder
56+
2. copy the `plugins/` directory to the working folder
57+
3. modify the code so it performs instead of the default sequence or default tracker
58+
4. Open the GUI and Plugins -> Choose , then run the rest: Init -> Sequence

bump_version.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ def increment_version(version, part='minor'):
4141
elif part == 'minor':
4242
minor += 1
4343
patch = 0
44+
elif part == 'patch':
45+
patch += 1
4446
else:
4547
raise ValueError("Invalid part specified. Use 'major' or 'minor'.")
4648

@@ -56,7 +58,7 @@ def increment_version(version, part='minor'):
5658
print(f"Current version is {current_version}")
5759

5860
# Example usage
59-
new_version = increment_version(current_version, 'minor')
61+
new_version = increment_version(current_version, 'patch')
6062
print(f"New version is {new_version}")
6163

6264
# Update the version in pyproject.toml

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pyptv"
7-
version = "0.3.1"
7+
version = "0.3.2"
88
description = "Python GUI for the OpenPTV library `liboptv`"
99
authors = [
1010
{ name = "Alex Liberzon", email = "alex.liberzon@gmail.com" }

pyptv/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.3.1"
1+
__version__ = "0.3.2"

pyptv/external_sequence_list.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
ext_sequence_denis
1+
plugins/ext_sequence_denis
2+
plugins/ext_sequence_rembg
23

pyptv/external_tracker_list.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
ext_tracker_denis
1+
plugins/ext_tracker_denis
22

File renamed without changes.

pyptv/plugins/ext_sequence_rembg.py

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import random
2+
3+
import numpy as np
4+
from imageio.v3 import imread, imwrite
5+
from pathlib import Path
6+
7+
from skimage import img_as_ubyte
8+
from skimage import filters, measure, morphology
9+
from skimage.color import rgb2gray, label2rgb
10+
from skimage.segmentation import clear_border
11+
from skimage.morphology import binary_erosion, binary_dilation, disk
12+
from skimage.util import img_as_ubyte
13+
14+
from optv.correspondences import correspondences, MatchedCoords
15+
from optv.tracker import default_naming
16+
from optv.orientation import point_positions
17+
18+
import matplotlib.pyplot as plt
19+
20+
21+
def mask_image(imname : Path, display: bool = False) -> np.ndarray:
22+
"""Mask the image using a simple high pass filter.
23+
24+
Parameters
25+
----------
26+
img : np.ndarray
27+
The image to be masked.
28+
29+
Returns
30+
-------
31+
np.ndarray
32+
The masked image.
33+
"""
34+
35+
img = imread(imname)
36+
if img.ndim > 2:
37+
img = rgb2gray(img)
38+
39+
if img.dtype != np.uint8:
40+
img = img_as_ubyte(img)
41+
42+
# Apply Gaussian filter to smooth the image
43+
smoothed_frame = filters.gaussian(img, sigma=5)
44+
45+
if display:
46+
plt.figure()
47+
plt.imshow(smoothed_frame)
48+
plt.show()
49+
50+
# Apply Otsu's thresholding method to segment the object
51+
thresh = filters.threshold_otsu(smoothed_frame)
52+
# print('Threshold:', thresh)
53+
binary_frame = smoothed_frame > 1.1*thresh
54+
55+
if display:
56+
plt.figure()
57+
plt.imshow(binary_frame)
58+
plt.show()
59+
60+
61+
# binary_frame_cleared = clear_border(binary_frame, buffer_size=20)
62+
binary_frame_cleared = binary_frame.copy()
63+
64+
# plt.figure()
65+
# plt.imshow(binary_frame_cleared)
66+
# plt.show()
67+
68+
# Remove small bright objects
69+
cleaned_frame = morphology.remove_small_objects(binary_frame_cleared, min_size=100000)
70+
71+
# %%
72+
# Apply morphological closing to close the boundary
73+
closed_cleaned_frame = binary_dilation(cleaned_frame, disk(21))
74+
closed_cleaned_frame = binary_erosion(closed_cleaned_frame, disk(21))
75+
76+
if display:
77+
# Display the result
78+
plt.figure()
79+
plt.imshow(closed_cleaned_frame, cmap='gray')
80+
plt.title('Closed Boundary of Cleaned Frame')
81+
plt.show()
82+
83+
84+
# check the size of the second largest black hole
85+
# labeled_frame = measure.label(~closed_cleaned_frame)
86+
# regions = measure.regionprops(labeled_frame)
87+
# areas = np.array([r.area for r in regions])
88+
# area_to_remove = np.sort(areas)[-2] # 2nd largest, 1st is the surrounding
89+
90+
# %%
91+
# Fill holes inside the binary frame to remove large black objects
92+
filled_frame = morphology.remove_small_holes(closed_cleaned_frame, area_threshold=2e6)
93+
94+
if display:
95+
# # Display the result
96+
plt.figure()
97+
plt.imshow(filled_frame, cmap='gray')
98+
plt.title('Binary Frame with Large Black Objects Removed')
99+
plt.show()
100+
101+
# %%
102+
103+
# # Remove small objects and clear the border
104+
# cleaned_frame = morphology.remove_small_objects(binary_frame, min_size=100000)
105+
# # Fill holes inside the binary frame to remove dark islands
106+
# filled_frame = morphology.remove_small_holes(cleaned_frame, area_threshold=100000)
107+
108+
# filled_frame = clear_border(filled_frame)
109+
110+
# Label the segmented regions
111+
labeled_frame = measure.label(filled_frame)
112+
113+
if display:
114+
# Show the labeled filled frame as a color labeled image
115+
plt.figure()
116+
plt.imshow(label2rgb(labeled_frame, image=img, bg_label=0))
117+
plt.title('Color Labeled Frame with Filled Holes')
118+
plt.show()
119+
120+
# %%
121+
122+
# Find region properties
123+
regions = measure.regionprops(labeled_frame)
124+
125+
# Assuming the largest region is the object of interest
126+
largest_region = max(regions, key=lambda r: r.area)
127+
128+
129+
# Find the smooth contour that surrounds the largest region
130+
smooth_contour = morphology.convex_hull_image(largest_region.image)
131+
132+
# Create an empty image to draw the smooth contour
133+
smooth_contour_image = np.zeros_like(labeled_frame, dtype=bool)
134+
135+
# Place the smooth contour in the correct location
136+
minr, minc, maxr, maxc = largest_region.bbox
137+
smooth_contour_image[minr:maxr, minc:maxc] = smooth_contour
138+
139+
if display:
140+
# Display the smooth contour on the labeled image
141+
plt.figure()
142+
plt.imshow(labeled_frame, cmap='jet')
143+
plt.contour(smooth_contour_image, colors='red', linewidths=2)
144+
plt.title(f'Segmented Object with Smooth Contour')
145+
plt.show()
146+
147+
148+
# Convert the largest region to a black and white image
149+
bw_image = np.zeros_like(labeled_frame, dtype=bool)
150+
bw_image[largest_region.coords[:, 0], largest_region.coords[:, 1]] = True
151+
152+
# plt.figure(), plt.imshow(bw_image, cmap='gray')
153+
154+
# Apply morphological closing to remove sharp spikes
155+
closed_image = binary_dilation(bw_image, disk(21))
156+
closed_image = binary_erosion(closed_image, disk(21))
157+
158+
if display:
159+
# Display the result
160+
plt.figure()
161+
plt.imshow(closed_image, cmap='gray')
162+
plt.title('Smooth Boundary without Sharp Spikes')
163+
plt.show()
164+
165+
166+
# Apply morphological operations to get the external contour
167+
eroded_image = binary_erosion(closed_image, disk(1))
168+
external_contour = closed_image & ~eroded_image
169+
170+
imwrite(imname.with_suffix('.jpg'), img_as_ubyte(external_contour))
171+
172+
# Dilate the external contour for better visibility
173+
dilated_external_contour = binary_dilation(external_contour, disk(3))
174+
175+
# Create a masked image of the same size as the input image
176+
masked_image = np.zeros_like(img, dtype=np.uint8)
177+
# Mask out (black) everything outside of closed_image
178+
masked_image[closed_image] = img[closed_image]
179+
180+
if display:
181+
plt.figure()
182+
plt.imshow(masked_image)
183+
plt.show()
184+
185+
return masked_image
186+
187+
class Sequence:
188+
"""Sequence class defines external tracking addon for pyptv
189+
User needs to implement the following functions:
190+
do_sequence(self)
191+
192+
Connection to C ptv module is given via self.ptv and provided by pyptv software
193+
Connection to active parameters is given via self.exp1 and provided by pyptv software.
194+
195+
User responsibility is to read necessary files, make the calculations and write the files back.
196+
"""
197+
198+
def __init__(self, ptv=None, exp=None):
199+
self.ptv = ptv
200+
self.exp = exp
201+
202+
def do_sequence(self):
203+
""" Copy of the sequence loop with one change we call everything as
204+
self.ptv instead of ptv.
205+
206+
"""
207+
# Sequence parameters
208+
209+
n_cams, cpar, spar, vpar, tpar, cals = (
210+
self.exp.n_cams,
211+
self.exp.cpar,
212+
self.exp.spar,
213+
self.exp.vpar,
214+
self.exp.tpar,
215+
self.exp.cals,
216+
)
217+
218+
# # Sequence parameters
219+
# spar = SequenceParams(num_cams=n_cams)
220+
# spar.read_sequence_par(b"parameters/sequence.par", n_cams)
221+
222+
223+
# sequence loop for all frames
224+
first_frame = spar.get_first()
225+
last_frame = spar.get_last()
226+
print(f" From {first_frame = } to {last_frame = }")
227+
228+
for frame in range(first_frame, last_frame + 1):
229+
# print(f"processing {frame = }")
230+
231+
detections = []
232+
corrected = []
233+
for i_cam in range(n_cams):
234+
base_image_name = spar.get_img_base_name(i_cam).decode()
235+
imname = Path(base_image_name % frame) # works with jumps from 1 to 10
236+
masked_image = mask_image(imname)
237+
238+
# img = imread(imname)
239+
# if img.ndim > 2:
240+
# img = rgb2gray(img)
241+
242+
# if img.dtype != np.uint8:
243+
# img = img_as_ubyte(img)
244+
245+
246+
247+
high_pass = self.ptv.simple_highpass(masked_image, cpar)
248+
targs = self.ptv.target_recognition(high_pass, tpar, i_cam, cpar)
249+
250+
targs.sort_y()
251+
detections.append(targs)
252+
masked_coords = MatchedCoords(targs, cpar, cals[i_cam])
253+
pos, _ = masked_coords.as_arrays()
254+
corrected.append(masked_coords)
255+
256+
# if any([len(det) == 0 for det in detections]):
257+
# return False
258+
259+
# Corresp. + positions.
260+
sorted_pos, sorted_corresp, _ = correspondences(
261+
detections, corrected, cals, vpar, cpar)
262+
263+
# Save targets only after they've been modified:
264+
# this is a workaround of the proper way to construct _targets name
265+
for i_cam in range(n_cams):
266+
base_name = spar.get_img_base_name(i_cam).decode()
267+
# base_name = replace_format_specifiers(base_name) # %d to %04d
268+
self.ptv.write_targets(detections[i_cam], base_name, frame)
269+
270+
print("Frame " + str(frame) + " had " +
271+
repr([s.shape[1] for s in sorted_pos]) + " correspondences.")
272+
273+
# Distinction between quad/trip irrelevant here.
274+
sorted_pos = np.concatenate(sorted_pos, axis=1)
275+
sorted_corresp = np.concatenate(sorted_corresp, axis=1)
276+
277+
flat = np.array([
278+
corrected[i].get_by_pnrs(sorted_corresp[i])
279+
for i in range(len(cals))
280+
])
281+
pos, _ = point_positions(flat.transpose(1, 0, 2), cpar, cals, vpar)
282+
283+
# if len(cals) == 1: # single camera case
284+
# sorted_corresp = np.tile(sorted_corresp,(4,1))
285+
# sorted_corresp[1:,:] = -1
286+
287+
if len(cals) < 4:
288+
print_corresp = -1 * np.ones((4, sorted_corresp.shape[1]))
289+
print_corresp[:len(cals), :] = sorted_corresp
290+
else:
291+
print_corresp = sorted_corresp
292+
293+
# Save rt_is
294+
rt_is_filename = default_naming["corres"].decode()
295+
rt_is_filename = rt_is_filename + f'.{frame}'
296+
with open(rt_is_filename, "w", encoding="utf8") as rt_is:
297+
rt_is.write(str(pos.shape[0]) + "\n")
298+
for pix, pt in enumerate(pos):
299+
pt_args = (pix + 1, ) + tuple(pt) + tuple(print_corresp[:, pix])
300+
rt_is.write("%4d %9.3f %9.3f %9.3f %4d %4d %4d %4d\n" % pt_args)
301+
302+
File renamed without changes.

0 commit comments

Comments
 (0)