Skip to content

Commit 985d249

Browse files
Added picking_methods, more filters, and a showcase file
1 parent 140265a commit 985d249

File tree

4 files changed

+580
-55
lines changed

4 files changed

+580
-55
lines changed
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Tutorial on how to automatically select good features to track"
8+
]
9+
},
10+
{
11+
"cell_type": "code",
12+
"execution_count": null,
13+
"metadata": {},
14+
"outputs": [],
15+
"source": [
16+
"import os\n",
17+
"import sys\n",
18+
"repo_root = os.path.abspath(\"..\")\n",
19+
"sys.path.insert(0, repo_root)\n",
20+
"\n",
21+
"import numpy as np\n",
22+
"import matplotlib.pyplot as plt\n",
23+
"import pyidi"
24+
]
25+
},
26+
{
27+
"cell_type": "markdown",
28+
"metadata": {},
29+
"source": [
30+
"Load the video"
31+
]
32+
},
33+
{
34+
"cell_type": "code",
35+
"execution_count": null,
36+
"metadata": {},
37+
"outputs": [],
38+
"source": [
39+
"filename = '../data/data_synthetic.cih'\n",
40+
"video = pyidi.VideoReader(filename)"
41+
]
42+
},
43+
{
44+
"cell_type": "markdown",
45+
"metadata": {},
46+
"source": [
47+
"### `Lucas Kanade` method as a reference"
48+
]
49+
},
50+
{
51+
"cell_type": "code",
52+
"execution_count": null,
53+
"metadata": {},
54+
"outputs": [],
55+
"source": [
56+
"tol = 1e-8\n",
57+
"roi_size = (5,5)\n",
58+
"idi_lk = pyidi.LucasKanade(video)\n",
59+
"idi_lk.configure(reference_image = (0,100), resume_analysis = False, tol=tol, roi_size=roi_size)\n",
60+
"reference_image = idi_lk._set_reference_image(video, idi_lk.reference_image)"
61+
]
62+
},
63+
{
64+
"cell_type": "markdown",
65+
"metadata": {},
66+
"source": [
67+
"### Generate a score image\n",
68+
"- Initialize the Feature Selector\n",
69+
"- Set a filter type\n",
70+
"- Set roi_size. Either directly in set_filter or trough set_roi\n",
71+
"- Apply filter to generate the score_image (saved in feature_selector.score_image)\n",
72+
"\n",
73+
"All available methods are listed when initializing the FeatureSelector, unless verbose = False"
74+
]
75+
},
76+
{
77+
"cell_type": "code",
78+
"execution_count": null,
79+
"metadata": {},
80+
"outputs": [],
81+
"source": [
82+
"from pyidi.Automatic_selection.feature_selector import FeatureSelector\n",
83+
"feature_selector = FeatureSelector(reference_image)\n",
84+
"\n",
85+
"feature_selector.set_filter('ST', roi_size = roi_size)\n",
86+
"# feature_selector.set_filter('DF', dij=(1, 0), c = 0.1)\n",
87+
"# feature_selector.set_filter('HARRIS', alpha = 0.04)\n",
88+
"# feature_selector.set_filter('TRIGGS', alpha = 0.05)\n",
89+
"# feature_selector.set_filter('HM')\n",
90+
"\n",
91+
"feature_selector.set_roi(roi_size)\n",
92+
"\n",
93+
"feature_selector.apply_filter()"
94+
]
95+
},
96+
{
97+
"cell_type": "markdown",
98+
"metadata": {},
99+
"source": [
100+
"- Set a picker and parse parameters\n",
101+
"- Pick features. If no score_image is generated yet, pick_points() will call apply_filter() to generate one. pick_points() will also take a score_image directly, like this: pick_points(score_image)."
102+
]
103+
},
104+
{
105+
"cell_type": "code",
106+
"execution_count": null,
107+
"metadata": {},
108+
"outputs": [],
109+
"source": [
110+
"%matplotlib inline\n",
111+
"feature_selector.set_picker('LM', min_distance = 10, n_points = 50)\n",
112+
"# feature_selector.set_picker('ANMS', n_points=50)\n",
113+
"# feature_selector.set_picker('DS', n_points=50, min_distance = 7, min_score = 50)\n",
114+
"points = feature_selector.pick_points()\n",
115+
"\n",
116+
"fig, ax = plt.subplots(2)\n",
117+
"ax[0].imshow(reference_image, cmap = 'grey')\n",
118+
"ax[0].plot(points[:,1], points[:,0], 'x')\n",
119+
"ax[1].imshow(feature_selector.score_image, cmap = 'grey')\n",
120+
"ax[1].plot(points[:,1], points[:,0], 'x')\n",
121+
"plt.show()"
122+
]
123+
},
124+
{
125+
"cell_type": "markdown",
126+
"metadata": {},
127+
"source": [
128+
"### Masking\n",
129+
"I had been using ROIselect (now SubsetSelection) to generate a ROI polygon. I only select features inside of the ROI\n",
130+
"\n",
131+
"I think will think about how to include this more elegantly in the pipeline, but here is an example below"
132+
]
133+
},
134+
{
135+
"cell_type": "code",
136+
"execution_count": null,
137+
"metadata": {},
138+
"outputs": [],
139+
"source": [
140+
"# from pyidi.GUIs.selection import SubsetSelection\n",
141+
"from matplotlib.path import Path\n",
142+
"\n",
143+
"# roi_select = SubsetSelection(video)\n",
144+
"# roi_select.polygon\n",
145+
"# polygon_new = np.array(roi_select.polygon)\n",
146+
"\n",
147+
"polygon = np.array([[4, 4, 97, 98, 4], [25, 218, 219, 25, 25]])\n",
148+
"path = Path(polygon.T)\n",
149+
"\n",
150+
"\n",
151+
"mask_image = path.contains_points(np.array([(i,j) for i in range(reference_image.shape[0]) for j in range(reference_image.shape[1])])).reshape(reference_image.shape)\n",
152+
"mask_image = mask_image.reshape(reference_image.shape)\n",
153+
"\n",
154+
"score_image = feature_selector.score_image\n",
155+
"score_image[~mask_image] = 0\n",
156+
"\n",
157+
"points = feature_selector.pick_points()\n",
158+
"\n",
159+
"fig, ax = plt.subplots(2)\n",
160+
"ax[0].imshow(reference_image, cmap = 'grey')\n",
161+
"ax[0].plot(points[:,1], points[:,0], 'x')\n",
162+
"ax[1].imshow(score_image, cmap = 'grey')\n",
163+
"ax[1].plot(points[:,1], points[:,0], 'x')\n",
164+
"plt.show()"
165+
]
166+
}
167+
],
168+
"metadata": {
169+
"hide_input": false,
170+
"kernelspec": {
171+
"display_name": "pyidi-env",
172+
"language": "python",
173+
"name": "python3"
174+
},
175+
"language_info": {
176+
"codemirror_mode": {
177+
"name": "ipython",
178+
"version": 3
179+
},
180+
"file_extension": ".py",
181+
"mimetype": "text/x-python",
182+
"name": "python",
183+
"nbconvert_exporter": "python",
184+
"pygments_lexer": "ipython3",
185+
"version": "3.10.18"
186+
},
187+
"toc": {
188+
"base_numbering": 1,
189+
"nav_menu": {},
190+
"number_sections": true,
191+
"sideBar": true,
192+
"skip_h1_title": false,
193+
"title_cell": "Table of Contents",
194+
"title_sidebar": "Contents",
195+
"toc_cell": false,
196+
"toc_position": {},
197+
"toc_section_display": true,
198+
"toc_window_display": false
199+
}
200+
},
201+
"nbformat": 4,
202+
"nbformat_minor": 2
203+
}
Lines changed: 97 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,22 @@
11
import numpy as np
22
from scipy.ndimage import generic_filter
33

