Skip to content

Commit 49b803a

Browse files
committed
Code cleanup
1 parent 9651258 commit 49b803a

File tree

1 file changed

+38
-256
lines changed

1 file changed

+38
-256
lines changed

tests/integration/image_checker.py

Lines changed: 38 additions & 256 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,7 @@ def set_up_and_run_image_checker(
9292

9393

9494
def check_images(parameters: Parameters, prefix: str, cv_dict: Dict[str, Any] = {}):
95-
if cv_dict:
96-
test_results = _cv_check_mismatched_images(parameters, prefix, cv_dict)
97-
else:
98-
test_results = _check_mismatched_images(parameters, prefix)
95+
test_results = _check_mismatched_images(parameters, prefix, cv_dict)
9996
diff_subdir = f"{parameters.diff_dir}/{prefix}"
10097
if not os.path.exists(diff_subdir):
10198
os.makedirs(diff_subdir, exist_ok=True)
@@ -141,22 +138,20 @@ def construct_markdown_summary_table(
141138

142139

143140
def _check_mismatched_images(
144-
parameters: Parameters,
145-
prefix: str,
141+
parameters: Parameters, prefix: str, cv_dict: Dict[str, Any]
146142
) -> Results:
147143
missing_images: List[str] = []
148144
mismatched_images: List[str] = []
145+
features: List[np.ndarray] = []
146+
diff_image_paths: List[str] = []
149147

150148
counter = 0
151149
print(f"Opening expected images file {parameters.expected_images_list}")
152150
with open(parameters.expected_images_list) as f:
153151
print(f"Reading expected images file {parameters.expected_images_list}")
154152
for line in f:
155153
image_name = line.strip("./").strip("\n")
156-
proceed = False
157154
if image_name.startswith(prefix):
158-
proceed = True
159-
if proceed:
160155
counter += 1
161156
if counter % 250 == 0:
162157
print("On line #", counter)
@@ -167,14 +162,33 @@ def _check_mismatched_images(
167162
parameters.expected_images_dir, image_name
168163
)
169164

170-
_compare_actual_and_expected(
171-
missing_images,
172-
mismatched_images,
173-
image_name,
174-
path_to_actual_png,
175-
path_to_expected_png,
176-
parameters.diff_dir,
177-
)
165+
if cv_dict:
166+
# Compare a single image's actual & expected, compute diffs
167+
_cv_compare_actual_and_expected(
168+
missing_images,
169+
mismatched_images,
170+
image_name,
171+
path_to_actual_png,
172+
path_to_expected_png,
173+
parameters.diff_dir,
174+
features,
175+
diff_image_paths,
176+
cv_dict,
177+
)
178+
else:
179+
_compare_actual_and_expected(
180+
missing_images,
181+
mismatched_images,
182+
image_name,
183+
path_to_actual_png,
184+
path_to_expected_png,
185+
parameters.diff_dir,
186+
)
187+
188+
if cv_dict:
189+
# Compare all the diffs we found
190+
# Pass in parallel lists: features & diff_image_paths
191+
group_diffs(features, diff_image_paths, parameters.diff_dir, cv_dict)
178192

179193
verbose: bool = False
180194
if verbose:
@@ -388,36 +402,15 @@ def _make_image_diff_grid(diff_subdir, pdf_name="image_diff_grid.pdf", rows_per_
388402

389403
def cv_prototype(try_num: int, cv_dict: Dict[str, Any]):
390404
print("Computer Vision Prototype for image checker")
391-
"""
392-
cd /lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2
393-
mkdir -p cv_prototype/actual_from_20250613/e3sm_diags/
394-
cp -r zppy_weekly_comprehensive_v3_www/test_weekly_20250613/v3.LR.historical_0051/e3sm_diags/atm_monthly_180x360_aave cv_prototype/actual_from_20250613/e3sm_diags/
395-
ls cv_prototype/actual_from_20250613/e3sm_diags/atm_monthly_180x360_aave/model_vs_obs_1987-1988/
396-
# Contains the different sets, good
397-
"""
398405
actual_images_dir: str = (
399406
"/lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2/cv_prototype/actual_from_20250613"
400407
)
401-
"""
402-
cd /lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2
403-
mkdir -p cv_prototype/expected_from_unified/e3sm_diags/
404-
cp -r /lcrc/group/e3sm/public_html/zppy_test_resources_previous/expected_results_for_unified_1.11.1/expected_comprehensive_v3/e3sm_diags/atm_monthly_180x360_aave cv_prototype/expected_from_unified/e3sm_diags/
405-
ls cv_prototype/expected_from_unified/e3sm_diags/atm_monthly_180x360_aave/model_vs_obs_1987-1988/
406-
# Contains the different sets, good
407-
"""
408408
expected_images_dir: str = (
409409
"/lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2/cv_prototype/expected_from_unified"
410410
)
411411
diff_dir: str = (
412412
f"/lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2/cv_prototype/diff_try{try_num}"
413413
)
414-
"""
415-
cd cv_prototype/expected_from_unified/
416-
find . -type f -name '*.png' > ../image_list_expected.txt
417-
cd ..
418-
ls
419-
# actual_from_20250613 expected_from_unified image_list_expected.txt
420-
"""
421414
expected_images_list: str = (
422415
"/lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2/cv_prototype/image_list_expected.txt"
423416
)
@@ -429,107 +422,20 @@ def cv_prototype(try_num: int, cv_dict: Dict[str, Any]):
429422
}
430423
print(f"Removing diff_dir={d['diff_dir']} to produce new results")
431424
if os.path.exists(d["diff_dir"]):
432-
print(f"{d['diff_dir']} exists, increment try_num={try_num+1}")
425+
raise RuntimeError(f"{d['diff_dir']} exists, increment try_num={try_num+1}")
433426
print("Image checking dict:")
434427
for key in d:
435428
print(f"{key}: {d[key]}")
436429
parameters: Parameters = Parameters(d)
437-
# TODO: if/when merging, do the following:
438-
# To use the CV-version of the image checker for actual integration testing,
439-
# we'd simply need to update `set_up_and_run_image_checker` from
440-
# `check_images(parameters, task)` to `check_images(parameters, task, use_cv=cv_dict)`
441430
test_results: Results = check_images(parameters, "e3sm_diags", cv_dict=cv_dict)
442-
# Try | Total | Correct | Missing | Mismatched | Notes
443-
# 1,2: original | 1713 | 1644 | 12 | 57 |
444-
# 7,9: compute_diff_image | 1713 | 1632 | 12 | 69 | +12 mismatched
445-
# 29,30,31,32: clustering | 1713 | 1632 | 12 | 69 |
446431
print(f"Done with try {try_num}")
447432
assert test_results.image_count_total == 1713
448433
assert test_results.image_count_missing == 12
434+
# This 12 more than the 57 using the original image checker.
449435
assert test_results.image_count_mismatched == 69
450436
assert test_results.image_count_correct == 1632
451437

452438

453-
def _cv_check_mismatched_images(
454-
parameters: Parameters, prefix: str, cv_dict: Dict[str, Any]
455-
) -> Results:
456-
missing_images: List[str] = []
457-
mismatched_images: List[str] = []
458-
features: List[np.ndarray] = []
459-
diff_image_paths: List[str] = []
460-
461-
counter = 0
462-
print(f"Opening expected images file {parameters.expected_images_list}")
463-
with open(parameters.expected_images_list) as f:
464-
print(f"Reading expected images file {parameters.expected_images_list}")
465-
# Step 1: Process all pairs and collect diff regions
466-
for line in f:
467-
image_name = line.strip("./").strip("\n")
468-
if image_name.startswith(prefix):
469-
counter += 1
470-
if counter % 250 == 0:
471-
print("On line #", counter)
472-
path_to_actual_png = os.path.join(
473-
parameters.actual_images_dir, image_name
474-
)
475-
path_to_expected_png = os.path.join(
476-
parameters.expected_images_dir, image_name
477-
)
478-
479-
# Compare a single image's actual & expected, compute diffs
480-
_cv_compare_actual_and_expected(
481-
missing_images,
482-
mismatched_images,
483-
image_name,
484-
path_to_actual_png,
485-
path_to_expected_png,
486-
parameters.diff_dir,
487-
features,
488-
diff_image_paths,
489-
cv_dict,
490-
)
491-
492-
# Compare all the diffs we found
493-
# Pass in parallel lists: features & diff_image_paths
494-
group_diffs(features, diff_image_paths, parameters.diff_dir, cv_dict)
495-
496-
verbose: bool = False
497-
if verbose:
498-
if missing_images:
499-
print("Missing images:")
500-
for i in missing_images:
501-
print(i)
502-
if mismatched_images:
503-
print("Mismatched images:")
504-
for i in mismatched_images:
505-
print(i)
506-
507-
# Count summary
508-
print(f"Total: {counter}")
509-
print(f"Number of missing images: {len(missing_images)}")
510-
print(f"Number of mismatched images: {len(mismatched_images)}")
511-
print(
512-
f"Number of correct images: {counter - len(missing_images) - len(mismatched_images)}"
513-
)
514-
test_results = Results(
515-
parameters.diff_dir, prefix, counter, missing_images, mismatched_images
516-
)
517-
518-
# Make diff_dir readable
519-
if os.path.exists(parameters.diff_dir):
520-
# Execute permission for user is needed to remove diff_dir if we're re-running the image checks.
521-
# Execute permission for others is needed to make diff_dir visible on the web server.
522-
# 7 - rwx for user
523-
# 5 - r-x for group, others
524-
_chmod_recursive(parameters.diff_dir, 0o755)
525-
else:
526-
# diff_dir won't exist if all the expected images are missing
527-
# That is, if we're in this case, we expect the following:
528-
assert len(missing_images) == counter
529-
530-
return test_results
531-
532-
533439
def _cv_compare_actual_and_expected(
534440
missing_images: List[str],
535441
mismatched_images: List[str],
@@ -591,11 +497,6 @@ def compute_diff_image(
591497
if len(gray_diff.shape) != 2:
592498
raise ValueError(f"gray_diff.shape={gray_diff.shape} should have 2 dimensions")
593499
mask: np.ndarray
594-
# Before, we filtered based on `if fraction >= 0.0002`
595-
# That is, the fraction of mismatched pixels should be less than 0.02%
596-
# This is a little different.
597-
# Here, we're seeing how high the grayscale value is for each pixel in the diff.
598-
# If it's > gray_diff_threshold, we add it to the mask.
599500
_, mask = cv2.threshold(
600501
gray_diff, cv_dict["gray_diff_threshold"], 255, cv2.THRESH_BINARY
601502
)
@@ -696,6 +597,8 @@ def extract_features(img: np.ndarray, cv_dict: Dict[str, Any]) -> np.ndarray:
696597
def get_sector_slices(img_h, img_w):
697598
# Example: hardcoded values, adjust to your layout!
698599
thirds = np.linspace(0, img_h, 4, dtype=int)
600+
# The plots usually have 5 tickmarks, dividing the world into 6 lat/lon bands.
601+
# Add a segement on each side, and we get an extremely rough estimate of 8 "segments".
699602
segment_h = img_h // 8
700603
segment_w = img_w // 8
701604
top_bound = segment_h
@@ -836,133 +739,12 @@ def copy_to_cluster_subdir(full_path: str, diff_dir: str, clusters_subdir: str)
836739

837740

838741
if __name__ == "__main__":
839-
# Tries 1-2: with current image checker functions
840-
# Tries 3-9: first attempt using cv2, compute_diff_image
841-
# Tries 10-13: detect_features
842-
# Tries 14-24: reworking code to group multiple diff images together
843-
# Tries 25-28: working on clusters
844-
# Tries 29-32: 4 combinations of DBSCAN/KMeans & extract features on diff/diff+actual
845-
# Try 33: cleaned up code, DBSCAN & diff-only
846-
# Try 34: cleaned up code, DBSCAN & diff+actual
847-
# Tries 35-38: new feature detection, clustering algorithms
848-
"""
849-
###########################################################################
850-
To run:
851-
```bash
852-
# Initial setup
853-
cd /home/ac.forsyth2/ez/zppy
854-
lcrc_conda # alias to set up conda
855-
pre-commit run --all-files
856-
git add -A
857-
conda clean --all --y
858-
conda env create -f conda/dev.yml -n zppy-hackathon-20250707-with-cv
859-
conda activate zppy-hackathon-20250707-with-cv
860-
pip install .
861-
862-
# Each run
863-
# Update `try_num` below (the argument to `cv_prototype`)
864-
pre-commit run --all-files
865-
git add -A
866-
python tests/integration/image_checker.py
867-
# Compare missing/mismatched images with the original image checker's results
868-
# Change the number below to the `try_num`
869-
${num} = 0
870-
diff /lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2/cv_prototype/diff_try${num}/e3sm_diags/missing_images.txt /lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2/cv_prototype/diff_try1/e3sm_diags/missing_images.txt
871-
diff /lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2/cv_prototype/diff_try${num}/e3sm_diags/mismatched_images.txt /lcrc/group/e3sm/public_html/diagnostic_output/ac.forsyth2/cv_prototype/diff_try1/e3sm_diags/mismatched_images.txt
872-
```
873-
874-
###########################################################################
875-
Try 29: feature detection -- actual + diffs, clustering algorithm -- DBSCAN
876-
Total of 69 mismatched images. That's 12 more than the original image checker found,
877-
because we're using a new mask-and-threshold method. Those new diffs are:
878-
< lat_lon/Cloud SSM/I/SSMI-TGCLDLWP_OCN-ANN-global.png
879-
< lat_lon/Cloud SSM/I/SSMI-TGCLDLWP_OCN-DJF-global.png
880-
< lat_lon/Cloud SSM/I/SSMI-TGCLDLWP_OCN-JJA-global.png
881-
< lat_lon/OMI-MLS/OMI-MLS-TCO-JJA-60S60N.png
882-
< lat_lon/SST_CL_HadISST/HadISST_CL-SST-ANN-global.png
883-
< lat_lon/SST_CL_HadISST/HadISST_CL-SST-DJF-global.png
884-
< at_lon/SST_CL_HadISST/HadISST_CL-SST-JJA-global.png
885-
< lat_lon/SST_CL_HadISST/HadISST_CL-SST-MAM-global.png
886-
< lat_lon/SST_PD_HadISST/HadISST_PD-SST-ANN-global.png
887-
< lat_lon/SST_PD_HadISST/HadISST_PD-SST-DJF-global.png
888-
< lat_lon/SST_PD_HadISST/HadISST_PD-SST-MAM-global.png
889-
< lat_lon/SST_PI_HadISST/HadISST_PI-SST-ANN-global.png
890-
891-
These 27 diffs all involve the bottom plot AND the metrics
892-
0: 5 polar MERRA2 > MERRA2-U-850-{season}-polar_S, diffs in bottom plot/metrics
893-
1: 7 polar MERRA2 > MERRA2-T-850-{season}-polar_{hemisphere}, diffs in bottom plot/metrics
894-
2: 5 polar MERRA2 > MERRA2-U-850-{season}_polar_{hemisphere}, diffs in bottom plot/metrics
895-
6: 5 lat_lon Cloud_Calpiso > CALIPSOCOSP-CLDLOW_CAL-{season}-global, diffs in bottom plot/metrics
896-
11: 5 lat_lon MERRA2 > MERRA2-OMEGA-850-{season}-global, diffs in bottom plot/metrics
897-
898-
These 7 diffs all involve all 3 plots
899-
3: 3 tropical_subseasonal wavernumber-frequency > PRECT_{}_15N-15S, diffs in all 3 plots
900-
4: 4 tropical_subseasonal wavernumber-frequency > PRECT_norm_{}_15N-15S, diffs in all 3 plots
901-
902-
These 5 diffs all involve the bottom plot only
903-
9: 5 lat_lon MERRA2 > MERRA2-U-850-{season}-global, all barely noticeable diffs in bottom plot
904-
905-
These 25 diffs all involve the bottom plots and/or metrics, but with less similarity
906-
5: 15 lat_lon SST_{}_HadISST > HadISST_{}-SST-{season}-global diff always in bottom plot, sometimes on bottom metrics; some diffs are barely visible, but some are noticeable
907-
7: 3 lat_lon OMI-MLS > OMI-MLS-TCO-{season}-60S60N, 1 barely noticeable diff in bottom plot, 1 diff in bottom plot/metrics, 1 diff in bottom plot only
908-
8: 3 lat_lon Cloud_SSM.I > SSMI-TGCLDLWP_OCN-{season}-global, 2 nearly invisible diffs in bottom plot, 1 nearly invisible diff in bottom metrics
909-
10: 4 lat_lon MERRA2 > MERRA2-T-850-{season}-global, 2 diffs in bottom plot/metrics and 2 just in bottom plot
910-
911-
noise: 5 remaining diffs. 1 lat_lon, 3 polar, 1 qbo
912-
913-
CONCLUSIONS
914-
- Diffs of plots in the same family almost always have the same things wrong with them.
915-
- Our combination of feature detection + clustering algorithm (DBSCAN) is actually able to tell which plot types are which, so that is interesting/good. HOWEVER, we're really more interested in grouping together similar diffs. E.g., clusters 0,1,2,6,11 above all involve diffs in the bottom plot AND metrics. Is there *anything* we can do to get the feature detection/clustering algorithm to merge clusters 0,1,2,6,11 into one cluster?
916-
- The ultimate goal here is to be able to look at just a few representative diffs rather than needing to sort through many many diffs manually (69 diffs is already a lot, but there can be even more).
917-
918-
###########################################################################
919-
Try 30: feature detection -- diffs only, clustering algorithm -- DBSCAN
920-
921-
cluster_0 has all 69 diffs in it.
922-
923-
CONCLUSIONS
924-
- Looking at only the diffs, it doesn't seem "smart" enough to distinguish that the 3-plot square diffs of tropical_subseasonal clearly belong in a different cluster than the small world map diffs of the other plots.
925-
926-
###########################################################################
927-
Try 31: feature detection -- diffs only, clustering algorithm -- KMeans
928-
929-
Trying with n_clusters = 3. Can we get it to make the following 3 clusters: the world map plots, the tropical_subseasonal plots, and the qbo plots?
930-
931-
Clusters:
932-
0: 2 tropical_subseasonal diffs
933-
2: 2 tropical_subseasonal diffs
934-
1: the remaining 65 diffs, including 3 tropical_subseasonal diffs
935-
936-
CONCLUSIONS
937-
- Not good at all. The tropical subeasonal plots are spread out into 3 clusters and everything else is in one of those.
938-
939-
###########################################################################
940-
Try 32: feature detection -- actual + diffs, clustering algorithm -- KMeans
941-
942-
Clusters:
943-
0: 39 diffs in lat_lon, polar, tropical_subseasonal
944-
1: 21 diffs in lat_lon, polar
945-
2: 9 diffs in polar
946-
947-
###########################################################################
948-
Try 38: feature detection -- diffs, clustering algorithm -- AC
949-
4 clusters -- 2 for tropical_subseasonal, 1 for qbo, and 1 for lat_lon/polar
950-
Pretty decent!
951-
952-
###########################################################################
953-
Try 40: feature detection -- sector slice on diffs, clustering algorithm -- AC
954-
3 clusters -- 2 for tropical_subseasonal/qbo, 1 for lat_lon, 1 for lat_lon/polar/tropical_subseasonal
955-
So, not that great
956-
957-
diff_try# subdirectories to keep: 1, 2, 7, 9, 29, 30, 31, 32, 38
958-
TODO: delete remaining try# subdirectories
959-
"""
960742
cv_dict: Dict[str, Any] = {
961743
# Image diff
962744
"gray_diff_threshold": 30, # Out of 255
963745
# Feature extraction
964746
"extract_diff_features_only": True,
965-
"feature_extraction_algorithm": "sector_slice",
747+
"feature_extraction_algorithm": "combined_features",
966748
"simple_hist_bins": 32,
967749
"combined_features_bins": 8,
968750
# Preprocessing
@@ -975,6 +757,6 @@ def copy_to_cluster_subdir(full_path: str, diff_dir: str, clusters_subdir: str)
975757
"eps": 0.5,
976758
"min_samples": 2,
977759
# Clustering > KMeans, AgglomerativeClustering
978-
"n_clusters": 3,
760+
"n_clusters": 4,
979761
}
980-
cv_prototype(41, cv_dict)
762+
cv_prototype(42, cv_dict)

0 commit comments

Comments
 (0)