seed_detections.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. # -*- coding: utf-8 -*-
  2. """插入 20 条桥梁安全隐患检测记录(需已有 user / model / media 种子数据)。"""
  3. from __future__ import annotations
  4. import os
  5. import random
  6. import shutil
  7. import sys
  8. from datetime import datetime, timedelta
  9. from pathlib import Path
  10. from zoneinfo import ZoneInfo
  11. ROOT = Path(__file__).resolve().parents[1]
  12. sys.path.insert(0, str(ROOT))
  13. os.environ.setdefault(
  14. 'SQLALCHEMY_DATABASE_URI',
  15. 'mysql+pymysql://root:bridgedisease_root@127.0.0.1:3307/bridge_disease?charset=utf8mb4',
  16. )
  17. from app import create_app
  18. from app.constants import DiseaseGrade, TaskStatus
  19. from app.models import Detection, Media, Model, User, db
  20. TZ = ZoneInfo('Asia/Shanghai')
  21. # (模型 ID, 隐患数量, 周长, 面积, 形状复杂度, 纹理粗糙度, 裂缝宽度, 色调, 状态)
  22. # 模型:1锈蚀 2涂层 3剥落露筋 4钢裂缝 5混凝土裂缝 6风化 7坑凼
  23. SEED_ROWS = [
  24. # admin 巡检 7 条
  25. (5, 3, 842.5, 12560.0, 0.41, 128.0, 4.2, 42.0, TaskStatus.COMPLETED),
  26. (5, 2, 610.0, 9800.0, 0.35, 95.0, 3.1, 38.0, TaskStatus.COMPLETED),
  27. (7, 5, 1200.0, 18200.0, 0.52, 210.0, 0.0, 55.0, TaskStatus.COMPLETED),
  28. (1, 4, 920.0, 15400.0, 0.48, 165.0, 0.0, 12.0, TaskStatus.COMPLETED),
  29. (3, 2, 480.0, 6200.0, 0.29, 88.0, 5.8, 28.0, TaskStatus.COMPLETED),
  30. (5, 1, 320.0, 4100.0, 0.22, 62.0, 2.4, 40.0, TaskStatus.COMPLETED),
  31. (6, 3, 750.0, 11200.0, 0.38, 142.0, 0.0, 48.0, TaskStatus.COMPLETED),
  32. # developer 7 条
  33. (5, 4, 1105.0, 16800.0, 0.45, 155.0, 3.8, 36.0, TaskStatus.COMPLETED),
  34. (4, 2, 540.0, 8900.0, 0.33, 102.0, 6.1, 15.0, TaskStatus.COMPLETED),
  35. (2, 3, 680.0, 9400.0, 0.36, 118.0, 0.0, 22.0, TaskStatus.COMPLETED),
  36. (1, 5, 1350.0, 22100.0, 0.58, 198.0, 0.0, 8.0, TaskStatus.COMPLETED),
  37. (7, 6, 1580.0, 24500.0, 0.61, 225.0, 0.0, 60.0, TaskStatus.COMPLETED),
  38. (3, 1, 210.0, 2800.0, 0.18, 45.0, 4.5, 30.0, TaskStatus.COMPLETED),
  39. (5, 2, 590.0, 7600.0, 0.31, 91.0, 2.9, 41.0, TaskStatus.FAILED),
  40. # demo 6 条
  41. (5, 2, 505.0, 7200.0, 0.27, 78.0, 3.2, 39.0, TaskStatus.COMPLETED),
  42. (3, 3, 890.0, 13100.0, 0.42, 136.0, 7.2, 25.0, TaskStatus.COMPLETED),
  43. (1, 4, 1020.0, 14900.0, 0.44, 148.0, 0.0, 10.0, TaskStatus.COMPLETED),
  44. (6, 2, 430.0, 5500.0, 0.24, 58.0, 0.0, 52.0, TaskStatus.COMPLETED),
  45. (4, 1, 180.0, 2200.0, 0.16, 40.0, 5.2, 18.0, TaskStatus.COMPLETED),
  46. (7, 4, 990.0, 14200.0, 0.39, 125.0, 0.0, 58.0, TaskStatus.COMPLETED),
  47. ]
  48. # 按媒体名称关键字匹配默认模型(SEED_ROWS 中 None 时回填)
  49. MEDIA_MODEL_HINTS = [
  50. (('锈蚀', '钢'), 1),
  51. (('涂层', '剥落', '鼓包'), 2),
  52. (('露筋', '剥落'), 3),
  53. (('钢', '施工'), 4),
  54. (('裂缝', '收缩', '弯曲', '下部'), 5),
  55. (('风化',), 6),
  56. (('龟裂', '铺装', '坑'), 7),
  57. ]
  58. DESCRIPTIONS = {
  59. DiseaseGrade.MILD: '隐患程度较轻,建议纳入日常巡检观察。',
  60. DiseaseGrade.MODERATE: '存在中等程度结构隐患,建议安排专项复核与养护。',
  61. DiseaseGrade.SEVERE: '隐患较为明显,应尽快组织检测评估并制定处置方案。',
  62. DiseaseGrade.CRITICAL: '隐患严重,需立即采取限载或封闭措施并启动应急处置。',
  63. }
  64. def pick_grade(score: float) -> DiseaseGrade:
  65. if score >= 0.8:
  66. return DiseaseGrade.CRITICAL
  67. if score >= 0.5:
  68. return DiseaseGrade.SEVERE
  69. if score >= 0.2:
  70. return DiseaseGrade.MODERATE
  71. return DiseaseGrade.MILD
  72. def score_from_metrics(count, area, perimeter, complexity) -> float:
  73. base = min(1.0, (count * 0.08 + area / 50000 + perimeter / 3000 + complexity * 0.3))
  74. return round(max(0.05, min(0.95, base + random.uniform(-0.05, 0.08))), 3)
  75. def resolve_model_id(media: Media, hinted: int | None) -> int:
  76. if hinted is not None:
  77. return hinted
  78. name = media.media_name + (media.description or '')
  79. for keys, mid in MEDIA_MODEL_HINTS:
  80. if any(k in name for k in keys):
  81. return mid
  82. return 5
  83. def ensure_result_image(media: Media, username: str) -> str:
  84. """复制媒体图为检测结果占位图,便于列表预览。"""
  85. rel = os.path.join('static', 'results', username, Path(media.media_path).name)
  86. rel = rel.replace('\\', '/')
  87. if rel.lower().endswith('.jpeg'):
  88. rel = rel[:-5] + '.jpg'
  89. elif not rel.lower().endswith(('.jpg', '.png', '.mp4')):
  90. rel = str(Path(rel).with_suffix('.jpg'))
  91. src = ROOT / 'app' / media.media_path.replace('/', os.sep)
  92. dest = ROOT / 'app' / rel.replace('/', os.sep)
  93. dest.parent.mkdir(parents=True, exist_ok=True)
  94. if src.is_file():
  95. shutil.copy2(src, dest)
  96. return rel
  97. def assign_media_pairs():
  98. """为 20 条记录分配互不重复的 (owner_id, media_id)。"""
  99. medias = Media.query.order_by(Media.media_id.asc()).all()
  100. if len(medias) < 3:
  101. print('媒体库为空或过少,请先运行: python scripts/seed_medias.py')
  102. sys.exit(1)
  103. users = [1, 2, 3]
  104. pairs: list[tuple[int, Media]] = []
  105. for uid in users:
  106. for m in medias:
  107. pairs.append((uid, m))
  108. if len(pairs) >= 20:
  109. return pairs[:20]
  110. return pairs[:20]
  111. def main() -> None:
  112. random.seed(20260401)
  113. app = create_app()
  114. with app.app_context():
  115. existing = Detection.query.count()
  116. if existing >= 20:
  117. print(f'检测记录已有 {existing} 条,跳过(如需重建请先清空 detection 表)。')
  118. return
  119. users = {u.user_id: u for u in User.query.all()}
  120. models = {m.model_id: m for m in Model.query.all()}
  121. media_pairs = assign_media_pairs()
  122. if len(SEED_ROWS) != len(media_pairs):
  123. print('内部配置条数与媒体分配不一致')
  124. sys.exit(1)
  125. base_time = datetime.now(TZ) - timedelta(days=28)
  126. added = 0
  127. for idx, (pair, row) in enumerate(zip(media_pairs, SEED_ROWS)):
  128. owner_id, media = pair
  129. (
  130. model_id,
  131. disease_count,
  132. disease_perimeter,
  133. disease_area,
  134. shape_complexity,
  135. texture_roughness,
  136. crack_width,
  137. avg_hue,
  138. status,
  139. ) = row
  140. owner = users.get(owner_id)
  141. if not owner:
  142. print(f'跳过:用户 {owner_id} 不存在')
  143. continue
  144. if Detection.query.filter_by(owner_id=owner_id, media_id=media.media_id).first():
  145. print(f'[skip] 已存在 owner={owner_id} media={media.media_id}')
  146. continue
  147. mid = model_id if model_id in models else resolve_model_id(media, None)
  148. severity = score_from_metrics(
  149. disease_count, disease_area, disease_perimeter, shape_complexity
  150. )
  151. grade = pick_grade(severity)
  152. model = models[mid]
  153. t = base_time + timedelta(
  154. days=idx % 27,
  155. hours=9 + (idx % 8),
  156. minutes=(idx * 13) % 60,
  157. )
  158. result_path = (
  159. ensure_result_image(media, owner.username) if status == TaskStatus.COMPLETED else None
  160. )
  161. det = Detection(
  162. result_path=result_path,
  163. disease_count=disease_count,
  164. disease_perimeter=round(disease_perimeter, 2),
  165. disease_area=round(disease_area, 2),
  166. shape_complexity=round(shape_complexity, 3),
  167. texture_roughness=round(texture_roughness, 2),
  168. crack_width=round(crack_width, 2),
  169. avg_hue=round(avg_hue, 2),
  170. disease_severity_score=severity,
  171. disease_grade=grade,
  172. disease_description=(
  173. f'【{model.disease_category}】{DESCRIPTIONS[grade]} '
  174. f'检出隐患 {disease_count} 处,覆盖面积约 {int(disease_area)} 像素。'
  175. ),
  176. detection_duration=round(1200 + random.uniform(200, 3500), 2),
  177. avg_frame_detection_duration=round(800 + random.uniform(50, 400), 2),
  178. status=status,
  179. detection_at=t,
  180. updated_at=t + timedelta(minutes=random.randint(1, 45)),
  181. owner_id=owner_id,
  182. model_id=mid,
  183. media_id=media.media_id,
  184. )
  185. db.session.add(det)
  186. added += 1
  187. print(
  188. f'[insert] #{added} {owner.username} | {media.media_name} | '
  189. f'{model.model_name} | {grade.name} | {status.name}'
  190. )
  191. db.session.commit()
  192. print(f'完成:新增 {added} 条检测记录,当前共 {Detection.query.count()} 条。')
  193. if __name__ == '__main__':
  194. main()