4-
from .filters import ShiTomasi
5-
available_filter_shortcuts = {'ST': ShiTomasi}
4+
from .filters import ShiTomasi, DirectionalFilter, Harris, Triggs, HarmonicMean
5+
6+
available_filter_shortcuts = {
7+
'ST': ShiTomasi,
8+
'DF': DirectionalFilter,
9+
'HARRIS': Harris,
10+
'TRIGGS': Triggs,
11+
'HM': HarmonicMean,
12+
}
13+
from .picking_methods import LocalMaxima, ANMS, DescendingScore
14+
available_pickers_shortcuts = {
15+
'LM': LocalMaxima,
16+
'ANMS': ANMS,
17+
'DS': DescendingScore,
18+
}
19+
620

721
class FeatureSelector():
822
""" Selects features from an image using different filters.
@@ -14,34 +28,96 @@ class FeatureSelector():
1428
Args:
1529
image (ndarray): The image to select features from.
1630
"""
17-
def __init__(self, image) -> None:
31+
def __init__(self, image, verbose=True) -> None:
1832
self.image = image
19-
self.roi_size = 9
20-
self.available_filter_shortcuts = available_filter_shortcuts
21-
return
33+
self.roi_size = (9, 9)
34+
self.score_image = None
35+
36+
if verbose:
37+
print("\nAvailable Filters and the parameters they take:")
38+
for name, cls in available_filter_shortcuts.items():
39+
parameters = getattr(cls, 'parameters', [])
40+
filter_parameters = getattr(cls, 'filter_parameters', [])
41+
param_str = f"({', '.join(parameters)})" if parameters else ""
42+
filter_param_str = f"({', '.join(filter_parameters)})" if filter_parameters else ""
43+
print(f" '{name}': {cls.__name__} {param_str} {filter_param_str}")
44+
print("\nAvailable Pickers and the parameters they take:")
45+
for name, cls in available_pickers_shortcuts.items():
46+
parameters = getattr(cls, 'parameters', [])
47+
param_str = f"({', '.join(parameters)})" if parameters else ""
48+
print(f" '{name}': {cls.__name__} {param_str}")
2249

