@@ -55,6 +55,10 @@ def __init__(self, video):
5555 self .brush_deselect_mode = False
5656 self .installEventFilter (self )
5757
58+ self .gradient_direction_points = []
59+ self .gradient_direction = None
60+ self .setting_direction = False
61+
5862 self .selected_points = []
5963 self .manual_points = []
6064 self .candidate_points = []
@@ -185,13 +189,15 @@ def ui_graphics(self):
185189 brush = pg .mkBrush (0 , 255 , 0 , 200 ),
186190 size = 6
187191 )
192+ self .direction_line = pg .PlotDataItem (pen = pg .mkPen ('r' , width = 2 ))
188193
189194 self .view .addItem (self .image_item )
190195 self .view .addItem (self .polygon_line )
191196 self .view .addItem (self .polygon_points_scatter )
192197 self .view .addItem (self .roi_overlay ) # Add scatter for showing square points
193198 self .view .addItem (self .scatter ) # Add scatter for showing points
194199 self .view .addItem (self .candidate_scatter )
200+ self .view .addItem (self .direction_line )
195201
196202 self .splitter .addWidget (self .pg_widget )
197203
@@ -372,6 +378,7 @@ def ui_auto_right_menu(self):
372378 self .auto_method_buttons = {}
373379 method_names = [
374380 "Shi-Tomasi" ,
381+ "Gradient in direction" ,
375382 ]
376383 for i , name in enumerate (method_names ):
377384 button = QtWidgets .QPushButton (name )
@@ -426,19 +433,51 @@ def update_label_and_recompute(val):
426433 self .update_threshold_and_show_shi_tomsi () # Placeholder method
427434 self .threshold_slider .valueChanged .connect (update_label_and_recompute )
428435
436+ # Gradient in a specified direction settings
437+ self .direction_button = QtWidgets .QPushButton ("Set direction on image" )
438+ self .direction_button .setVisible (False )
439+ self .direction_button .setCheckable (True )
440+ self .direction_button .clicked .connect (self .set_gradient_direction_mode )
441+ self .automatic_layout .addWidget (self .direction_button )
442+
443+ self .direction_threshold = 10
444+ self .gradient_thresh_label = QtWidgets .QLabel (f"Threshold (grad): { self .direction_threshold } " )
445+ self .gradient_thresh_label .setVisible (False )
446+ self .automatic_layout .addWidget (self .gradient_thresh_label )
447+
448+ self .gradient_thresh_slider = QtWidgets .QSlider (QtCore .Qt .Orientation .Horizontal )
449+ self .gradient_thresh_slider .setRange (1 , 100 )
450+ self .gradient_thresh_slider .setSingleStep (1 )
451+ self .gradient_thresh_slider .setValue (self .direction_threshold )
452+ self .gradient_thresh_slider .setVisible (False )
453+ self .automatic_layout .addWidget (self .gradient_thresh_slider )
454+
455+ def update_direction_thresh (val ):
456+ self .gradient_thresh_label .setText (f"Threshold (grad): { val } " )
457+ self .update_threshold_and_show_gradient_direction ()
458+ self .gradient_thresh_slider .valueChanged .connect (update_direction_thresh )
459+
429460 self .automatic_layout .addStretch (1 )
430461
431462 def auto_method_selected (self , id : int ):
432463 method_name = list (self .auto_method_buttons .keys ())[id ]
433464 print (f"Selected automatic method: { method_name } " )
434465 # Here you can switch method behavior, show/hide widgets, etc.
435466 is_shi_tomasi = method_name == "Shi-Tomasi"
467+ is_gradient_dir = method_name == "Gradient in direction"
468+
436469 self .threshold_label .setVisible (is_shi_tomasi )
437470 self .threshold_slider .setVisible (is_shi_tomasi )
438471
439472 if is_shi_tomasi :
440473 self .compute_candidate_points_shi_tomasi ()
441474
475+ self .direction_button .setVisible (is_gradient_dir )
476+ self .gradient_thresh_label .setVisible (is_gradient_dir )
477+ self .gradient_thresh_slider .setVisible (is_gradient_dir )
478+ if is_gradient_dir and self .gradient_direction is not None :
479+ self .compute_candidate_points_gradient_direction ()
480+
442481 def method_selected (self , id : int ):
443482 method_name = list (self .method_buttons .keys ())[id ]
444483 print (f"Selected method: { method_name } " )
@@ -487,6 +526,20 @@ def switch_mode(self, mode: str):
487526 # self.candidate_scatter.setVisible(True)
488527
489528 def on_mouse_click (self , event ):
529+ if self .setting_direction :
530+ pos = event .scenePos ()
531+ if self .view .sceneBoundingRect ().contains (pos ):
532+ point = self .view .mapSceneToView (pos )
533+ self .gradient_direction_points .append ((point .x (), point .y ()))
534+ if len (self .gradient_direction_points ) == 2 :
535+ self .compute_direction_vector ()
536+ self .update_direction_line ()
537+ self .setting_direction = False
538+ self .direction_button .setChecked (False )
539+ print (f"Gradient direction set: { self .gradient_direction } " )
540+ self .compute_candidate_points_gradient_direction ()
541+ return
542+
490543 if self .mode == "automatic" :
491544 return
492545
@@ -820,6 +873,7 @@ def handle_remove_point(self, event):
820873 self .update_selected_points ()
821874
822875 # Automatic filtering
876+ # Shi-Tomasi method
823877 def compute_candidate_points_shi_tomasi (self ):
824878 """Compute good feature points using structure tensor analysis (Shi–Tomasi style)."""
825879 from scipy .ndimage import sobel
@@ -914,6 +968,83 @@ def clear_candidates(self):
914968
915969 self .update_selected_points () # Update main display to remove candidates
916970
971+ # Gradient in a specified direction
972+ def set_gradient_direction_mode (self ):
973+ self .setting_direction = True
974+ self .gradient_direction_points = []
975+ self .direction_button .setChecked (True ) # Keep it visually pressed
976+ print ("Click two points to set the gradient direction." )
977+
978+ def compute_direction_vector (self ):
979+ p1 , p2 = self .gradient_direction_points
980+ dx , dy = p2 [0 ] - p1 [0 ], p2 [1 ] - p1 [1 ]
981+ norm = np .sqrt (dx ** 2 + dy ** 2 )
982+ if norm == 0 :
983+ self .gradient_direction = None
984+ else :
985+ self .gradient_direction = (dx / norm , dy / norm )
986+
987+ def compute_candidate_points_gradient_direction (self ):
988+ from scipy .ndimage import sobel
989+
990+ if self .gradient_direction is None :
991+ return
992+
993+ dy , dx = self .gradient_direction
994+ subset_size = self .subset_size_spinbox .value ()
995+ roi_size = subset_size // 2
996+
997+ img = self .image_item .image .astype (np .float32 )
998+ candidates = []
999+
1000+ for row , col in self .selected_points :
1001+ y , x = int (round (row )), int (round (col ))
1002+
1003+ if (y - roi_size < 0 or y + roi_size + 1 > img .shape [0 ] or
1004+ x - roi_size < 0 or x + roi_size + 1 > img .shape [1 ]):
1005+ continue
1006+
1007+ roi = img [y - roi_size : y + roi_size + 1 ,
1008+ x - roi_size : x + roi_size + 1 ]
1009+
1010+ gx = sobel (roi , axis = 1 )
1011+ gy = sobel (roi , axis = 0 )
1012+
1013+ gdir = np .abs (gx * dx ) + np .abs (gy * dy )
1014+ strength = np .sum (np .abs (gdir ))
1015+
1016+ candidates .append ((x + 0.0 , y + 0.0 , strength ))
1017+
1018+ if not candidates :
1019+ self .candidate_points = []
1020+ self .update_candidate_display ()
1021+ return
1022+
1023+ values = np .array ([v [2 ] for v in candidates ])
1024+ self .max_grad_dir = np .max (values )
1025+ self .candidates_grad_dir = candidates
1026+ self .update_threshold_and_show_gradient_direction ()
1027+
1028+ def update_threshold_and_show_gradient_direction (self ):
1029+ threshold_ratio = self .gradient_thresh_slider .value () / 100.0
1030+ threshold = self .max_grad_dir * threshold_ratio
1031+
1032+ self .candidate_points = [
1033+ (round (y )+ 0.5 , round (x )+ 0.5 )
1034+ for (x , y , v ) in self .candidates_grad_dir
1035+ if v > threshold
1036+ ]
1037+ self .update_candidate_display ()
1038+ self .update_candidate_points_count ()
1039+
1040+ def update_direction_line (self ):
1041+ if len (self .gradient_direction_points ) == 2 :
1042+ xs = [p [0 ] for p in self .gradient_direction_points ]
1043+ ys = [p [1 ] for p in self .gradient_direction_points ]
1044+ self .direction_line .setData (xs , ys )
1045+ else :
1046+ self .direction_line .clear ()
1047+
9171048 # Brush
9181049 def handle_brush_start (self , ev ):
9191050 if self .image_item .image is None :
0 commit comments