Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. """Classes for producing videos"""
  2. from abc import ABC
  3. import json
  4. import logging
  5. import os
  6. import subprocess
  7. import tempfile
  8. # for visualisations:
  9. import matplotlib.pyplot as plt
  10. class Producer(ABC):
  11. """Generic producer interface."""
  12. def __init__(self, features):
  13. """All producers should take a list of features as input"""
  14. def produce(self):
  15. """All Producers should produce something!"""
  16. class VideoProducer(Producer):
  17. """Video producer interface."""
  18. class FfmpegVideoProducer(VideoProducer):
  19. """Produce videos using ffmpeg"""
  20. # TODO: consider output filename options
  21. def __init__(self, features):
  22. if not features:
  23. raise ValueError("No features provided")
  24. # TODO: consider if we want to permit empty features (producing no video)
  25. self.features = features
  26. def _ffmpeg_feature_to_clip(self, feature=None, output_filepath=None):
  27. """use ffmpeg to produve a video clip from a feature"""
  28. OVERWRITE = True # TODO: consider making this a config option
  29. if not feature or not feature.interval:
  30. raise ValueError("No feature provided")
  31. if not output_filepath:
  32. raise ValueError("No output filepath provided")
  33. ffmpeg_prefix = ["ffmpeg", "-y"] if OVERWRITE else ["ffmpeg"]
  34. ffmpeg_suffix = ["-r", "60", "-c:v", "libx264", "-crf", "26", "-c:a", "aac", "-preset", "ultrafast"]
  35. # TODO: match framerate of input video
  36. # TODO: adjustable encoding options
  37. seek = ["-ss", str(feature.interval.start)]
  38. duration = ["-t", str(feature.interval.duration)]
  39. ffmpeg_args = ffmpeg_prefix + seek + ["-i"] + [feature.path] + duration + ffmpeg_suffix + [output_filepath]
  40. logging.info(f"ffmpeg_args: {ffmpeg_args}")
  41. subprocess.run(ffmpeg_args, stdout=None, stderr=None)
  42. def _ffmpeg_concat_clips(self, clips=None, output_filepath=None):
  43. """use ffmpeg to concatenate clips into a single video"""
  44. OVERWRITE = True
  45. ffmpeg_prefix = ["ffmpeg"]
  46. ffmpeg_prefix += ["-y"] if OVERWRITE else []
  47. ffmpeg_prefix += ["-f", "concat", "-safe", "0", "-i"]
  48. # there is a method to do this via process substitution, but it's not portable
  49. # so we'll use the input file list method
  50. if not clips:
  51. raise ValueError("No clips provided")
  52. if not output_filepath:
  53. raise ValueError("No output filepath provided")
  54. # generate a temporary file with the list of clips
  55. join_file = tempfile.NamedTemporaryFile(mode="w")
  56. for clip in clips:
  57. join_file.write(f"file '{clip}'\n")
  58. join_file.flush()
  59. ffmpeg_args = ffmpeg_prefix + [join_file.name] + ["-c", "copy", output_filepath]
  60. logging.info(f"ffmpeg_args: {ffmpeg_args}")
  61. subprocess.run(ffmpeg_args, stdout=None, stderr=None)
  62. join_file.close()
  63. def produce(self):
  64. OUTPUT_DIR = "/tmp/" # TODO: make this a config option
  65. clips = []
  66. for num, feature in enumerate(self.features):
  67. output_filepath = f"{OUTPUT_DIR}/highlight_{num}.mp4"
  68. self._ffmpeg_feature_to_clip(feature, output_filepath)
  69. clips.append(output_filepath)
  70. # concatenate the clips
  71. output_filepath = f"{OUTPUT_DIR}/highlights.mp4"
  72. self._ffmpeg_concat_clips(clips, output_filepath)
  73. logging.info(f"Produced video: {output_filepath}")
  74. class VisualisationProducer(Producer):
  75. """Visualisation producer -- illustrate the features we have extracted"""
  76. def __init__(self, features):
  77. if not features:
  78. raise ValueError("No features provided")
  79. self.features = features
  80. def produce(self):
  81. """Produce visualisation"""
  82. # basic idea: use matplotlib to plot:
  83. # - a wide line segment representing the source video[s]
  84. # - shorter line segments representing the features extracted where:
  85. # + width represents duration
  86. # + colour represents feature type
  87. # + position represents time
  88. # - save as image
  89. plotted_source_videos = []
  90. bar_labels = []
  91. fig, ax = plt.subplots()
  92. for feature in self.features:
  93. # plot source video line if not done already
  94. if feature.source not in plotted_source_videos:
  95. # use video duration as width
  96. # ax.plot([0, feature.source.duration()], [0, 0], color='black', linewidth=10)
  97. ax.broken_barh([(0, feature.source.duration())], (0, 5), facecolors='grey')
  98. plotted_source_videos.append(feature.source)
  99. bar_labels.append(os.path.basename(feature.source.path))
  100. # annotate the source video
  101. ax.text(0.25, 0.25, os.path.basename(feature.source.path), ha='left', va='bottom',
  102. fontsize=16)
  103. # plot feature line
  104. # ax.plot([feature.interval.start, feature.interval.end], [1, 1], color='red', linewidth=5)
  105. ax.broken_barh([(feature.interval.start, feature.interval.duration)], (10, 5), facecolors='red')
  106. if feature.feature_extractor not in bar_labels:
  107. bar_labels.append(feature.feature_extractor)
  108. # label bar with feature extractor
  109. ax.text(0, 8, feature.feature_extractor, ha='left', va='bottom',
  110. fontsize=16)
  111. # label the plot's axes
  112. ax.set_xlabel('Time')
  113. # ax.set_yticks([], labels=bar_labels)
  114. ax.set_yticks([])
  115. # ax.tick_params(axis='y', labelrotation=90, ha='right')
  116. # save the plot
  117. plt.savefig("/tmp/visualisation.png")
  118. plt.close()
  119. class JSONProducer(Producer):
  120. """Produce JSON output"""
  121. def __init__(self, features):
  122. if not features:
  123. raise ValueError("No features provided")
  124. self.features = features
  125. def produce(self):
  126. # FIXME: config option for output path
  127. with open("/tmp/features.json", "w") as jsonfile:
  128. jsonfile.write(json.dumps(self.features, cls=PipelineJSONEncoder, indent=4))