Skip to content

Commit a21c93d

Browse files
committed
refactor each component; tooltip;
1 parent c634855 commit a21c93d

11 files changed

+1106
-57
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*.JPG
44
*.tar
55
*.png
6+
*.yaml
67
__pycache__
78
build
89
cvops.egg-info

cvops/dialogs/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from .visualize_dialog import VisualizeDialog
2+
from .merge_dialog import MergeDialog
3+
from .split_dialog import SplitDialog
4+
from .update_dialog import UpdateDialog
5+
from .post_update_dialog import PostUpdateDialog
6+
from .s3_update_dialog import S3UpdateDialog
7+
from .remap_categories_dialog import RemapCategoriesDialog

cvops/dialogs/merge_dialog.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import os
2+
from PyQt5.QtWidgets import (
3+
QFileDialog,
4+
QMessageBox,
5+
QDialog,
6+
QVBoxLayout,
7+
QPushButton,
8+
QLabel,
9+
QFileDialog,
10+
QCheckBox,
11+
)
12+
13+
from coco_assistant import COCO_Assistant
14+
from pycocotools.coco import COCO
15+
16+
17+
class MergeDialog(QDialog):
18+
"""
19+
A dialog window for merging multiple COCO datasets into a single dataset.
20+
21+
The dialog facilitates the merging process by allowing users to select directories
22+
for the images and annotations of the datasets they wish to merge. Users can
23+
optionally choose to merge the images as well. This feature is particularly useful
24+
for tasks that involve consolidating datasets from different sources to create a
25+
larger, comprehensive dataset for training or evaluation purposes.
26+
"""
27+
28+
def __init__(self, parent=None):
29+
super().__init__(parent)
30+
self.setWindowTitle("Merge COCO Datasets")
31+
layout = QVBoxLayout()
32+
33+
self.imgDirLabel = QLabel("Image Directory: Not Selected")
34+
layout.addWidget(self.imgDirLabel)
35+
36+
self.annDirLabel = QLabel("Annotation Directory: Not Selected")
37+
layout.addWidget(self.annDirLabel)
38+
39+
imgDirButton = QPushButton("Select Image Directory")
40+
imgDirButton.clicked.connect(self.selectImgDir)
41+
layout.addWidget(imgDirButton)
42+
43+
annDirButton = QPushButton("Select Annotation Directory")
44+
annDirButton.clicked.connect(self.selectAnnDir)
45+
layout.addWidget(annDirButton)
46+
47+
self.mergeImagesCheckBox = QCheckBox("Merge Images")
48+
layout.addWidget(self.mergeImagesCheckBox)
49+
50+
mergeButton = QPushButton("Merge")
51+
mergeButton.clicked.connect(self.merge)
52+
layout.addWidget(mergeButton)
53+
54+
self.setLayout(layout)
55+
56+
def selectImgDir(self):
57+
directory = QFileDialog.getExistingDirectory(self, "Select Image Directory")
58+
if directory:
59+
self.imgDirLabel.setText(f"Image Directory: {directory}")
60+
61+
def selectAnnDir(self):
62+
directory = QFileDialog.getExistingDirectory(
63+
self, "Select Annotation Directory"
64+
)
65+
if directory:
66+
self.annDirLabel.setText(f"Annotation Directory: {directory}")
67+
68+
def merge(self):
69+
img_dir = self.imgDirLabel.text().replace("Image Directory: ", "")
70+
ann_dir = self.annDirLabel.text().replace("Annotation Directory: ", "")
71+
72+
if not os.path.exists(img_dir) or not os.path.exists(ann_dir):
73+
QMessageBox.critical(self, "Error", "Please select valid directories.")
74+
return
75+
76+
cas = COCO_Assistant(img_dir, ann_dir)
77+
cas.merge()
78+
print("update merged.json into right format.")
79+
merged_coco_path = os.path.join(
80+
os.path.dirname(ann_dir), "results", "merged", "annotations", "merged.json"
81+
)
82+
try:
83+
coco_file = COCO(merged_coco_path)
84+
# Iterate over annotation IDs
85+
for ann_id in coco_file.anns:
86+
ann = coco_file.anns[ann_id]
87+
# Check if the 'segmentation' key exists and if it's empty
88+
if "segmentation" in ann and ann["segmentation"] == [[]]:
89+
# Replace empty segmentation with an empty list
90+
ann["segmentation"] = []
91+
92+
# Now coco_file.anns should have empty segmentations replaced with []
93+
QMessageBox.information(self, "Merge", "Datasets merged successfully.")
94+
except TypeError:
95+
QMessageBox.information(self, "Merge", "Datasets merge fail!")

