Комментарии 75
p.s. Цифровал на чем было — TV-тюнер «Manli Home TV» (подключение тюльпанами) + Fly2000TV + комп из разряда celeron 1.3 с диском 40Гб. Вот это было приключение :)
Интересно, сколько просмотров от родственников наберут эти видео? ;)
возможно вам будет интересно, посмотрите например приложение для android remini

Проблема в том, что нейросети не улучшают, а додумывают
А можете дать ссылки на то, чем пользовались?
https://play.google.com/store/apps/details?id=com.bigwinepot.nwdn.international
Сейчас ещё пытаюсь вот этот проект майкрософта завести локально https://github.com/microsoft/Bringing-Old-Photos-Back-to-Life хочу попробовать пакетную обработку всех фото запустить
Локально запустилось, пока не обрабатывает тестовый файл с царапинами «а» по неясной причине и обычный «с» из-за нехватки памяти, карта GTX1660 6Gb.
Главное правило, которое я для себя сделал это ролик должен быть не более 8 минут. Иначе никто смотреть не будет. Да и сам не будешь смотреть. Вот до 8 минут еще можно.
Пожтому привозя видео из путешествий я делаю один ролик — один день.
Куда их потом девать тот еще вопрос… Диски ломаются, облако дорого…
Куда их потом девать тот еще вопрос…
Думается, винчестер на "холодном хранении" из современных технологий самое надёжное. (Стример — ещё дороже, флешки/ссд — на зарядку каждые два месяца, оптика сд/двд — помирает потихой и новая делается одноразовой, оптика бд — ещё не показала себя на масштабе десятилетий).
Глядя на огромные ящики miniDV кассет родителей и смонтированные с них двухчасовые диски, которые ни разу не отсматривались, а потом добавляющиеся ещё в очередь на оцифровку ящики фоток от бабушек, прабабушек и прапра-..., решил снимать для интернета немного, а остальное писать в голову. Всё же потом хочется вспоминать то, как был где-то и делал что-то, а не как смотрел на всё это через экран размером с почтовую марку, стараясь записать кадр получше...
www.youtube.com/watch?v=77R8Dfu0FmA
У друзей были круглые глаза, когда я собрал ролик с поездки, пока ждали заказ в кафе.
Оригиналы хранить даже смысла не вижу. Ну или по крайней мере удалять спустя полгода — год
Сильную долю в аудио-треке оно само распознаёт как-то? Или надо как-то дорожку разметить, и оно потом будет помогать склейки расставлять по этой разметке?
У меня для вас плохие новости — и до 8 минут никто смотреть не будет.
Впрочем, если у вас интересный материал — "клип о том, как меня сожрала анаконда но я смог выбраться с другой стороны" то это другое дело. Но такие записи редкость.
Меня особенно удивило, что вместо поиска причины проблемы (сходить поспрошать на реддите хотя бы) автор маниакально пытался экспериментировать вслепую. "Чего там думать, прыгать надо".
Ох уж эти китайские видеозахватки в USB — прям как конфеты у Форрест Гампа, никогда не догадаешься, что внутри, даже если бренд японский. Настолько рандомные штуки, что поиск по ютубу "Как решить проблему с драйверами на Easycap 007" выдаёт видос, как кто-то заливает его бензином и поджигает.
Совет начинающим оцифровщикам — берите Canopus или на крайняк Datavideo, в пределах СНГ ещё применим очень бюджетный вариант с очень хорошим качеством — Behold TV и их тюнеры на любой вкус и размер. С учётом, что в регионах России почти любая "оцифровочная контора" вам вернёт видео разрешения 240х240 и жёваную плёнку со словами «Ну а что вы хотели, это же с кассеты!», сделать всё дома будет проще, дешевле и надёжнее.
О, кажется, здесь хорошее место, чтобы немного поныть на эту тему. Я вот тоже хочу оцифровать свои кассеты, но не могу определиться, как именно это сделать.
Видео чересстрочное, и значит, наверное, нужно провести деинтерлейсинг, но мне попадалось инфа, что алгоритмы деинтерлейсинга могут вносить искажения, поэтому стрёмно. Или на самом деле всё это чепуха и можно зарядить yadif2x без опаски?
Если не проводить деинтерлейсинг (благо H.264 поддерживает чересстрочность), то возникает другая прооблема. Исходный видеопоток, отдаваемый видеозахваткой, закодирован в YUV 4:2:2, который аппаратно поддерживается примерно никем, а если его перекодировать YUV 4:2:0, то из-за смешивания цветоразностных компонентов соседних полей цвета убиваются в хлам:

