seed_medias.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. # -*- coding: utf-8 -*-
  2. """将 seed_assets/medias 中的真实照片导入 static/medias 并同步 media 表。"""
  3. import os
  4. import shutil
  5. import sys
  6. from pathlib import Path
  7. from PIL import Image
  8. ROOT = Path(__file__).resolve().parents[1]
  9. sys.path.insert(0, str(ROOT))
  10. os.environ.setdefault(
  11. 'SQLALCHEMY_DATABASE_URI',
  12. 'mysql+pymysql://root:bridgedisease_root@127.0.0.1:3307/bridge_disease',
  13. )
  14. from app import create_app
  15. from app.models import Media, db
  16. ASSETS_DIR = ROOT / 'seed_assets' / 'medias'
  17. MEDIAS_DIR = ROOT / 'app' / 'static' / 'medias'
  18. MAX_WIDTH = 1600
  19. # 源文件(seed_assets 内) -> 目标与元数据
  20. SEED_ITEMS = [
  21. {
  22. 'source': '01_concrete_crack_bridge.jpg',
  23. 'filename': '01_concrete_crack_bridge.jpg',
  24. 'media_name': '混凝土桥面裂缝_01.jpg',
  25. 'description': '桥梁混凝土底板裂缝(德国 Darmsheim 桥,实景)',
  26. 'owner_id': 1,
  27. },
  28. {
  29. 'source': '02_bridge_concrete_cracks.jpg',
  30. 'filename': '02_bridge_concrete_cracks.jpg',
  31. 'media_name': '桥梁混凝土结构裂缝_02.jpg',
  32. 'description': '桥梁混凝土结构多处裂缝(实景)',
  33. 'owner_id': 2,
  34. },
  35. {
  36. 'source': '03_steel_bridge_corrosion.jpg',
  37. 'filename': '03_steel_bridge_corrosion.jpg',
  38. 'media_name': '钢构件锈蚀_03.jpg',
  39. 'description': '金属构件锈蚀与氧化皮(实景)',
  40. 'owner_id': 2,
  41. },
  42. {
  43. 'source': '04_concrete_bending_cracks.jpg',
  44. 'filename': '04_concrete_bending_cracks.jpg',
  45. 'media_name': '混凝土弯曲裂缝_04.jpg',
  46. 'description': '混凝土试件弯曲裂缝(RILEM 三点弯曲试验实景)',
  47. 'owner_id': 2,
  48. },
  49. {
  50. 'source': '05_bridge_substructure.jpg',
  51. 'filename': '05_bridge_substructure.jpg',
  52. 'media_name': '桥梁下部结构_05.jpg',
  53. 'description': '桥梁混凝土底板与下部结构裂缝(实景)',
  54. 'owner_id': 1,
  55. 'fallback_source': '01_concrete_crack_bridge.jpg',
  56. },
  57. {
  58. 'source': '06_shrinkage_cracks_concrete.jpg',
  59. 'filename': '06_shrinkage_cracks_concrete.jpg',
  60. 'media_name': '混凝土收缩裂缝_06.jpg',
  61. 'description': '钢筋混凝土收缩裂缝(实景)',
  62. 'owner_id': 3,
  63. },
  64. {
  65. 'source': '07_asphalt_crocodile_cracking.jpg',
  66. 'filename': '07_asphalt_crocodile_cracking.jpg',
  67. 'media_name': '桥面铺装龟裂_07.jpg',
  68. 'description': '沥青路面龟裂网状裂缝(实景)',
  69. 'owner_id': 1,
  70. },
  71. {
  72. 'source': '08_concrete_rebar_corrosion.jpg',
  73. 'filename': '08_concrete_rebar_corrosion.jpg',
  74. 'media_name': '混凝土钢筋锈蚀_08.jpg',
  75. 'description': '混凝土结构钢筋锈蚀与表面劣化(实景)',
  76. 'owner_id': 3,
  77. },
  78. {
  79. 'source': '09_steel_beam_site.jpg',
  80. 'filename': '09_steel_beam_site.jpg',
  81. 'media_name': '施工现场钢梁_09.jpg',
  82. 'description': '施工现场钢梁吊装与拼装(实景)',
  83. 'owner_id': 1,
  84. },
  85. ]
  86. LEGACY_NAMES = [
  87. 'main_span_deck_inspection.png',
  88. 'steel_box_girder_u_rib.png',
  89. 'expansion_joint_j03.png',
  90. 'pier_cap_east_view.png',
  91. 'bearing_pad_top_view.png',
  92. 'deck_pavement_drone_01.png',
  93. 'cable_saddle_interior.png',
  94. 'parapet_root_seepage.png',
  95. 'concrete_crack_deck.jpg',
  96. 'steel_corrosion_girder.jpg',
  97. 'expansion_joint_spalling.jpg',
  98. 'steel_beam_construction_site.jpg',
  99. 'bearing_pad_surface.jpg',
  100. 'coating_peel_steel.jpg',
  101. 'concrete_rebar_exposure.jpg',
  102. 'pavement_distress.jpg',
  103. '混凝土桥面裂缝_01.jpg',
  104. '钢箱梁锈蚀_02.jpg',
  105. '伸缩缝剥落_03.jpg',
  106. '施工现场钢梁_04.jpg',
  107. '支座垫石开裂_05.jpg',
  108. '涂层剥落_06.jpg',
  109. '混凝土露筋_07.jpg',
  110. '桥面铺装破损_08.jpg',
  111. ]
  112. def prepare_image(src: Path, dest: Path) -> None:
  113. """压缩并统一为 JPEG(PNG 源文件)。"""
  114. dest.parent.mkdir(parents=True, exist_ok=True)
  115. with Image.open(src) as im:
  116. im = im.convert('RGB')
  117. w, h = im.size
  118. if w > MAX_WIDTH:
  119. nh = int(h * MAX_WIDTH / w)
  120. im = im.resize((MAX_WIDTH, nh), Image.Resampling.LANCZOS)
  121. out = dest
  122. if dest.suffix.lower() == '.png':
  123. out = dest.with_suffix('.jpg')
  124. im.save(out, format='JPEG', quality=88, optimize=True)
  125. if out != dest and dest.exists():
  126. dest.unlink()
  127. return out
  128. def resolve_source(item: dict) -> Path | None:
  129. p = ASSETS_DIR / item['source']
  130. if p.is_file():
  131. return p
  132. fb = item.get('fallback_source')
  133. if fb:
  134. p2 = ASSETS_DIR / fb
  135. if p2.is_file():
  136. print(f" [fallback] {item['source']} -> {fb}")
  137. return p2
  138. return None
  139. def main():
  140. if not ASSETS_DIR.is_dir():
  141. print(f'缺少目录 {ASSETS_DIR},请先运行: python scripts/download_real_medias.py')
  142. sys.exit(1)
  143. MEDIAS_DIR.mkdir(parents=True, exist_ok=True)
  144. app = create_app()
  145. with app.app_context():
  146. Media.query.filter(Media.media_name.in_(LEGACY_NAMES)).delete(synchronize_session=False)
  147. for legacy in LEGACY_NAMES:
  148. p = MEDIAS_DIR / legacy
  149. if p.is_file():
  150. p.unlink()
  151. db.session.commit()
  152. added, updated = 0, 0
  153. for item in SEED_ITEMS:
  154. src = resolve_source(item)
  155. if not src:
  156. print(f'[skip] 缺少源图: {item["source"]}')
  157. continue
  158. dest_name = Path(item['filename'])
  159. if dest_name.suffix.lower() == '.png':
  160. dest_name = dest_name.with_suffix('.jpg')
  161. item = {**item, 'filename': dest_name.name, 'media_name': item['media_name'].replace('.png', '.jpg')}
  162. dest = MEDIAS_DIR / dest_name.name
  163. saved = prepare_image(src, dest)
  164. rel_path = f'static/medias/{saved.name}'.replace('\\', '/')
  165. abs_path = ROOT / 'app' / rel_path.replace('/', os.sep)
  166. with Image.open(abs_path) as im:
  167. w, h = im.size
  168. file_size = os.path.getsize(abs_path) / 1024
  169. file_type = 'jpeg'
  170. existing = Media.query.filter_by(media_name=item['media_name']).first()
  171. if existing:
  172. existing.media_path = rel_path
  173. existing.description = item['description']
  174. existing.file_size = round(file_size, 2)
  175. existing.file_type = file_type
  176. existing.resolution_width = w
  177. existing.resolution_height = h
  178. existing.frame_count = 1
  179. updated += 1
  180. print(f'[update] {item["media_name"]}')
  181. else:
  182. db.session.add(
  183. Media(
  184. media_name=item['media_name'],
  185. media_path=rel_path,
  186. description=item['description'],
  187. file_size=round(file_size, 2),
  188. file_type=file_type,
  189. resolution_width=w,
  190. resolution_height=h,
  191. frame_count=1,
  192. owner_id=item['owner_id'],
  193. )
  194. )
  195. added += 1
  196. print(f'[insert] {item["media_name"]}')
  197. db.session.commit()
  198. print(f'完成:新增 {added},更新 {updated},媒体库共 {Media.query.count()} 条。')
  199. print('照片来源说明见 seed_assets/medias/ATTRIBUTION.md')
  200. if __name__ == '__main__':
  201. main()