cvops/dialogs/post_update_dialog.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import os
2+
import yaml
3+
import datetime
4+
5+
from cvops.coco_operation import postupdate as coco_postupdate
6+
from cvops.coco_operation import visualize as coco_visualize
7+
from tools.s3_handler import upload_s3_files, load_aws_credentials
8+
9+
from PyQt5.QtCore import Qt
10+
11+
from PyQt5.QtWidgets import (
12+
QFileDialog,
13+
QMessageBox,
14+
QDialog,
15+
QVBoxLayout,
16+
QPushButton,
17+
QLabel,
18+
QFileDialog,
19+
QCheckBox,
20+
QInputDialog,
21+
)
22+
23+
24+
class PostUpdateDialog(QDialog):
25+
"""
26+
A dialog window for performing post-update operations on COCO datasets.
27+
28+
After a dataset has been updated with new annotations and images, certain post-update
29+
steps may be necessary. This dialog offers options for processing the updated dataset,
30+
such as reorganizing files, updating indices, or applying additional transformations.
31+
This is crucial for maintaining dataset integrity and ensuring compatibility with
32+
machine learning pipelines.
33+
"""
34+
35+
def __init__(self, parent=None):
36+
super(PostUpdateDialog, self).__init__(parent)
37+
self.setWindowTitle("Post Update COCO Dataset")
38+
layout = QVBoxLayout()
39+
40+
# Checkbox for using the latest update configuration
41+
self.useLatestConfigCheckbox = QCheckBox("Use Latest Update Configurations")
42+
layout.addWidget(self.useLatestConfigCheckbox)
43+
self.useLatestConfigCheckbox.stateChanged.connect(self.toggleDirSelection)
44+
45+
# Initialization of labels and buttons for directory selection
46+
self.newSamplesDirLabel = QLabel("New Samples Directory: Not Selected")
47+
self.existingSamplesDirLabel = QLabel(
48+
"Existing Samples Directory: Not Selected"
49+
)
50+
self.resultsDirLabel = QLabel("Results Directory: Not Selected")
51+
52+
self.labelsAndButtons = [
53+
(self.newSamplesDirLabel, "Select New Samples Directory"),
54+
(self.existingSamplesDirLabel, "Select Existing Samples Directory"),
55+
(self.resultsDirLabel, "Select Results Directory"),
56+
]
57+
58+
for label, dialogTitle in self.labelsAndButtons:
59+
layout.addWidget(label)
60+
button = QPushButton(dialogTitle)
61+
button.clicked.connect(
62+
lambda _, lbl=label, title=dialogTitle: self.selectDirectory(lbl, title)
63+
)
64+
layout.addWidget(button)
65+
66+
# Button to execute post update
67+
postUpdateButton = QPushButton("Post Update Dataset")
68+
postUpdateButton.clicked.connect(self.postUpdate)
69+
layout.addWidget(postUpdateButton)
70+
71+
self.setLayout(layout)
72+
self.toggleDirSelection(
73+
self.useLatestConfigCheckbox.checkState()
74+
) # Ensure correct initial state
75+
76+
def toggleDirSelection(self, state):
77+
shouldHide = state == Qt.Checked
78+
for label, _ in self.labelsAndButtons:
79+
# This loop adjusts visibility based on the checkbox state
80+
label.setVisible(not shouldHide)
81+
label.nextInFocusChain().setVisible(
82+
not shouldHide
83+
) # Adjusts the visibility of the button
84+
85+
def selectDirectory(self, labelWidget, dialogTitle):
86+
directory = QFileDialog.getExistingDirectory(self, dialogTitle)
87+
if directory:
88+
labelWidget.setText(f"{dialogTitle}: {directory}")
89+
90+
def postUpdate(self):
91+
if self.useLatestConfigCheckbox.isChecked():
92+
try:
93+
with open("latest_update_configs.yaml", "r") as file:
94+
config = yaml.safe_load(file)
95+
96+
# Using full paths from the configuration
97+
existing_samples_dir = os.path.dirname(config.get("train_ann_path", ""))
98+
results_path = os.path.dirname(
99+
os.path.dirname(config.get("outcome_train_ann", ""))
100+
)
101+
102+
# Validate paths are valid directories
103+
if not (
104+
os.path.isdir(existing_samples_dir) and os.path.isdir(results_path)
105+
):
106+
raise ValueError(
107+
"One or more directories from the config don't exist."
108+
)
109+
110+
except FileNotFoundError:
111+
QMessageBox.critical(
112+
self,
113+
"Error",
114+
"Configuration file 'latest_update_configs.yaml' not found.",
115+
)
116+
return
117+
except ValueError as ve:
118+
QMessageBox.critical(self, "Error", str(ve))
119+
return
120+
except Exception as e:
121+
QMessageBox.critical(
122+
self, "Error", f"Failed to read the configuration file: {str(e)}"
123+
)
124+
return
125+
else:
126+
# Extract directly provided paths from the dialog's fields
127+
existing_samples_dir = (
128+
self.existingSamplesDirLabel.text().split(": ")[1].strip()
129+
)
130+
results_path = self.resultsDirLabel.text().split(": ")[1].strip()
131+
132+
# Direct paths validation
133+
if not (
134+
os.path.isdir(existing_samples_dir) and os.path.isdir(results_path)
135+
):
136+
QMessageBox.critical(self, "Error", "Please select valid directories.")
137+
return
138+
139+
try:
140+
train_json, train_img_dir, val_json, val_img_dir = coco_postupdate(
141+
existing_samples_dir=existing_samples_dir, results_path=results_path
142+
)
143+
QMessageBox.information(
144+
self, "Post-update", "Dataset post-update completed successfully."
145+
)
146+
147+
# Ask the user if they want to visualize data after post-update
148+
reply = QMessageBox.question(
149+
self,
150+
"Visualize Data",
151+
"Do you want to visualize the updated dataset?",
152+
QMessageBox.Yes | QMessageBox.No,
153+
QMessageBox.No,
154+
)
155+
156+
if reply == QMessageBox.Yes:
157+
self.visualize(train_img_dir, train_json)
158+
self.visualize(val_img_dir, val_json)
159+
QMessageBox.information(
160+
self, "Visualization", "Visualization completed."
161+
)
162+
else:
163+
QMessageBox.information(self, "Visualization", "Visualization skipped.")
164+
165+
except Exception as e:
166+
QMessageBox.critical(
167+
self, "Error", f"Failed during post-update operation: {str(e)}"
168+
)
169+
170+
# Prompt the user for S3 upload
171+
upload_reply = QMessageBox.question(
172+
self,
173+
"Upload to S3",
174+
"Would you like to upload the results to an AWS S3 bucket?",
175+
QMessageBox.Yes | QMessageBox.No,
176+
QMessageBox.No,
177+
)
178+
179+
if upload_reply == QMessageBox.Yes:
180+
s3_uri, ok = QInputDialog.getText(
181+
self,
182+
"S3 URI",
183+
"Enter the S3 base URI (e.g., s3://hexa-cv-dataset/Fragaria × ananassa/fruit_detection/):",
184+
)
185+
if ok and s3_uri:
186+
try:
187+
# Extract bucket name and path from s3_uri
188+
if not s3_uri.startswith("s3://"):
189+
raise ValueError("Invalid S3 URI. Must start with 's3://'.")
190+
191+
processed_results_path = os.path.dirname(train_json)
192+
# Extract epoch time from the path
193+
epoch_time = processed_results_path.split("/")[-1]
194+
195+
# Convert epoch time to a datetime object
196+
time_obj = datetime.datetime.fromtimestamp(int(epoch_time))
197+
198+
# Format the datetime object into a human-readable string, e.g., "YYYY-MM-DD_HH-MM-SS"
199+
# You can adjust the formatting to your needs
200+
time_str = time_obj.strftime("%Y-%m-%d_%H-%M-%S")
201+
202+
bucket_name, s3_key = s3_uri[5:].split("/", 1)
203+
aws_access_key_id, aws_secret_access_key = load_aws_credentials()
204+
205+
# Assuming results_path contains the path to the results you want to upload
206+
upload_s3_files(
207+
aws_access_key_id,
208+
aws_secret_access_key,
209+
bucket_name,
210+
processed_results_path,
211+
s3_key + f"{time_str}",
212+
)
213+
214+
QMessageBox.information(
215+
self, "S3 Upload", "Results successfully uploaded to S3."
216+
)
217+
except ValueError as ve:
218+
QMessageBox.critical(self, "S3 Upload Error", str(ve))
219+
except Exception as e:
220+
QMessageBox.critical(
221+
self,
222+
"S3 Upload Error",
223+
f"Failed to upload results to S3: {str(e)}",
224+
)
225+
226+
def visualize(self, img_dir, ann_path):
227+
try:
228+
coco_visualize(img_dir, ann_path)
229+
230+
except Exception as e:
231+
QMessageBox.critical(
232+
self, "Error", f"Failed to process the annotations file: {str(e)}"
233+
)

0 commit comments

Comments
 (0)