GARMINの.fitデータを動画に変換するツール rev1
2021/08/22 categories:TOOL| tags:Python|fitdecode|Open CV|GARMIN|
Pythonのfitdecodeを使ってGARMINの.fitデータを動画変換するツールを作成しました。
下記の記事で作成したプログラムにGUIを追加して、動画作成ツールとして使いやすくしてみました。
ファイルの読み込み
File openボタンをクリックしてファイルを選択すると.fitデータを読み込みます。読み込みが完了すると右側のビューにプレビュー画像が表示されます。
表示の変更
左側の数値を変更することで、表示する文字の位置やフォントなどを変更することが出来ます。
動画変換
Output videoボタンをクリックすると動画を作成して保存されます。変換時間はFPSと.fitのデータ量によって変化します。4時間記録した.fitデータを1FPSの動画として変換した場合、変換時間は15分程度でした。
変換した動画
変換した動画は以下の通りです。.fitデータは数秒間隔で記録されているため、以下の動画のように30FPSで変換した場合は、.fitデータを動画のFPSに合わせて線形補完しているため、FPSを高くした場合は変換時間が伸びます。FPSは1FPSで十分かと思います。
ソースコード
# -*- coding: utf-8 -*-
import cv2
import datetime
import fitdecode
import json
import sys
import numpy as np
from PyQt5 import QtWidgets, QtCore, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.resize(1750, 800)
self.video_start_time = 0
json_open = open('settings.json', mode='r',encoding='utf-8')
self.settings = json.load(json_open)
self.model = QtGui.QStandardItemModel()
self.video_output = VideoOutput(self)
self.button_file_open = QtWidgets.QPushButton('File open')
self.button_output_video = QtWidgets.QPushButton('Output Video')
self.progress_bar = QtWidgets.QProgressBar()
self.graphics_view = QtWidgets.QGraphicsView()
self.resolution_width = SpinBox('Output width', 0, 9999, 1280)
self.resolution_height = SpinBox('Output height', 0, 9999, 720)
self.fps = SpinBox('FPS', 0, 999, 1)
self.font_scale_title = DoubleSpinBox('Font scale title', 0, 99, 1.3)
self.font_scale_value = DoubleSpinBox('Font scale value', 0, 99, 1.8)
self.font_thickness_inner = SpinBox('Font thickness inner', 0, 99, 2)
self.font_thickness_outer = SpinBox('Font thickness outer', 0, 99, 6)
self.title_offset = SpinBox('Title offset', -999, 999, -65)
self.unit_offset = SpinBox('Unit offset', -999, 999, 200)
self.table_view = QtWidgets.QTableView()
widget0 = QtWidgets.QWidget()
widget0.setLayout( QtWidgets.QVBoxLayout() )
widget0.layout().addWidget(self.button_file_open)
widget0.layout().addWidget(self.button_output_video)
widget0.layout().addWidget(self.resolution_width)
widget0.layout().addWidget(self.resolution_height)
widget0.layout().addWidget(self.fps)
widget0.layout().addWidget(self.font_scale_title)
widget0.layout().addWidget(self.font_scale_value)
widget0.layout().addWidget(self.font_thickness_inner)
widget0.layout().addWidget(self.font_thickness_outer)
widget0.layout().addWidget(self.title_offset)
widget0.layout().addWidget(self.unit_offset)
widget0.layout().addWidget(self.table_view)
widget0.setMaximumWidth(400)
widget0.setContentsMargins(2,2,2,2)
self.setCentralWidget( QtWidgets.QWidget() )
self.centralWidget().setLayout( QtWidgets.QHBoxLayout() )
self.centralWidget().layout().addWidget(widget0)
self.centralWidget().layout().addWidget(self.graphics_view)
self.centralWidget().setContentsMargins(2,2,2,2)
self.statusBar().addPermanentWidget(self.progress_bar)
self.statusBar().showMessage('Hello!')
self.table_view.setModel(self.model)
self.table_view.horizontalHeader().setStretchLastSection(True)
self.model.setVerticalHeaderLabels([ d[0] for d in self.settings['datas'][1:] ])
self.model.setHorizontalHeaderLabels( self.settings['datas'][0][1:] )
for row in range( self.model.rowCount() ):
item = QtGui.QStandardItem()
item.setCheckable(True)
self.model.setItem(row, 0, item)
for row, row_data in enumerate(self.settings['datas'][1:]):
for column, data in enumerate(row_data[1:]):
index = self.model.index(row, column)
if column == 0:
item = self.model.itemFromIndex(index)
item.setCheckState(data)
else:
self.model.setData(index, data)
self.table_view.setColumnWidth(0, 50)
self.table_view.setColumnWidth(1, 50)
self.table_view.setColumnWidth(2, 50)
self.table_view.setColumnWidth(3, 50)
self.button_file_open.clicked.connect(self.file_open)
self.button_output_video.clicked.connect(self.output_video)
self.video_output.nextSignal.connect(self.video_output_next)
self.model.dataChanged.connect(self.model_changed)
self.resolution_height.spin_box.valueChanged.connect( lambda : self.model_changed(0) )
self.resolution_width.spin_box.valueChanged.connect( lambda : self.model_changed(0) )
self.fps.spin_box.valueChanged.connect( lambda : self.model_changed(0) )
self.font_scale_title.spin_box.valueChanged.connect( lambda : self.model_changed(0) )
self.font_scale_value.spin_box.valueChanged.connect( lambda : self.model_changed(0) )
self.font_thickness_inner.spin_box.valueChanged.connect( lambda : self.model_changed(0) )
self.font_thickness_outer.spin_box.valueChanged.connect( lambda : self.model_changed(0) )
self.title_offset.spin_box.valueChanged.connect( lambda : self.model_changed(0) )
self.unit_offset.spin_box.valueChanged.connect( lambda : self.model_changed(0) )
self.video_output.finishSignal.connect( lambda : self.statusBar().showMessage( self.statusBar().currentMessage() + ' Finished') )
def model_changed(self, i):
try:
data = {}
for row in range( self.model.rowCount() ):
d = {}
for column in range( self.model.columnCount() ):
index = self.model.index(row, column)
item = self.model.itemFromIndex(index)
horizontal_header = self.model.horizontalHeaderItem(column).text()
if horizontal_header == 'X' or horizontal_header == 'Y':
d[horizontal_header] = int(item.text())
else:
d[horizontal_header] = item.text()
vertical_header = self.model.verticalHeaderItem(row).text()
data[vertical_header] = d
self.video_output.datas = data
self.video_output.setting['width'] = self.resolution_width.spin_box.value()
self.video_output.setting['height'] = self.resolution_height.spin_box.value()
self.video_output.setting['fps'] = self.fps.spin_box.value()
self.video_output.setting['font_scale_title'] = self.font_scale_title.spin_box.value()
self.video_output.setting['font_scale_value'] = self.font_scale_value.spin_box.value()
self.video_output.setting['font_thickness_inner'] = self.font_thickness_inner.spin_box.value()
self.video_output.setting['font_thickness_outer'] = self.font_thickness_outer.spin_box.value()
self.video_output.setting['title_offset'] = self.title_offset.spin_box.value()
self.video_output.setting['unit_offset'] = self.unit_offset.spin_box.value()
record = self.video_output.records[0]
frame = self.video_output.image(record)
rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB).data
image = QtGui.QImage(rgb, self.video_output.setting['width'], self.video_output.setting['height'], self.video_output.setting['width']*3, QtGui.QImage.Format_RGB888)
pixmap = QtGui.QPixmap.fromImage(image)
self.graphics_view.scene().clear()
self.graphics_view.scene().addPixmap(pixmap)
self.statusBar().showMessage('Updated')
except:
self.statusBar().showMessage('Error')
def closeEvent(self, event):
if self.video_output.isRunning():
self.video_output.terminate()
self.video_output.video.release()
def file_open(self):
filename, filter = QtWidgets.QFileDialog.getOpenFileName(None, 'Open file', '', 'GARMIN FIT Data(*.fit)')
if filename == '':
return
records = []
with fitdecode.FitReader(filename, processor=fitdecode.StandardUnitsDataProcessor()) as fit:
for frame in fit:
if not isinstance(frame, fitdecode.FitDataMessage):
continue
if not frame.name == 'record':
continue
records.append(frame)
count = len(records) - 1
self.progress_bar.setValue(0)
self.progress_bar.setMaximum(count)
self.statusBar().showMessage( 'output : 0/' + str(count) )
self.video_output.records = records
self.video_output.start_time = records[0].get_field('timestamp').value.timestamp()
self.graphics_view.setScene(
QtWidgets.QGraphicsScene(0, 0, self.video_output.setting['width'], self.video_output.setting['height'], self.graphics_view)
)
self.model_changed(0)
def output_video(self):
filename, filter = QtWidgets.QFileDialog.getSaveFileName(None, 'Save file', '', 'Video(*.mp4)')
if filename == '':
return
self.video_output.filename = filename
self.video_output.start()
self.video_start_time = datetime.datetime.now()
def video_output_next(self):
count = self.progress_bar.value() + 1
self.progress_bar.setValue(count)
elapsed_time = (datetime.datetime.now() - self.video_start_time).total_seconds()
frame_text = 'output : ' + str(count) + '/' + str(self.progress_bar.maximum())
elapsed_time_text = 'elapsed time ' + '{0:02d}:{1:02d}:{2:02d}'.format(int(elapsed_time//3600), int(elapsed_time%3600//60), int(elapsed_time%60))
self.statusBar().showMessage( frame_text + ' ' + elapsed_time_text )
class SpinBox(QtWidgets.QWidget):
def __init__(self, text, min, max, value):
super(SpinBox, self).__init__()
self.label = QtWidgets.QLabel()
self.label.setText(text)
self.spin_box = QtWidgets.QSpinBox()
self.spin_box.setMinimum(min)
self.spin_box.setMaximum(max)
self.spin_box.setValue(value)
self.setLayout( QtWidgets.QHBoxLayout() )
self.layout().addWidget(self.label)
self.layout().addWidget(self.spin_box)
self.setContentsMargins(2,2,2,2)
self.layout().setContentsMargins(2,2,2,2)
class DoubleSpinBox(QtWidgets.QWidget):
def __init__(self, text, min, max, value):
super(DoubleSpinBox, self).__init__()
self.label = QtWidgets.QLabel()
self.label.setText(text)
self.spin_box = QtWidgets.QDoubleSpinBox()
self.spin_box.setMinimum(min)
self.spin_box.setMaximum(max)
self.spin_box.setValue(value)
self.spin_box.setSingleStep(0.1)
self.setLayout( QtWidgets.QHBoxLayout() )
self.layout().addWidget(self.label)
self.layout().addWidget(self.spin_box)
self.setContentsMargins(2,2,2,2)
self.layout().setContentsMargins(2,2,2,2)
class MyRecord():
class Field():
def __init__(self, value, unit):
self.value = value
self.unit = unit
def __init__(self, keys, values, units):
self.fields = { k : self.Field(v, u) for k, v, u in zip(keys, values, units) }
def get_field(self, field_name_or_num):
return self.fields[field_name_or_num]
def has_field(self, field_name_or_num):
return field_name_or_num in self.fields
class VideoOutput(QtCore.QThread):
nextSignal = QtCore.pyqtSignal()
finishSignal = QtCore.pyqtSignal()
def __init__(self, parent):
super(VideoOutput, self).__init__(parent)
self.records = []
self.datas = {}
self.codec = cv2.VideoWriter_fourcc(*'mp4v')
self.filename = 'output.mp4'
self.start_time = 0
self.setting = {
'width':1280, 'height':720, 'fps':1.0, 'font_scale_title':1.3, 'font_scale_value':1.8,
'font_thickness_inner':2, 'font_thickness_outer':6, 'font_face':cv2.FONT_HERSHEY_DUPLEX,
'font_color_inner':(255,255,255), 'font_color_outer':(0,0,0), 'font_line_type':cv2.LINE_AA,
'backgound_color':(0, 255, 0), 'title_offset':-65, 'unit_offset':200
}
def image(self, record):
image = np.full( (self.setting['height'], self.setting['width'], 3), (0, 255, 0), dtype=np.uint8 )
font_face = self.setting['font_face']
font_scale_title = self.setting['font_scale_title']
font_scale_value = self.setting['font_scale_value']
font_color_inner = self.setting['font_color_inner']
font_color_outer = self.setting['font_color_outer']
font_thickness_outer = self.setting['font_thickness_outer']
font_thickness_inner = self.setting['font_thickness_inner']
font_line_type = self.setting['font_line_type']
title_offset = self.setting['title_offset']
unit_offset = self.setting['unit_offset']
for key in self.datas:
data = self.datas[key]
if record.has_field( data['Key'] ):
value = record.get_field( data['Key'] ).value
else:
if key == 'Time' or key == 'Timestamp':
value = datetime.datetime(1900, 1, 1, 0, 0, 0, 0)
else:
value = 0
x, y, value, unit = data['X'], data['Y'], data['Format'].format(value), data['Unit']
cv2.putText(image, key, (x,y+title_offset), font_face, font_scale_title, font_color_outer, font_thickness_outer, font_line_type)
cv2.putText(image, key, (x,y+title_offset), font_face, font_scale_title, font_color_inner, font_thickness_inner, font_line_type)
cv2.putText(image, value, (x,y), font_face, font_scale_value, font_color_outer, font_thickness_outer, font_line_type)
cv2.putText(image, value, (x,y), font_face, font_scale_value, font_color_inner, font_thickness_inner, font_line_type)
cv2.putText(image, unit, (x+unit_offset,y), font_face, font_scale_title, font_color_outer, font_thickness_outer, font_line_type)
cv2.putText(image, unit, (x+unit_offset,y), font_face, font_scale_title, font_color_inner, font_thickness_inner, font_line_type)
return image
def run(self):
height, width, fps = self.setting['height'], self.setting['width'], self.setting['fps']
video = cv2.VideoWriter( self.filename, self.codec, fps, (width, height) )
keys = [ self.datas[k]['Key'] for k in self.datas ]
units = [ self.datas[k]['Unit'] for k in self.datas ]
self.start_time = self.records[0].get_field('timestamp').value.timestamp()
last_frame = None
for i, record1 in enumerate(self.records[1:]):
record0 = self.records[i]
t0 = record0.get_field('timestamp').value.timestamp()
t1 = record1.get_field('timestamp').value.timestamp()
frame_count = int( (t1 - t0) * fps )
arrays = {}
arrays['timestamp'] = np.linspace( t0, t1, frame_count + 1 )
arrays['timestamp'] = [ datetime.datetime.fromtimestamp(t) for t in arrays['timestamp'] ]
arrays['time'] = np.linspace( t0 - self.start_time, t1 - self.start_time, frame_count + 1 )
arrays['time'] = [ datetime.datetime.fromtimestamp(t) for t in arrays['time'] ]
for key in keys:
if key == 'timestamp':
continue
if not record0.has_field(key):
continue
if not record1.has_field(key):
continue
arrays[key] = np.linspace( record0.get_field(key).value, record1.get_field(key).value, frame_count + 1 )
arrays = np.array([ arrays.get(k) for k in keys]).T
for array in arrays[:-1]:
try:
record2 = MyRecord(keys, array, units)
image = self.image(record2)
video.write(image)
last_frame = image
except:
video.write(last_frame)
self.nextSignal.emit()
video.release()
self.finishSignal.emit()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()