1414import json
1515import logging
1616import time
17+ import random
1718import matplotlib
1819matplotlib .use ("TkAgg" )
1920import matplotlib .pyplot as plt
@@ -349,9 +350,19 @@ def step(self, target_speed, current_speed, dt=0.05):
349350
350351
351352class WeatherRobustnessSystem :
352- """主系统:自动遍历7种天气,实时显示感知数据,输出鲁棒性评分报告"""
353-
354- def __init__ (self ):
353+ """主系统:自动遍历10种天气(随机顺序),实时显示感知数据,输出鲁棒性评分报告"""
354+
355+ def __init__ (self , num_rounds = 1 , shuffle_weather = True , random_seed = None ):
356+ """
357+ Args:
358+ num_rounds: 测试轮数(默认1轮),多轮时每轮天气顺序随机,结果取平均
359+ shuffle_weather: 是否随机打乱天气顺序
360+ random_seed: 随机种子(固定种子可复现结果)
361+ """
362+ self .num_rounds = num_rounds
363+ self .shuffle_weather = shuffle_weather
364+ if random_seed is not None :
365+ random .seed (random_seed )
355366 self .client = carla .Client (CARLA_HOST , CARLA_PORT )
356367 self .client .set_timeout (CARLA_TIMEOUT )
357368 self .world = self .vehicle = self .camera = self .lidar = None
@@ -367,6 +378,9 @@ def __init__(self):
367378 self ._is_transitioning = False # 是否正在过渡
368379 self ._is_stable = True # 是否处于稳定期
369380 self ._stable_steps = 0 # 稳定期步数计数
381+ # 多轮累积统计
382+ self ._all_rounds_reports = [] # 存储每轮报告
383+ self ._weather_order_history = [] # 记录每轮天气顺序
370384
371385 def connect (self ):
372386 self .world = self .client .load_world (CARLA_MAP )
@@ -698,46 +712,157 @@ def _add_dust_effect(self, frame):
698712 frame = cv2 .GaussianBlur (frame , (7 , 7 ), 0 )
699713 return frame
700714
715+ def _run_single_round (self , weather_names_ordered , round_idx ):
716+ """运行单轮测试(指定天气顺序)"""
717+ logger .info (f"\n { '=' * 50 } \n 第 { round_idx + 1 } /{ self .num_rounds } 轮测试开始\n 天气顺序: { ' -> ' .join (WEATHER_LABELS .get (n , n ) for n in weather_names_ordered )} \n { '=' * 50 } " )
718+ self ._weather_order_history .append (weather_names_ordered )
719+
720+ # 重置评分器(每轮独立)
721+ self .scorer = RobustnessScorer ()
722+
723+ for weather_name in weather_names_ordered :
724+ self .apply_weather (weather_name )
725+ for step in range (STEPS_PER_WEATHER ):
726+ self .run_step (weather_name , step )
727+ # 按Q可提前退出
728+ key = cv2 .waitKey (1 ) & 0xFF
729+ if key == ord ('q' ) or key == ord ('Q' ):
730+ return False # 提前退出
731+ return True # 正常完成
732+
701733 def run (self ):
702734 try :
703735 self .connect ()
704- self .spawn_ego_vehicle ()
705-
706- for weather_name in WEATHER_NAMES :
707- self .apply_weather (weather_name )
708- for step in range (STEPS_PER_WEATHER ):
709- self .run_step (weather_name , step )
710- # 按Q可提前退出
711- key = cv2 .waitKey (1 ) & 0xFF
712- if key == ord ('q' ) or key == ord ('Q' ):
713- break
714- else :
715- continue
716- break
717736
718- report = self .scorer .generate_report ()
719- print ("\n " + "=" * 60 )
720- print (" 鲁棒性测试报告" )
721- print ("=" * 60 )
722- for wname , score in report .items ():
723- if wname == "__overall__" :
724- print (f"\n >>> 总体鲁棒性评分: { score :.1f} /100 <<<" )
737+ for round_idx in range (self .num_rounds ):
738+ # 每轮重新 spawn 车辆(避免上一轮碰撞影响)
739+ if round_idx == 0 :
740+ self .spawn_ego_vehicle ()
741+ else :
742+ # 清理上一轮传感器和车辆
743+ for sensor in [self .camera , self .lidar ]:
744+ if sensor :
745+ sensor .stop ()
746+ sensor .destroy ()
747+ if self .vehicle :
748+ self .vehicle .destroy ()
749+ self .spawn_ego_vehicle ()
750+ # 重置内部状态
751+ self ._camera_image = self ._lidar_data = None
752+ self ._current_weather_params = None
753+ self ._target_weather_params = None
754+ self ._transition_step = 0
755+ self ._is_transitioning = False
756+ self ._is_stable = True
757+ self ._stable_steps = 0
758+
759+ # 生成本轮天气顺序
760+ if self .shuffle_weather :
761+ weather_order = list (WEATHER_NAMES )
762+ random .shuffle (weather_order )
725763 else :
726- print (f"\n [{ WEATHER_LABELS .get (wname , wname )} ] { wname } :" )
727- print (f" 评分={ score ['score' ]:.1f} , 碰撞={ score ['collisions' ]} , 碰撞率={ score ['collision_rate' ]:.3f} " )
728- print (f" 图像质量={ score ['avg_image_quality' ]:.2f} , LiDAR主导比例={ score ['lidar_dominant_ratio' ]:.2f} " )
729- if "detection_rate" in score :
730- print (f" 检测率={ score ['detection_rate' ]:.3f} , 平均检测距离={ score ['avg_detection_dist' ]:.1f} m" )
731- print ("\n " + "=" * 60 )
764+ weather_order = list (WEATHER_NAMES )
765+
766+ completed = self ._run_single_round (weather_order , round_idx )
767+ report = self .scorer .generate_report ()
768+ self ._all_rounds_reports .append (report )
769+
770+ # 打印单轮报告
771+ self ._print_report (report , round_idx )
732772
733- # 生成多天气对比柱状图
734- self ._plot_comparison_chart (report )
773+ if not completed :
774+ logger .info (f"第 { round_idx + 1 } 轮被用户中断" )
775+ break
776+
777+ # 汇总多轮结果
778+ if len (self ._all_rounds_reports ) > 1 :
779+ self ._print_multi_round_summary ()
780+
781+ # 生成图表
782+ if len (self ._all_rounds_reports ) == 1 :
783+ self ._plot_comparison_chart (self ._all_rounds_reports [0 ])
784+ else :
785+ self ._plot_multi_round_chart ()
735786
736787 except KeyboardInterrupt :
737788 logger .info ("用户中断" )
738789 finally :
739790 self .cleanup ()
740791
792+ def _print_report (self , report , round_idx ):
793+ """打印单轮测试报告"""
794+ round_label = f"第{ round_idx + 1 } 轮" if self .num_rounds > 1 else ""
795+ print ("\n " + "=" * 60 )
796+ print (f" 鲁棒性测试报告 { round_label } " )
797+ print ("=" * 60 )
798+ for wname , score in report .items ():
799+ if wname == "__overall__" :
800+ print (f"\n >>> 总体鲁棒性评分: { score :.1f} /100 <<<" )
801+ else :
802+ print (f"\n [{ WEATHER_LABELS .get (wname , wname )} ] { wname } :" )
803+ print (f" 评分={ score ['score' ]:.1f} , 碰撞={ score ['collisions' ]} , 碰撞率={ score ['collision_rate' ]:.3f} " )
804+ print (f" 图像质量={ score ['avg_image_quality' ]:.2f} , LiDAR主导比例={ score ['lidar_dominant_ratio' ]:.2f} " )
805+ if "detection_rate" in score :
806+ print (f" 检测率={ score ['detection_rate' ]:.3f} , 平均检测距离={ score ['avg_detection_dist' ]:.1f} m" )
807+ print ("\n " + "=" * 60 )
808+
809+ def _compute_multi_round_average (self ):
810+ """计算多轮平均值"""
811+ avg = {}
812+ all_overalls = []
813+ # 以天气名称为 key 汇总
814+ weather_accum = {wn : {"scores" : [], "collisions" : [], "collision_rates" : [],
815+ "img_qualities" : [], "lidar_ratios" : [], "detection_rates" : [],
816+ "detection_dists" : []} for wn in WEATHER_NAMES }
817+ for report in self ._all_rounds_reports :
818+ all_overalls .append (report .get ("__overall__" , 0 ))
819+ for wn in WEATHER_NAMES :
820+ if wn in report :
821+ s = report [wn ]
822+ weather_accum [wn ]["scores" ].append (s ["score" ])
823+ weather_accum [wn ]["collisions" ].append (s ["collisions" ])
824+ weather_accum [wn ]["collision_rates" ].append (s ["collision_rate" ])
825+ weather_accum [wn ]["img_qualities" ].append (s ["avg_image_quality" ])
826+ weather_accum [wn ]["lidar_ratios" ].append (s ["lidar_dominant_ratio" ])
827+ weather_accum [wn ]["detection_rates" ].append (s .get ("detection_rate" , 0 ))
828+ weather_accum [wn ]["detection_dists" ].append (s .get ("avg_detection_dist" , 0 ))
829+
830+ for wn in WEATHER_NAMES :
831+ acc = weather_accum [wn ]
832+ avg [wn ] = {
833+ "score" : np .mean (acc ["scores" ]) if acc ["scores" ] else 0 ,
834+ "score_std" : np .std (acc ["scores" ]) if len (acc ["scores" ]) > 1 else 0 ,
835+ "collisions" : np .mean (acc ["collisions" ]) if acc ["collisions" ] else 0 ,
836+ "collision_rate" : np .mean (acc ["collision_rates" ]) if acc ["collision_rates" ] else 0 ,
837+ "avg_image_quality" : np .mean (acc ["img_qualities" ]) if acc ["img_qualities" ] else 0 ,
838+ "lidar_dominant_ratio" : np .mean (acc ["lidar_ratios" ]) if acc ["lidar_ratios" ] else 0 ,
839+ "detection_rate" : np .mean (acc ["detection_rates" ]) if acc ["detection_rates" ] else 0 ,
840+ "avg_detection_dist" : np .mean (acc ["detection_dists" ]) if acc ["detection_dists" ] else 0 ,
841+ }
842+ avg ["__overall__" ] = np .mean (all_overalls ) if all_overalls else 0
843+ avg ["__overall_std__" ] = np .std (all_overalls ) if len (all_overalls ) > 1 else 0
844+ avg ["__num_rounds__" ] = len (self ._all_rounds_reports )
845+ return avg
846+
847+ def _print_multi_round_summary (self ):
848+ """打印多轮汇总报告"""
849+ avg = self ._compute_multi_round_average ()
850+ print ("\n " + "=" * 70 )
851+ print (f" 多轮测试汇总报告(共 { avg ['__num_rounds__' ]} 轮,随机天气顺序)" )
852+ print ("=" * 70 )
853+ print (f"\n 天气顺序历史:" )
854+ for i , order in enumerate (self ._weather_order_history ):
855+ names = [WEATHER_LABELS .get (n , n ) for n in order ]
856+ print (f" 第{ i + 1 } 轮: { ' -> ' .join (names )} " )
857+ print (f"\n >>> 综合鲁棒性评分: { avg ['__overall__' ]:.1f} ± { avg ['__overall_std__' ]:.1f} /100 <<<" )
858+ for wn in WEATHER_NAMES :
859+ s = avg [wn ]
860+ print (f"\n [{ WEATHER_LABELS .get (wn , wn )} ] { wn } :" )
861+ print (f" 评分={ s ['score' ]:.1f} ±{ s ['score_std' ]:.1f} , 碰撞={ s ['collisions' ]:.1f} , 碰撞率={ s ['collision_rate' ]:.3f} " )
862+ print (f" 图像质量={ s ['avg_image_quality' ]:.2f} , LiDAR主导比例={ s ['lidar_dominant_ratio' ]:.2f} " )
863+ print (f" 检测率={ s ['detection_rate' ]:.3f} , 平均检测距离={ s ['avg_detection_dist' ]:.1f} m" )
864+ print ("\n " + "=" * 70 )
865+
741866 def _plot_comparison_chart (self , report ):
742867 """生成多天气对比柱状图"""
743868 weather_names = [n for n in WEATHER_NAMES if n in report ]
@@ -866,5 +991,82 @@ def cleanup(self):
866991 logger .info ("资源已清理" )
867992
868993
994+ def _plot_multi_round_chart (self ):
995+ """生成多轮平均对比柱状图(带误差线)"""
996+ avg = self ._compute_multi_round_average ()
997+ weather_names = [n for n in WEATHER_NAMES if n in avg ]
998+ if not weather_names :
999+ return
1000+
1001+ labels = [WEATHER_LABELS .get (n , n ) for n in weather_names ]
1002+ scores = [avg [n ]["score" ] for n in weather_names ]
1003+ score_stds = [avg [n ]["score_std" ] for n in weather_names ]
1004+ img_qualities = [avg [n ]["avg_image_quality" ] for n in weather_names ]
1005+ detection_rates = [avg [n ]["detection_rate" ] for n in weather_names ]
1006+ overall = avg ["__overall__" ]
1007+ overall_std = avg ["__overall_std__" ]
1008+ num_rounds = avg ["__num_rounds__" ]
1009+
1010+ colors = ["#2ecc71" , "#3498db" , "#f39c12" , "#e74c3c" , "#9b59b6" , "#1abc9c" , "#e67e22" ,
1011+ "#c0392b" , "#8e44ad" , "#d4ac0d" ]
1012+
1013+ fig , axes = plt .subplots (2 , 2 , figsize = (14 , 10 ))
1014+ fig .suptitle (f"CARLA Weather Robustness Comparison ({ num_rounds } Rounds, Random Order)\n "
1015+ f"Overall: { overall :.1f} ± { overall_std :.1f} /100" ,
1016+ fontsize = 14 , fontweight = "bold" )
1017+
1018+ # 1. 鲁棒性评分(带误差线)
1019+ ax = axes [0 , 0 ]
1020+ bars = ax .bar (labels , scores , yerr = score_stds , color = colors [:len (labels )], edgecolor = "white" ,
1021+ capsize = 5 , error_kw = {"linewidth" : 1.5 })
1022+ ax .set_title (f"Robustness Score (avg of { num_rounds } rounds)" )
1023+ ax .set_ylabel ("Score / 100" )
1024+ ax .set_ylim (0 , 100 )
1025+ for bar , v , s in zip (bars , scores , score_stds ):
1026+ ax .text (bar .get_x () + bar .get_width ()/ 2 , bar .get_height () + s + 1 ,
1027+ f"{ v :.0f} " , ha = "center" , fontsize = 9 )
1028+ ax .tick_params (axis = "x" , rotation = 30 )
1029+
1030+ # 2. 图像质量
1031+ ax = axes [0 , 1 ]
1032+ bars = ax .bar (labels , img_qualities , color = colors [:len (labels )], edgecolor = "white" )
1033+ ax .set_title ("Avg Image Quality" )
1034+ ax .set_ylabel ("Score" )
1035+ ax .set_ylim (0 , 1 )
1036+ for bar , v in zip (bars , img_qualities ):
1037+ ax .text (bar .get_x () + bar .get_width ()/ 2 , bar .get_height () + 0.01 , f"{ v :.2f} " , ha = "center" , fontsize = 9 )
1038+ ax .tick_params (axis = "x" , rotation = 30 )
1039+
1040+ # 3. 障碍物检测率
1041+ ax = axes [1 , 0 ]
1042+ bars = ax .bar (labels , detection_rates , color = colors [:len (labels )], edgecolor = "white" )
1043+ ax .set_title ("Obstacle Detection Rate" )
1044+ ax .set_ylabel ("Rate" )
1045+ ax .set_ylim (0 , 1 )
1046+ for bar , v in zip (bars , detection_rates ):
1047+ ax .text (bar .get_x () + bar .get_width ()/ 2 , bar .get_height () + 0.01 ,
1048+ f"{ v * 100 :.0f} %" , ha = "center" , fontsize = 9 )
1049+ ax .tick_params (axis = "x" , rotation = 30 )
1050+
1051+ # 4. 天气顺序历史
1052+ ax = axes [1 , 1 ]
1053+ ax .axis ("off" )
1054+ ax .set_title ("Weather Order History" , fontweight = "bold" )
1055+ history_text = ""
1056+ for i , order in enumerate (self ._weather_order_history ):
1057+ names = [WEATHER_LABELS .get (n , n ) for n in order ]
1058+ history_text += f"Round { i + 1 } :\n "
1059+ for j , n in enumerate (names ):
1060+ history_text += f" { j + 1 } . { n } \n "
1061+ history_text += "\n "
1062+ ax .text (0 , 1 , history_text .strip (), transform = ax .transAxes , fontsize = 9 ,
1063+ verticalalignment = "top" , fontfamily = "monospace" ,
1064+ bbox = dict (boxstyle = "round,pad=0.5" , facecolor = "#f5f5f5" , edgecolor = "#ccc" ))
1065+
1066+ plt .tight_layout ()
1067+ plt .subplots_adjust (top = 0.9 )
1068+ plt .show ()
1069+
1070+
8691071if __name__ == "__main__" :
870- WeatherRobustnessSystem ().run ()
1072+ WeatherRobustnessSystem (num_rounds = 2 , shuffle_weather = True ).run ()
0 commit comments