@@ -32,6 +32,14 @@ class AdjustGuiConfig(scfg.DataConfig):
3232 Helper to find good robust normalization parameters for input images.
3333 """
3434 img_fpath = scfg .Value (None , help = 'input' , position = 1 )
35+ scaling = 'sigmoid'
36+ extrema = 'quantile'
37+ low = 0.1
38+ mid = 0.5
39+ high = 0.9
40+ crop = scfg .Value ("null" , type = str , help = 'A crop string e.g. y1:y2, x1:x2' )
41+ expr = scfg .Value ("null" , type = str )
42+ cmap = scfg .Value ('None' , type = str )
3543
3644
3745def report_thread_error (fn ):
@@ -179,7 +187,7 @@ def qt_is_editable(self):
179187
180188 def qt_set_value (self , data ):
181189 # TODO: casting
182- data = smartcast_mod .smartcast (data )
190+ data = smartcast_mod .smartcast (data , allow_split = False )
183191 self .value = data
184192
185193 def qt_delegate_style (self ):
@@ -224,11 +232,18 @@ class QConfigNode(ub.NiceRepr, _Qt_ConfigNodeMixin):
224232 >>> 'opt1': 1,
225233 >>> 'opt2': 2,
226234 >>> },
227- >>> 'general_opt': 'abc',
235+ >>> 'general_opt1': 'abc',
236+ >>> 'general_opt2': 'abc,efg',
228237 >>> }
229238 >>> self = QConfigNode.from_indexable(config)
230239 >>> print('self = {}'.format(ub.urepr(self, nl=1)))
231- >>> print(self.to_indexable())
240+ >>> config = self.to_indexable()
241+ >>> print(f'config = {ub.urepr(config, nl=1)}')
242+ >>> self.children['general_opt2'].qt_set_value('fds,fds')
243+ >>> assert self.children['general_opt2'].value == 'fds,fds'
244+ >>> config = self.to_indexable()
245+ >>> print(f'config = {ub.urepr(config, nl=1)}')
246+ >>> assert config['general_opt2'] == 'fds,fds'
232247 """
233248
234249 def __init__ (self , value = NoValue , type = None , parent = None , choices = None ,
@@ -538,7 +553,14 @@ class QConfigModel(QtCore.QAbstractItemModel):
538553 >>> pindex = root_config.children['algo1'].qt_get_persistant_index(self)
539554 >>> index = QtCore.QModelIndex(pindex)
540555 >>> self.setData(index, 'foo')
541- >>> self.data(index)
556+ >>> got_value = self.data(index) # why does this not return foo?
557+ >>> config = self.root_config.to_indexable()
558+ >>> print(f'config = {ub.urepr(config, nl=1)}')
559+ >>> assert config['algo1'] == 'foo'
560+ >>> self.setData(index, 'foo,bar')
561+ >>> config = self.root_config.to_indexable()
562+ >>> print(f'config = {ub.urepr(config, nl=1)}')
563+ >>> assert config['algo1'] == 'foo,bar'
542564 """
543565 @report_thread_error
544566 def __init__ (self , root_config , parent = None ):
@@ -911,6 +933,7 @@ class AdjustWidget(QtWidgets.QWidget):
911933 def __init__ (self , config = None , raw_img = None ):
912934 super ().__init__ ()
913935 self .raw_img = raw_img
936+ self .processed_img = raw_img
914937 self .config = QConfigNode .coerce (config )
915938
916939 main_layout = QtWidgets .QVBoxLayout (self )
@@ -941,10 +964,38 @@ def update_normalization(self, key=None):
941964 from geowatch .cli .coco_spectra import _weighted_auto_bins
942965 import pandas as pd
943966
944- params = self .config .to_indexable ()
945967 print ('Update Norm' )
968+ params = self .config .to_indexable ()
946969 print ('params = {}' .format (ub .urepr (params , nl = 1 )))
947- norm_img , norm_info = kwarray .robust_normalize (self .raw_img , params = params , return_info = True )
970+
971+ norm_param_names = {
972+ 'scaling' , 'extrema' , 'low' , 'mid' , 'high'
973+ }
974+
975+ cropstr = params .get ('crop' , 'null' )
976+ sl = parse_cropstr (cropstr )
977+ if sl is not None :
978+ self .processed_img = self .raw_img [sl ]
979+ else :
980+ self .processed_img = self .raw_img
981+
982+ expr = params .get ('expr' , 'img' )
983+ if expr == 'null' or not expr :
984+ expr = None
985+ if expr is not None :
986+ # Hack: not safe
987+ img = self .processed_img
988+ ns = globals () | locals ()
989+ self .processed_img = eval (expr , ns )
990+ # self.processed_img = ns['img']
991+
992+ normalizer_params = ub .udict (params ) & norm_param_names
993+ norm_img , norm_info = kwarray .robust_normalize (self .processed_img , params = normalizer_params , return_info = True )
994+
995+ processed_stats = kwarray .stats_dict (self .processed_img )
996+ norm_stats = kwarray .stats_dict (norm_img )
997+ print (f'processed_stats = { ub .urepr (processed_stats , nl = 1 )} ' )
998+ print (f'norm_stats = { ub .urepr (norm_stats , nl = 1 )} ' )
948999
9491000 self .norm_img = norm_img
9501001 self .norm_info = norm_info
@@ -954,13 +1005,23 @@ def update_normalization(self, key=None):
9541005 fig = self .mpl_widget .fig
9551006
9561007 fig .clf ()
957- ax1 = fig .add_subplot (1 , 2 , 1 )
958- ax2 = fig .add_subplot (1 , 2 , 2 )
959-
960- ax1 .imshow (self .norm_img )
1008+ nSubplots = 2
1009+ SHOW_NORM_HIST = True
1010+ if SHOW_NORM_HIST :
1011+ nSubplots += 1
1012+ ax1 = fig .add_subplot (1 , nSubplots , 1 )
1013+ ax2 = fig .add_subplot (1 , nSubplots , 2 )
1014+ if SHOW_NORM_HIST :
1015+ ax3 = fig .add_subplot (1 , nSubplots , 3 )
1016+
1017+ imshow_params = {}
1018+ cmap = params .get ('cmap' , None )
1019+ if cmap is not None and cmap .lower () not in {'null' , 'none' }:
1020+ imshow_params ['cmap' ] = cmap
1021+ ax1 .imshow (self .norm_img , ** imshow_params )
9611022 ax1 .grid (False )
9621023
963- counts , bins = np .histogram (self .raw_img , bins = 256 )
1024+ counts , bins = np .histogram (self .processed_img , bins = 256 )
9641025 centers = (bins [1 :] + bins [0 :- 1 ]) / 2
9651026 data = pd .DataFrame ({'value' : centers , 'weight' : counts })
9661027 n_equal_bins = _weighted_auto_bins (data , 'value' , 'weight' )
@@ -994,6 +1055,21 @@ def update_normalization(self, key=None):
9941055 ax2 .plot ([mid_val , mid_val ], [ymin , ymax ], '-' , color = 'blue' )
9951056 ax2 .plot ([max_val , max_val ], [ymin , ymax ], '-' , color = 'blue' )
9961057
1058+ if SHOW_NORM_HIST :
1059+ counts , bins = np .histogram (self .norm_img , bins = 256 )
1060+ centers = (bins [1 :] + bins [0 :- 1 ]) / 2
1061+ data = pd .DataFrame ({'value' : centers , 'weight' : counts })
1062+ n_equal_bins = _weighted_auto_bins (data , 'value' , 'weight' )
1063+ hist_data_kw = dict (
1064+ x = 'value' ,
1065+ weights = 'weight' ,
1066+ bins = n_equal_bins ,
1067+ )
1068+ hist_style_kw = dict (
1069+ )
1070+ hist_data_kw_ = hist_data_kw .copy ()
1071+ sns .histplot (ax = ax3 , data = data , ** hist_data_kw_ , ** hist_style_kw )
1072+
9971073 fig .canvas .draw ()
9981074
9991075 def on_mpl_widget_click (self , event ):
@@ -1007,7 +1083,7 @@ def on_mpl_widget_click(self, event):
10071083 keys = ['low' , 'mid' , 'high' ]
10081084 qarr = [config .children [k ].value for k in keys ]
10091085
1010- raw_data = self .raw_img .ravel ().copy ()
1086+ raw_data = self .processed_img .ravel ().copy ()
10111087 raw_data .sort ()
10121088
10131089 clicked_percentile = stats .percentileofscore (raw_data , event .xdata )
@@ -1025,6 +1101,56 @@ def on_mpl_widget_click(self, event):
10251101 model .setData (index , clicked_quantile )
10261102
10271103
1104+ def parse_cropstr (cropstr , error_policy = 'return-none' ):
1105+ """
1106+ Parse a string of the form R1:R2,C1:C2 into a tuple of slices using regex.
1107+
1108+ Args:
1109+ cropstr: A string specifying row and column ranges in format "R1:R2,C1:C2"
1110+ (empty values mean unbounded, e.g. "10:, :20" means from 10 to end,
1111+ and from beginning to 20)
1112+
1113+ Returns:
1114+ A tuple of slice objects (row_slice, col_slice)
1115+
1116+ Example:
1117+ >>> # xdoctest: +REQUIRES(module:PyQt5)
1118+ >>> parse_cropstr("null")
1119+ None
1120+ >>> parse_cropstr("")
1121+ (slice(None, None, None), slice(None, None, None))
1122+ >>> parse_cropstr("10:20,30:40")
1123+ (slice(10, 20), slice(30, 40))
1124+ >>> parse_cropstr(":20,30:")
1125+ (slice(None, 20), slice(30, None))
1126+ """
1127+ import re
1128+ if not cropstr .strip ():
1129+ return (slice (None ), slice (None ))
1130+
1131+ # Regex pattern to match either:
1132+ # 1. empty string (treated as full slice)
1133+ # 2. a single number (e.g. "5" becomes 5:6)
1134+ # 3. a range with start:end (either can be empty)
1135+ pattern = r'^\s*((?P<r1>\d*):(?P<r2>\d*))\s*,\s*((?P<c1>\d*):(?P<c2>\d*))\s*$'
1136+
1137+ match = re .fullmatch (pattern , cropstr )
1138+ if not match :
1139+ if error_policy == 'return-none' :
1140+ return None
1141+ raise ValueError (f"Invalid crop string format: '{ cropstr } '. Expected 'R1:R2,C1:C2'" )
1142+
1143+ def to_int_or_none (s ):
1144+ return int (s ) if s else None
1145+
1146+ r1 = to_int_or_none (match .group ('r1' ))
1147+ r2 = to_int_or_none (match .group ('r2' ))
1148+ c1 = to_int_or_none (match .group ('c1' ))
1149+ c2 = to_int_or_none (match .group ('c2' ))
1150+
1151+ return (slice (r1 , r2 ), slice (c1 , c2 ))
1152+
1153+
10281154def main (cmdline = 1 , ** kwargs ):
10291155 config = AdjustGuiConfig .cli (cmdline = cmdline , data = kwargs , strict = True )
10301156 print ('config = ' + ub .urepr (dict (config ), nl = 1 ))
@@ -1046,11 +1172,14 @@ def main(cmdline=1, **kwargs):
10461172 raw_img = kwimage .grab_test_image ()
10471173
10481174 config = {
1049- 'scaling' : QConfigNode ('sigmoid' , choices = ['sigmoid' , 'linear' ]),
1050- 'extrema' : QConfigNode ('quantile' , choices = ['quantile' , 'adaptive-quantile' , 'iqr' , 'iqr-clip' ]),
1051- 'low' : QConfigNode (0.1 , min_value = 0.0 , max_value = 1.0 , step_value = 0.01 ),
1052- 'mid' : QConfigNode (0.5 , min_value = 0.0 , max_value = 1.0 , step_value = 0.01 ),
1053- 'high' : QConfigNode (0.9 , min_value = 0.0 , max_value = 1.0 , step_value = 0.01 ),
1175+ 'scaling' : QConfigNode (config .scaling , choices = ['sigmoid' , 'linear' ]),
1176+ 'extrema' : QConfigNode (config .extrema , choices = ['quantile' , 'adaptive-quantile' , 'iqr' , 'iqr-clip' ]),
1177+ 'low' : QConfigNode (config .low , min_value = 0.0 , max_value = 1.0 , step_value = 0.01 ),
1178+ 'mid' : QConfigNode (config .mid , min_value = 0.0 , max_value = 1.0 , step_value = 0.01 ),
1179+ 'high' : QConfigNode (config .high , min_value = 0.0 , max_value = 1.0 , step_value = 0.01 ),
1180+ 'crop' : QConfigNode (config .crop ),
1181+ 'expr' : QConfigNode (config .expr ),
1182+ 'cmap' : QConfigNode (config .cmap ),
10541183 }
10551184
10561185 widget = AdjustWidget (config , raw_img )
0 commit comments