Skip to content

Commit 37acba0

Browse files
committed
client side active learning updates
1 parent bbacefe commit 37acba0

File tree

2 files changed

+147
-8
lines changed

2 files changed

+147
-8
lines changed

painter/src/main/python/nav.py

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,127 @@ def get_path_list(self, dir_path):
7171
for a in all_files]
7272
return all_paths
7373

74+
75+
def select_highest_entropy_image(self):
76+
"""
77+
Return filename of active image with highest entropy among
78+
active images *after* the current image, or None if unavailable.
79+
"""
80+
active_images_path = os.path.join(self.parent.proj_location, "active_images.txt")
81+
uncertainty_dir = os.path.join(self.parent.proj_location, "uncertainty")
82+
83+
if not (os.path.isfile(active_images_path) and os.path.isdir(uncertainty_dir)):
84+
return None
85+
86+
# Get active images set
87+
with open(active_images_path, "r") as f:
88+
active_images = {line.strip() for line in f if line.strip()}
89+
90+
# Determine current image position
91+
current_fname = os.path.basename(self.image_path)
92+
try:
93+
current_idx = self.all_fnames.index(current_fname)
94+
except ValueError:
95+
return None # safety fallback
96+
97+
# Only consider images after current
98+
future_images = set(self.all_fnames[current_idx+1:])
99+
candidate_images = active_images.intersection(future_images)
100+
101+
# Gather entropy scores for candidates
102+
entropy_scores = {}
103+
for fname in candidate_images:
104+
score_file = os.path.join(uncertainty_dir, fname + ".txt")
105+
if os.path.isfile(score_file):
106+
try:
107+
with open(score_file, "r") as f_score:
108+
entropy = float(f_score.read().strip())
109+
entropy_scores[fname] = entropy
110+
except Exception:
111+
continue
112+
113+
if not entropy_scores:
114+
return None
115+
116+
return max(entropy_scores, key=entropy_scores.get)
117+
118+
119+
def prefill_next_active_image(self):
120+
"""
121+
Check if next even-index image (after current) is active and replace with highest entropy image.
122+
"""
123+
current_idx = self.all_fnames.index(os.path.basename(self.image_path))
124+
next_idx = current_idx + 1
125+
if next_idx >= len(self.all_fnames):
126+
return # no next image
127+
128+
# Check if next image is in active list
129+
active_images_path = os.path.join(self.parent.proj_location, "active_images.txt")
130+
if not os.path.isfile(active_images_path):
131+
return
132+
133+
with open(active_images_path, "r") as f:
134+
active_images = {line.strip() for line in f if line.strip()}
135+
136+
next_image_name = self.all_fnames[next_idx]
137+
if next_image_name not in active_images:
138+
return # next image is not active → no replacement
139+
140+
# Select best future active image
141+
next_entropy_fname = self.select_highest_entropy_image()
142+
if not next_entropy_fname:
143+
return
144+
145+
# Replace only if different
146+
if self.all_fnames[next_idx] != next_entropy_fname:
147+
print(f"Prefill active: replacing {self.all_fnames[next_idx]}{next_entropy_fname}")
148+
self.all_fnames[next_idx] = next_entropy_fname
149+
150+
# Save updated list to project file
151+
import json
152+
project_file = self.parent.proj_file_path
153+
with open(project_file, 'r') as f:
154+
project_data = json.load(f)
155+
project_data['file_names'] = self.all_fnames
156+
with open(project_file, 'w') as f:
157+
json.dump(project_data, f, indent=4)
158+
159+
74160
def show_next_image(self):
75161
self.next_image_button.setEnabled(False)
76162
self.next_image_button.setText('Loading..')
77-
self.next_image_button.setEnabled(False)
78163
QtWidgets.QApplication.processEvents()
164+
165+
# normal next
79166
dir_path, _ = os.path.split(self.image_path)
80167
all_paths = self.get_path_list(dir_path)
81168
cur_idx = all_paths.index(self.image_path)
82-
next_idx = cur_idx + 1
83-
if next_idx >= len(all_paths):
84-
next_idx = 0
85-
self.image_path = all_paths[next_idx]
169+
next_idx = (cur_idx + 1) % len(all_paths)
170+
next_fname = os.path.basename(all_paths[next_idx])
171+
172+
# if next image is active, use highest entropy image instead
173+
next_entropy_fname = None
174+
active_images_path = os.path.join(self.parent.proj_location, "active_images.txt")
175+
if os.path.isfile(active_images_path):
176+
with open(active_images_path, "r") as f:
177+
active_images = {line.strip() for line in f if line.strip()}
178+
if next_fname in active_images:
179+
next_entropy_fname = self.select_highest_entropy_image()
180+
181+
if next_entropy_fname:
182+
next_image_path = os.path.join(dir_path, next_entropy_fname)
183+
else:
184+
next_image_path = all_paths[next_idx]
185+
186+
# update current image
187+
self.image_path = next_image_path
86188
self.file_change.emit(self.image_path)
87189
self.update_nav_label()
88190

191+
# prefill future active slot
192+
self.prefill_next_active_image()
193+
194+
89195
def show_prev_image(self):
90196
dir_path, _ = os.path.split(self.image_path)
91197
all_paths = self.get_path_list(dir_path)

painter/src/main/python/root_painter.py

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@
7474
Image.MAX_IMAGE_PIXELS = None
7575

7676

77+
def generate_active_images_file(project_dir, image_fnames, step=2, skip=6):
78+
active_list = image_fnames[skip::step] # skip first N, then every 2nd image
79+
active_file = os.path.join(project_dir, "active_images.txt")
80+
with open(active_file, "w") as f:
81+
for name in active_list:
82+
f.write(f"{name}\n")
83+
print(f"[RootPainter] Wrote {len(active_list)} active images to {active_file}")
84+
return len(active_list)
85+
86+
87+
7788
class RootPainter(QtWidgets.QMainWindow):
7889

7990
closed = QtCore.pyqtSignal()
@@ -686,12 +697,34 @@ def update_dataset_after_check():
686697
self.nav.update_nav_label()
687698
extend_dataset_btn.triggered.connect(update_dataset_after_check)
688699
extras_menu.addAction(extend_dataset_btn)
689-
690700

691701

692-
702+
generate_active_btn = QtWidgets.QAction(QtGui.QIcon('missing.png'),
703+
'Generate active image list',
704+
self)
705+
def call_generate_active():
706+
# Only works if project is open
707+
if hasattr(self, 'proj_location') and hasattr(self, 'image_fnames'):
708+
active_file = os.path.join(self.proj_location, "active_images.txt")
709+
if os.path.isfile(active_file):
710+
reply = QtWidgets.QMessageBox.question(
711+
self, 'File exists',
712+
"active_images.txt already exists.\nDo you want to overwrite it?",
713+
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
714+
QtWidgets.QMessageBox.No)
715+
if reply == QtWidgets.QMessageBox.No:
716+
return # do nothing
717+
718+
count = generate_active_images_file(self.proj_location, self.image_fnames)
719+
QtWidgets.QMessageBox.information(self, "Active list generated",
720+
f"active_images.txt created with {count} active images.")
721+
else:
722+
QtWidgets.QMessageBox.warning(self, "Error",
723+
"No project open. Open a project first.")
724+
725+
generate_active_btn.triggered.connect(call_generate_active)
726+
extras_menu.addAction(generate_active_btn)
693727

694-
695728

696729
def add_about_menu(self, menu_bar):
697730
about_menu = menu_bar.addMenu('About')

0 commit comments

Comments
 (0)