def ffmpeg_concat_rel_speed(filenames, filenames_file, output_file, rel_speed, fps): # Auto name FIRST_to_LAST if output_file is None: first_date = str2dt(filenames[0]).date() last_date = str2dt(filenames[-1]).date() output_file = first_date.isoformat() + "_to_" + last_date.isoformat() if rel_speed == 1: cl = "ffmpeg -hide_banner -y -f concat -safe 0 -i " + \ filenames_file + " -c copy " + f"-r {fps} {output_file}" else: # output_file += "_xoutput_file" + "{0:.1f}".format(rel_speed) factor = 1 / rel_speed cl = "ffmpeg -hide_banner -y -f concat -safe 0 -i " + filenames_file + " -preset veryfast -filter:v setpts=" + str( factor) + "*PTS " + f"-r {fps} {output_file}" the_call = cl.split(" ") # use abs path to easily report errors directing user to log log_filename = os.path.abspath(os.path.basename(output_file) + ".ffmpeg") p_log = open(log_filename, "w") p_log.write("called: {}\n".format( ' '.join(the_call))) # ignored? overwriten LOGGER.debug("calling: {}\n".format(' '.join(the_call))) r = subprocess.call(the_call, stdout=p_log, stderr=subprocess.STDOUT) if r == 0: # print(output_file) unlink_safe(log_filename) else: raise RuntimeError( "Failed on ffmpeg concat. Check log:{} Called:'{}' Return:{}". format(log_filename, ' '.join(the_call), r))
def test_rename_console(setup_module): """ Rename three files with only EXIF data """ for f in glob.glob(str(TEST_DATA / "rename" / "*.*", )): p = Path(f) shutil.copyfile(f, p.name) cl = ["rename", "--log-level", "DEBUG", "*.*"] with pytest.raises(SystemExit): image_tools_console(cl) renamed = list(glob.glob("*")) renamed.sort() assert len(renamed) == 3 assert str2dt(renamed[0]) == dt(2019, 3, 30, 17, 40, 48) assert str2dt(renamed[1]) == dt(2019, 3, 30, 18, 4, 6) assert str2dt(renamed[2]) == dt(2020, 12, 25, 19, 22, 28)
def __init__(self, filename, real_start_time, real_interval=None, real_end_time=None, real_date=None): super().__init__(filename) if (real_interval and real_end_time) or (not real_interval and not real_end_time): raise RuntimeError("Use real_interval or real_end_time, not both") self._real_start_time = real_start_time self._real_interval = real_interval self._real_end_time = real_end_time if not real_date: self._real_date = str2dt(Path(filename).stem).date()
def find_matching_files(date_list, video_files): matches = [] for d in date_list: try: # match if filename starts with the date match = next(video for video in video_files if d == str2dt(video).date()) matches.append(match) except BaseException: LOGGER.info("No video for {}".format(d)) # not found return matches
def video_join(src_videos: list, dest_video: str, start_datetime, end_datetime, speed_rel, fps): LOGGER.debug("Searching {} to {}".format(start_datetime.isoformat(), end_datetime.isoformat())) invalid_videos = [] for v in src_videos: if not valid(v) or str2dt(v, throw=False) is None: invalid_videos.append(v) if len(invalid_videos) > 0: LOGGER.warning("Ignoring {} invalid video(s) : {}".format( len(invalid_videos), invalid_videos)) video_files = list(set(src_videos) - set(invalid_videos)) video_files_in_range = [ v for v in video_files if start_datetime.date() <= str2dt(v).date() <= end_datetime.date() ] if len(video_files_in_range) < 1: raise VideoMakerError("No videos found to concat") video_files_in_range.sort( ) # probably redundant as ls returns in alphabetical order # logger.info("concat'ing: {}".format('\n'.join(video_files_in_range))) videos_filename = 'filelist.txt' with open(videos_filename, 'w') as f: f.write("# Auto-generated\n") for video in video_files_in_range: f.write("file '%s'\n" % str(video)) ffmpeg_concat_rel_speed(video_files_in_range, videos_filename, dest_video, speed_rel, fps) unlink_safe(videos_filename)
def read_image_times(self): self.images = [] n_errors = 0 LOGGER.debug(f"Reading dates of {len(self._file_list)} files...") LOGGER.debug( f"start: {self.start} end: {self.end} start_time:{self.start_time} end_time:{self.end_time}" ) if not self._file_list: raise VideoMakerError("No image files found in command-line") for fn in self._file_list: # Using second resolution can lead to *variable* intervals. For instance, if the interval is 4.1s, # the durations with be 4/300 (0.0133) but then each 10 frames 5/300 # It's therefore better to use constant frame rate, or to adjust this function # to millisecond resolution and/or round try: datetime_taken = str2dt(fn) if (self.end_time >= datetime_taken.time() >= self.start_time and self.end >= datetime_taken >= self.start): tlf = TLFile(fn, datetime_taken) if self.validate_images: if tlf.valid(): self.images.append(tlf) else: raise ImageError("Invalid image") else: self.images.append(tlf) except ImageError as exc: n_errors += 1 LOGGER.warning(f"Ignoring {Path(fn).absolute()}: {exc}") except Exception as exc: n_errors += 1 LOGGER.warning( f"Ignoring exception getting datetime of {Path(fn).absolute()}: {exc}" ) LOGGER.info( f"Got images for {len(self.images)}/{len(self._file_list)} files, and {n_errors} failures" ) if n_errors: LOGGER.warning( f"No dates available for {n_errors}/{len(self._file_list)} files. Ignoring them." ) return self.images.sort()
def extract_dated_images(filename, output, start_time=None, end_time=None, interval=None, ocr=False): """ Read a video, check metadata to understand real time and then extract images into dated files """ if start_time: vi = ManualTimes(filename, real_start_time=start_time, real_interval=interval, real_end_time=end_time) else: vi = VideoInfo(filename) the_call = ["ffmpeg", "-hide_banner", "-loglevel", "verbose", "-y"] # -y : overwrite the_call.extend(["-i", filename]) # frame_pts is new and unavailable - use real_timestamps instead of: # the_call.extend(['-frame_pts', 'true']) the_call.extend(['-qscale:v', '2']) # jpeg quality: 2-5 is good : https://stackoverflow.com/questions/10225403/how-can-i-extract-a-good-quality-jpeg-image-from-an-h264-video-file-with-ffmpeg the_call.extend(['%06d.jpg']) run_and_capture(the_call) # throw on fail rx = re.compile(r'\d\d\d\d\d\d\.jpg') # glob can't match this properly image_filenames = [f for f in Path(".").glob("*.jpg") if rx.match(str(f)) is not None] last_ts = vi.real_start try: for f in sorted(image_filenames): if ocr: im = Image.open(f) im_iv = ImageOps.grayscale(im) im_iv = ImageOps.invert(im_iv) im_iv = im_iv.crop((50, im.height - 100, 300, im.height)) im_iv.save("invert.jpg") text = image_to_string(im_iv, config="digits") text = image_to_string(im_iv, lang='eng', config="-c tessedit_char_whitelist=0123456789 -oem 0") ts = str2dt(text, throw=False) or (last_ts + interval) LOGGER.debug(f"file: {f} text:{text} ts:{ts}") raise NotImplementedError("tesseract cannot see digits") else: ts = vi.real_timestamps[int(f.stem) - 1] day_dir = Path(output) / Path(dt2str(ts.date())) day_dir.mkdir(exist_ok=True) new_filename = dt2str(ts) + f.suffix new_path = day_dir / new_filename LOGGER.debug(f"file: {f} ts:{ts} new:{new_path}") shutil.move(str(f), str(new_path)) last_ts = ts except KeyError as exc: KeyError(f"{exc}: cannot find metadata in {filename}?")
def video_join_console(): parser = argparse.ArgumentParser("Combine timelapse videos") parser.add_argument("start", "-s", type=lambda s: parse(s, ignoretz=True), default=dt.min, help="eg. \"2 days ago\", 2000-01-20T16:00:00") parser.add_argument("end", "-e", type=lambda s: parse(s, ignoretz=True), default=dt.max, help="eg. Today, 2000-01-20T16:00:00") parser.add_argument('--log-level', '-ll', default='WARNING', type=lambda s: LOG_LEVELS(s).name, nargs='?', choices=LOG_LEVELS.choices()) parser.add_argument("input_movies", nargs="+", help="All possible movie files to search") parser.add_argument( "--output", help= "Force the output filename, instead of automatically assigned based on dates." ) speed_group = parser.add_mutually_exclusive_group() speed_group.add_argument( "--speed-rel", default=1, help= "Relative speed multipler. e.g. 1 is no change, 2 is twice as fast, 0.5 is twice as slow." ) speed_group.add_argument("--speed-abs", default=None, help="Absolute speed (real time / video time)") args = (parser.parse_args()) try: logging.basicConfig(format=LOG_FORMAT) LOGGER.setLevel(args.log_level) if args.start is None or args.end is None: raise SyntaxError("Couldn't understand dates: {}, {}".format( args.start, args.end)) LOGGER.debug("Searching {} to {}".format(args.start.isoformat(), args.end.isoformat())) video_files = args.input_movies invalid_videos = [] for v in video_files: if not valid(v) or str2dt(v, throw=False) is None: invalid_videos.append(v) if len(invalid_videos) > 0: LOGGER.warning("Ignoring {} invalid videos: {}".format( len(invalid_videos), invalid_videos)) video_files = list(set(video_files) - set(invalid_videos)) video_files_in_range = [ v for v in video_files if args.start.date() <= str2dt(v).date() <= args.end.date() ] if len(video_files_in_range) < 1: LOGGER.debug( f"No videos found. video_files: {','.join(video_files)}") raise RuntimeError("No videos found to concat") video_files_in_range.sort( ) # probably redundant as ls returns in alphabetical order LOGGER.debug("concat'ing: {}".format('\n'.join(video_files_in_range))) videos_filename = 'filelist.txt' with open(videos_filename, 'w') as f: f.write("# Auto-generated by TMV\n") for video in video_files_in_range: f.write("file '%s'\n" % str(video)) if args.speed_abs is not None: raise NotImplementedError # ffmpeg_concat_abs_speed( # videos_filename, args.output, float(args.speed_abs)) else: ffmpeg_concat_rel_speed(video_files_in_range, videos_filename, args.output, float(args.speed_rel), 25) unlink_safe(videos_filename) sys.exit(0) except Exception as exc: print(exc, file=sys.stderr) LOGGER.error(exc) LOGGER.debug(f"Exception: {exc}", exc_info=exc) sys.exit(1)
def real_start(self) -> dt: description = self.info_dict['format']['tags']['description'] # couple with videod.py! dt_str, _ = description.split(",") return str2dt(dt_str)