Я так понимаю, автор просто забил болт на эти проблемы, и эта его «профессиональная оцифровка» даже 60fps умудрилась продолбать? Может, кто-то на Хабре тоже раздумывал, чё с этим получше сделать? Или воткнуть yadif2x и не париться?
Хотя с деинтерлейсингом вспомнил ещё одну проблему. Имеется тупой (во всех смыслах) телевизор DEXP, который умеет вопроизводить видеофайлы с USB, и если видео имеет частоту больше 30 кадров в секунду, то телевизор, похоже, считает его чересстрочным и пытается проводить собственный деинтерлейсинг — в итоге видео получается с кучей искажений, мерцанием и почему-то замедленной скоростью. А деинтерлейсить в 25/30фпс — себя не уважать
Используя EasyCap и OBS (там есть настройка устранения чересстрочности, я ставил Смешивание или Смешивание 2х) можно получить вполне терпимый результат. Но нужно понимать — в VHS используется полукадровая запись — пишутся по очереди четные и нечетные строки каждого кадра, поэтому при резких движениях будет гребенка или размытие.
Ну а потом в VSDC FreeVideoEditor пожно подрезать и сжать в H264.
В вебе, считайте, деинтерлейс отсутствует как класс. На ютубе довольно много видео, которое было залито интерлейсным, а затем с компа смотришь его уже как-бы как прогрессив, но с гребёнками.
Да мне по барабану. Тебя одного это беспокоит.
Кажется, это было даже не про то, что кто-то увидит эти видосы, а про то, что это вообще делается:) Десятки часов, как мой брат грызет клавиатуру или играется с котом?.. На свалку.
Туда же. Никто это смотреть всё равно не будет.
Напротив, если сохранить контекст — указать имена, места, занятия, описать сюжет — такие фотографии приобретают огромную важность для всей Википедии (не только для энциклопедических статей).
Если же она осталась битами в облаке, то конечно, будет бесполезна и безынтересна из-за не существования ни в каком виде во времена заинтересованных лиц.
то уже через 100 лет она будет ценным свидетельством материальной культуры: что носят, как носят, какие прически
Даже 20 лет вполне достаточно. В то время снимали не так много, большая часть любительских снимков была низкого качества и уже утрачена по разным причинам.
Поэтому хорошо снятые фото того времени пользуются большой популярностью, причем не у историков (им еще рано), а у современников (ностальгия :)
Если же она осталась битами в облаке, то конечно, будет бесполезна и безынтересна
Только из опасения, что это фейк.
Решил проблему оцифровки на работе покупкой Matrox RT.X2, аудиокартой ESI Juli@ и магнитофоном S-VHS с авито за 5000р. Максимум который можно выжать — выжал. Рассинхрона звука нет, видеопоток в максимальном качестве. Сжатием занимается Квадра М4000. Вот со сжатием больше всего проблем выдалось — любое сжатие на интерлейсном видео выдает так себе результат — либо слишком большой файл, либо шакалы.
Личный опыт.
Не мучайтесь как автор.
А через Европу много камер ввозилось в СНГ
Первую камеру я купил в 1997, вторую — в 2007.
Несмотря на десятилетнюю разницу — обе камеры попали к нам с Ближнего Востока (ОАЭ).
Еще лет 10 назад решили так же записать эксклюзивное видео с детства, обычный USB Capture 007 (вроде), sony vegas, и тоже не было проблем, кроме нарезки — снимали тогда все, у кого есть камера, и все подряд, а хотелось сделать что-то без 4 минут съемки потолка.
А кто то в курсе, какой хард/софт у коммерческих предложений?
Поскольку снимал видео правильно, выбирая интересные моменты и избегая долгих и монотонных планов -перемонтировать почти ничего не пришлось. Файлы видео записал на диски и раздал родственникам. Ну и себе, конечно, оставил, вместе с резервными копиями
Кассеты выбросил.
В 2020 году пришла мысль — вот
«Маргадон, один пистолет надо было зарядить!» (с)
(Часть кассет надо было оставить — так как сам процесс съемки и просмотра начал вызывать ностальгию :)
#! python3.5
import bisect
import os
import re
import subprocess
import sys
import threading
import tkinter
from tkinter import ttk
import win32api
import win32com.client
import win32con
import win32console
import win32gui
import mylib
class Application(ttk.Frame):
def __init__(self, master=None):
global padx, pady
super().__init__(master)
master.title(os.path.split(sys.argv[0])[1])
master.resizable(False, False)
# Определим размер шрифта для расчета горизонтальных и вертикальных отступов
ttk.Style().configure("TLabel", font=font)
self.label = ttk.Label(self, text="0" * 2)
self.label.pack()
padx = self.label.winfo_reqwidth()
pady = self.label.winfo_reqheight()
self.label["text"] = self.label["text"][0]
padx -= self.label.winfo_reqwidth()
self.label.destroy()
del self.label
self.correction_value = tkinter.IntVar()
self.correction_value.set(1100)
self.correction_focused = None
self.sign_factor = -1
self.correction0_value = tkinter.IntVar()
self.correction0_value.set(0)
self.sign0_factor = -1
self.correction0_focused = None
self.saved_value = None
self.grid()
self.createWidgets()
def update_application_window(self):
if root.wm_state() == "withdrawn":
x = (root.winfo_screenwidth() - root.winfo_reqwidth()) // 2
y = (root.winfo_screenheight() - root.winfo_reqheight()) // 2
root.wm_geometry("+{0:d}+{1:d}".format(x, y))
root.deiconify() # Делаем видимым основное окно
for suffix in ("", "0"):
correction = getattr(self, "correction{0}".format(suffix))
correction_focused = "focus" in correction.state()
if correction_focused and not getattr(self, "correction{0}_focused".format(suffix)):
correction.select_range(0, "end")
setattr(self, "correction{0}_focused".format(suffix), correction_focused)
state = "disabled" if self.correction_value.get() == self.correction0_value.get() else "normal"
self.hour["state"] = self.minute["state"] = (state, )
self.update_idletasks()
self.update()
self.after(100, self.update_application_window)
def change_sign(self, widget):
def factor():
return "sign{0}_factor".format("0" if correction0 else "")
correction0 = self.nametowidget(widget) in (self.correction0, self.sign0_button)
setattr(self, factor(), -getattr(self, factor()))
button = getattr(self, "sign{0}_button".format("0" if correction0 else ""))
button["text"] = "-" if getattr(self, factor()) < 0 else "+"
def key_return(self, event):
if event.widget == self.run_button:
self.run()
else:
event.widget.tk_focusNext().focus()
next_widget = root.focus_get()
if next_widget == self.sign0_button:
next_widget.tk_focusNext().focus()
next_widget = root.focus_get()
if next_widget in (self.correction, self.correction0, self.hour, self.minute):
next_widget.select_range(0, "end")
def validate_correction(self, widget, data, action):
correction0 = self.nametowidget(widget) == self.correction0
value = getattr(self, "correction{0}_value".format("0" if correction0 else ""))
other_value = getattr(self, "correction{0}_value".format("" if correction0 else "0"))
entry = getattr(self, "correction{0}".format("0" if correction0 else ""))
valid = re.match(r'^\d+$', data) is not None
if action == '0':
self.saved_value = value.get()
elif action == '1':
if not valid:
try:
value.get()
except Exception:
value.set(self.saved_value)
entry.select_range(0, "end")
if data in "-+":
self.change_sign(widget)
elif data == " ":
other_value.set(value.get())
entry.select_range(0, "end")
else:
return False
return valid
def validate_time(self, widget, new_value, data, action):
def process_widget(name, max_value):
widget = getattr(self, name)
if widget.get() == '':
widget.set(self.saved_value)
widget.select_range(0, "end")
if data == "-":
widget.set(str(max(0, int(widget.get()) - 1)))
elif data == "+":
widget.set(str(min(max_value, int(widget.get()) + 1)))
if action == '0':
self.saved_value = self.nametowidget(widget).get()
return True
elif action == '1':
if widget == str(self.hour):
valid = re.match(r'^\d$', new_value) is not None
if not valid:
process_widget('hour', 9)
elif widget == str(self.minute):
valid = re.match(r'^[0-5]?\d$', new_value) is not None
if not valid:
process_widget('minute', 59)
else:
return False
return valid
def run(self, event=None):
config = """VirtualDub.audio.SetSource(1);
VirtualDub.audio.SetMode(1);
VirtualDub.audio.SetInterleave(1,500,1,0,{2});
VirtualDub.audio.SetClipMode(1,1);
VirtualDub.audio.SetConversion(0,0,0,0,1);
VirtualDub.audio.SetVolume();
VirtualDub.audio.SetCompressionWithHint(8192,48000,2,0,32000,1024,"AC3ACM");
VirtualDub.audio.EnableFilterGraph({0});
VirtualDub.video.SetInputFormat(0);
VirtualDub.video.SetOutputFormat(7);
VirtualDub.video.SetMode(0);
VirtualDub.video.SetSmartRendering(0);
VirtualDub.video.SetPreserveEmptyFrames(0);
VirtualDub.video.SetFrameRate2(0,0,1);
VirtualDub.video.SetIVTC(0, 0, 0, 0);
VirtualDub.video.SetCompression();
VirtualDub.video.filters.Clear();
VirtualDub.audio.filters.Clear();
VirtualDub.audio.filters.Add("input");
VirtualDub.audio.filters.Add("time stretch");
VirtualDub.audio.filters.Connect(0, 0, 1, 0);
VirtualDub.audio.filters.instance[1].SetDouble(0, {1:.8});
VirtualDub.audio.filters.Add("output");
VirtualDub.audio.filters.Connect(1, 0, 2, 0);
"""
try:
correction = self.sign_factor * int(self.correction.get())
except ValueError:
threading.Thread(target=mylib.MessageBox, args=('Не задана коррекция!',
win32con.MB_ICONEXCLAMATION, 5)).start()
return
try:
correction0 = self.sign0_factor * int(self.correction0.get())
except ValueError:
threading.Thread(target=mylib.MessageBox, args=('Не задана коррекция нуля!',
win32con.MB_ICONEXCLAMATION, 5)).start()
return
try:
time_span = (int(self.hour.get()) * 60 + int(self.minute.get())) * 60
except ValueError:
time_span = 0
if not time_span:
threading.Thread(target=mylib.MessageBox, args=('Не задано время!',
win32con.MB_ICONEXCLAMATION, 5)).start()
return
if not os.path.exists(PROGRAM):
threading.Thread(target=mylib.MessageBox, args=('Не обнаружен "{0}"!'.format(PROGRAM),
win32con.MB_ICONEXCLAMATION, 5)).start()
return
correction -= correction0
if not correction:
config = re.sub(r'^.*\.audio\.filters\.(?!Clear).*$', '', config, flags=re.MULTILINE).rstrip()
factor = 1.0 + correction / 1000 / time_span
with open(CONFIG_NAME, "w") as config_file:
config_file.write(config.format(1 if correction else 0, factor, correction0))
subprocess.Popen('"' + PROGRAM + '" /s "{0}"'.format(CONFIG_NAME))
self.quit()
def createWidgets(self):
self.config(padding=(padx, int(pady/4)))
style = ttk.Style()
style.configure("TLabel", font=font)
style.configure("TButton", font=font)
style.configure("C.TButton", font=font, foreground="red")
style.configure("TCombobox", font=font)
style.configure("TRadiobutton", font=font)
root.bind('<Key-Return>', self.key_return)
root.bind('<Control-Key-Return>', self.run)
row = 0
self.header = ttk.Label(self, text="Задай коррекцию и время")
self.header.grid(row=row, column=0, columnspan=4)
row += 1
self.correction_label = ttk.Label(self, text="Коррекция(ms)*:")
self.correction_label.grid(row=row, column=0, sticky="e")
self.clear_button = ttk.Button(self, text="C", width=1, style="C.TButton")
self.clear_button["command"] = lambda:self.correction_value.set(0)
self.clear_button.grid(row=row, column=1)
self.sign_button = ttk.Button(self, text="-", width=1)
self.sign_button["command"] = (self.register(self.change_sign), self.sign_button)
self.sign_button.grid(row=row, column=2)
self.correction = ttk.Entry(self, width=5, textvariable=self.correction_value, validate="key",
validatecommand=(self.register(self.validate_correction), '%W', '%S', '%d'))
self.correction.grid(row=row, column=3)
self.correction.focus()
row += 1
self.correction0_label = ttk.Label(self, text="Коррекция нуля(ms)*:")
self.correction0_label.grid(row=row, column=0, columnspan=2, sticky="e")
self.sign0_button = ttk.Button(self, text="-", width=1)
self.sign0_button["command"] = (self.register(self.change_sign), self.sign0_button)
self.sign0_button.grid(row=row, column=2)
self.correction0 = ttk.Entry(self, width=5, textvariable=self.correction0_value, validate="key",
validatecommand=(self.register(self.validate_correction), '%W', '%S', '%d'))
self.correction0.grid(row=row, column=3)
row += 1
self.time_label = ttk.Label(self, text="Время(h:mm):")
self.time_label.grid(row=row, column=0, sticky="e")
self.hour = ttk.Combobox(self, width=1, validate="key",
validatecommand=(self.register(self.validate_time), '%W', '%P', '%S', '%d'))
self.hour["values"] = list(map(lambda x:"{0}".format(x), range(10)))
self.hour.current(0)
self.hour.grid(row=row, column=1)
self.sc1 = ttk.Label(self, text=":")
self.sc1.grid(row=row, column=2)
self.minute = ttk.Combobox(self, width=2, validate="key",
validatecommand=(self.register(self.validate_time), '%W', '%P', '%S', '%d'))
self.minute["values"] = list(map(lambda x:"{0:>02}".format(x*5), range(12)))
self.minute.current(10)
self.minute.grid(row=row, column=3)
row += 1
self.space_label = ttk.Label(self, text="*Space-копировать")
self.space_label.grid(row=row, column=0, sticky="w")
self.run_button = ttk.Button(self, text="Запуск", width=8, command=self.run)
self.run_button.grid(row=row, column=1, columnspan=3, pady=int(pady/4))
self.after(100, self.update_application_window)
if __name__ == "__main__":
FOLDER = os.path.split(sys.argv[0])[0]
PROGRAM = os.path.join(FOLDER, "VirtualDub.exe")
CONFIG_NAME = os.path.join(FOLDER, "config.vcf")
MONIKER = r"winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2"
SCRIPT_TITLE = os.path.split(sys.argv[0])[1]
keep_output = False
COMSPEC = os.getenv("comspec")
if COMSPEC: # Проверим на запуск в режиме сохраниеия вывода после завершения
parent_process_moniker = (MONIKER + ":Win32_Process.Handle=" + str(os.getppid()))
try:
for i in range(2):
parent_process = win32com.client.GetObject(parent_process_moniker)
keep_output += re.match('("?)' + COMSPEC.replace("\\", "\\\\").replace(".", "\\.") + '\\1\s+/[ck]\s+',
parent_process.CommandLine, re.IGNORECASE) is not None
parent_process_moniker = (MONIKER + ":Win32_Process.Handle=" + str(parent_process.ParentProcessId))
except Exception:
pass
finally:
parent_process = None
main_hwnd = win32console.GetConsoleWindow()
if main_hwnd:
if win32api.GetConsoleTitle() != SCRIPT_TITLE:
win32api.SetConsoleTitle(SCRIPT_TITLE)
if not keep_output:
win32gui.ShowWindow(main_hwnd, win32con.SW_HIDE)
root = tkinter.Tk()
root.withdraw() # Скроем основное окно до прорисовки точно в центре экрана (делаем видимым методом update_application_window)
# Важно! Шрифт моноширинный
font_sizes = [(0, 8), (480, 10), (864, 12)]
font = ("Lucida Console", font_sizes[bisect.bisect(font_sizes,
(root.winfo_screenheight(), float("inf"))) - 1][1], "normal")
app = Application(master=root)
app.mainloop()
try:
root.destroy()
except tkinter.TclError:
pass
Дважды оцифровывал видео. Один раз использовал плату Miro, второй раз — какую-то внешнюю коробочку кажется от Sony. Оба раза — никаких проблем.
Желающим оцифровывать я бы предложил такой пайплайн:
- Зарипать видео с кассеты, выход сохранить в interlaced режиме в ProRES или DNxHD, накрайняк h264 с высоким битрейтом (не меньше пары десятков мегабит в секунду), если при этом исходик не в YUV420 — то писать в YUV444. При этом следить за цветовых пространством (PAL/NTSC), и за частотой кадров (50i для PAL, 59.94i для NTSC) — всё должно быть как в исходнике. Звук — aac 256 кбит/с. Исходники не удалять, по крайней мере сразу.
- Намонтировать всё это в нормальные ролики, сделать деинтерлейс (в 25/29.97 кадров в секунду соответственно), цветокоррекцию, перевести в цветовое пространство BT.709. Всё сразу можно в Davinci Resolve, например.
- Выгнать в h264/h265 с битрейтом не менее 5/4 мбит/с, с двухпроходным кодированием. Звук aac с битрейтом не менее 196 кбит/с. Результат уже куда угодно заливать.
Я бы СРАЗУ пошел к профессионалам и выяснил бы у них, как они проводят оцифровку.
Если мне критична приватность, можно было бы либо договориться от личном присутствии при оцифровке, либо об аренде их оборудования с инструктажем.
Но я немного слышал, что самое лучшее качество получается, если просто купить цифровую видеокамеру с поддержкой композитного входа, и сделать оцифровку через нее. Без кривых китайских видеозахватов, без магических опций виртуалдаба, выбора формата, рассинхронов и так далее. Еще и камера бы осталась в доме.
В общем в статье первая часть — грустная. Часть третья — скорее бесполезная. Облачный стриминг для личного видео? Расшарить видео сейчас не слишком сложно.
А вот часть 2, про таблицу и скрипты — самая полезная. Возможно стоило поделиться наработками и примерами использования — вполне найдутся айтишники непрограммисты, которым могли бы пригодиться.
p.s. правда я бы нарезал через csv/excel + bash+ffmpeg
Мой восьмилетний квест по оцифровке 45 видеокассет. Часть 1