23-
def set_filter(self, filter_shortcut):
24-
"""
25-
Sets filter
26-
"""
27-
if isinstance(filter_shortcut, str) and filter_shortcut in self.available_filter_shortcuts.keys():
28-
self.filter_shortcut = filter_shortcut
29-
self.filter = self.available_filter_shortcuts[filter_shortcut](self.image)
30-
else:
31-
"Filter shortcut not recognized"
50+
def set_filter(self, name, **kwargs):
51+
name = name.upper()
52+
if name not in available_filter_shortcuts:
53+
raise ValueError(f"Filter '{name}' not recognized. Available: {list(available_filter_shortcuts.keys())}")
54+
55+
filter_class = available_filter_shortcuts[name]
56+
57+
# Extract and remove 'roi_size' if present
58+
roi_size = kwargs.pop('roi_size', None)
59+
if roi_size is not None:
60+
self.set_roi(roi_size)
61+
62+
# Validate kwargs
63+
supported1 = getattr(filter_class, 'parameters', [])
64+
supported2 = getattr(filter_class, 'filter_parameters', [])
65+
supported = supported1 + supported2
66+
for k in kwargs:
67+
if k not in supported:
68+
raise TypeError(f"Parameter '{k}' not supported by filter '{name}'. Supported: {supported}")
69+
70+
self.filter = filter_class(self.image, **kwargs)
3271

72+
def set_picker(self, name, **kwargs):
73+
name = name.upper()
74+
if name not in available_pickers_shortcuts:
75+
raise ValueError(f"Picker '{name}' not recognized. Available: {list(available_pickers_shortcuts.keys())}")
76+
77+
picker_class = available_pickers_shortcuts[name]
78+
supported = getattr(picker_class, 'parameters', [])
79+
for k in kwargs:
80+
if k not in supported:
81+
raise TypeError(f"Parameter '{k}' not supported by picker '{name}'. Supported: {supported}")
82+
83+
self.picker = picker_class(**kwargs)
84+
3385
def set_roi(self, roi_size):
34-
self.roi_size = roi_size
86+
if isinstance(roi_size, (int, float)):
87+
self.roi_size = (roi_size, roi_size)
88+
else:
89+
self.roi_size = roi_size
3590

3691
#### Apply filter
3792
def apply_filter(self):
38-
row_of_interest = self.filter.n_layers//2
39-
if self.filter.parameter is None:
40-
score_image = generic_filter(self.filter.to_filter, self.filter.filter, size=(self.roi_size, self.roi_size, self.filter.n_layers))[..., row_of_interest]
93+
if self.filter.n_layers == 0:
94+
# 2D filter window
95+
size = (self.roi_size[0], self.roi_size[1])
96+
row_of_interest = 0
97+
else:
98+
# 3D filter window
99+
size = (self.roi_size[0], self.roi_size[1], self.filter.n_layers)
100+
if self.filter.filter_parameters is None:
101+
score_image = generic_filter(self.filter.to_filter, self.filter.filter, size=size)
41102
else:
42-
score_image = generic_filter(self.filter.to_filter, self.filter.filter, size=(self.roi_size, self.roi_size, self.filter.n_layers), extra_arguments = (self.filter.parameter, ))[..., row_of_interest]
103+
score_image = generic_filter(self.filter.to_filter, self.filter.filter, size=size, extra_arguments = (self.filter.filter_parameters, ))
104+
105+
# For 3D filters, slice the middle layer:
106+
if self.filter.n_layers > 0:
107+
row_of_interest = self.filter.n_layers // 2
108+
score_image = score_image[..., row_of_interest]
109+
43110
score_image[np.isnan(score_image)] = 0
44111
self.score_image = score_image
45112
return
46113

47-
#### Pick points
114+
#### Pick points
115+
def pick_points(self, score_image = None):
116+
if self.picker is None:
117+
raise RuntimeError("Picker not set. Use set_picker() first.")
118+
if score_image is not None:
119+
self.score_image = score_image
120+
elif self.score_image is None:
121+
self.apply_filter()
122+
points = self.picker.pick(self.score_image)
123+
return points

0 commit comments

Comments
 (0)