@@ -92,10 +92,7 @@ def set_up_and_run_image_checker(
9292
9393
9494def 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
143140def _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
389403def 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-
533439def _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:
696597def 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
838741if __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