Эта статья является логичным продолжением одной из предыдущих, в которой показывалось применение бытового тепловизора в связке с квадрокоптером. Многие подробности реализации описаны там, здесь будет крупноузловая сборка и особенности применения.

Итак, раз на выходе "малины" у нас аналоговый видеосигнал, то ничто не мешает убрать находящееся между тепловизором и человеком звено в виде радиоканала и передавать картинку с датчика прямо в шлем/очки.

В данном случае для сборки потребовались уже знакомый тепловизор Infiray P2 Pro, одноплатный компьютер Raspberry Pi Zero 2W, а также бюджетный шлем Eachine EV800DM. Его часто советуют новичкам и за свои деньги он справляется с задачей на 5+. Огромным преимуществом именно шлема в сравнении с очками является наличие встроенного питания.

Общая схема предельно проста:

В крупную клетку
В крупную клетку

Запитать одноплатник можно прямо от шлема (разумеется, через преобразователь), внутри которого обычная литий-полимерная батарея на 3,7 В.

Разборка

Чтобы получить доступ к батарее и плате нужно выкрутить четыре винта, скрепляющих держатель линзы:

Главный герой повествования.
Главный герой повествования.

После выкручивания держатель ослаблен и, осторожно разжав его половины, извлекаем из пазов основную часть с экраном:

Экран кажется невероятно маленьким, а картинка на нём смотрится очень необычно.
Экран кажется невероятно маленьким, а картинка на нём смотрится очень необычно.

Кстати, сама линза даёт очень хорошее увеличение, поэтому вместе с держателем её можно использовать для пайки и мелкого ремонта просто надев на голову. Так что не спешите выбрасывать сломанный шлем, он ещё пригодится ;)

Увеличение наглядно. Держатель тут вверх ногами, но не суть.
Увеличение наглядно. Держатель тут вверх ногами, но не суть.

Но нас интересует не она, а плата:

Собственно плата
Собственно плата
Производитель не обманул насчёт двухканальности, на плате действительно два приёмника.
Производитель не обманул насчёт двухканальности, на плате действительно два приёмника.

Недолго думая, ко входу питания были припаяны выводы для подключения распространённого повышающего преобразователя HW-106:

И тут случилось непредвиденное: мощности преобразователя оказалось недостаточно для "малины" и в процессе загрузки одноплатник просто выключался. Потребовался более мощный и стабильный источник питания и выбор пал на U3V40F5 от Pololu, отдающий до 4 ампер (в пределе "малина" потребляет 2,5 А) при входном напряжении от 1,3 В, и имеющий КПД свыше 90%:

Распайка.
Распайка.

С ним всё завелось.

Подключение к очкам

Следующий участок работы – передача сигнала. В коробке со шлемом шел простой переходник для аналогового видео:

Для наших целей он едва ли применим, но службу сослужит. С помощью кнопки SRC/Menu переключим шлем в режим аналогового входа (его признаком кроме OSD-надписи "AV" является синий экран смерти):

Режим аналогового входа.
Режим аналогового входа.

Теперь, вставив переходник в гнездо шлема V-in и просто коснувшись его выводов проводами, припаянными к выходам TV и GND "малины", мы увидим в шлеме картинку:

В работе.
В работе.

Это доказывает работоспособность схемы.

Последняя часть – обычный четырёхвыводной TRRS-штекер (3,5"):

TRRS = Tip, Ring, Ring, Sleeve, т. е. наконечник, первое кольцо, второе кольцо, гильза.
TRRS = Tip, Ring, Ring, Sleeve, т. е. наконечник, первое кольцо, второе кольцо, гильза.

Прозвоном переходника определяем его распайку:

Гильза – земля (чёрный провод), нижнее кольцо – сигнал (желтый провод).
Гильза – земля (чёрный провод), нижнее кольцо – сигнал (желтый провод).

Обратите внимание, что в некоторых случаях разводка может отличаться. Обязательно прозванивайте переходник.

Прототип получился таким:

Первое поколение
Первое поколение

Теперь вставим в шлем карту памяти, примотаем всю конструкцию изолентой, и можно начинать испытания:

Вероятно, вы заметили, что переключение между палитрами выполняется с клавиатуры. Сделано это с помощью удалённого рабочего стола и подключения к "малине" по WiFi (подробности в прошлой статье). В моём случае это не особо нужно, но в общем система позволяет передавать картинку из очков на монитор компьютера, что может быть полезно для группового наблюдения.

Изображение получилось довольно шумным, чему виной два фактора:

  • работа импульсного преобразователя

  • включенный на одноплатнике вай-фай, рабочая частота которого (5,8 ГГц) совпадает с частотой приёмников в шлеме

Теперь займёмся подавлением помех.

Шумоподавление

Перво-наперво отключаем вай-фай. Сделать это можно как с помощью иконки в правом верхнем углу рабочего стола "малины", так и программно. Для надёжности в starter.sh пропишем

sudo rfkill block wifi

Из документации производителя следует, что частота импульсов ШИМ в преобразователе под нагрузкой достигает 600 кГц. Следствием этого становится рябь даже при выключенном вай-фае:

Картинка без фильтра

Первый и самый простой шумодав – это конденсатор на выходе преобразователя. У меня в запасе было несколько керамических конденсаторов поверхностного монтажа на 10 мкФ (GRM31CR71C106KA12L), поэтому начал с них:

Кривовато, но работает
Кривовато, но работает
Картинка стала чуть лучше

Горизонтальные полосы почти исчезли, время от времени вылезает непонятная сетчатая рябь вокруг руки.

Второе решение – полноценный LC-фильтр вслед за конденсатором. Для убойности я взял дроссель SRP1265A-4R7M (4,7 мкГн) и электролитический конденсатор с низким ЭПС (470 мкФ):

Расчёт по известной формуле показывает, что частота отсечки такого фильтра составляет около 3,39 кГц, что значительно ниже частоты ШИМ преобразователя и должно хорошо сглаживать пульсации напряжения.

Проверим:

К своему удивлению, я обнаружил зависимость помех от положения фильтра в пространстве: иногда они полностью отсутствуют, но если повернуть его чуть в сторону, то снова появляется рябь.

Вариант с LC-фильтром оказался крайне неудобным, т. к. добавляет в конструкцию ещё одну свисающую часть и лишние провода:

Третий – использование интегрального стабилизатора напряжения, например, LD29150. Его предельный ток составляет лишь 1,5 А, и, хотя по спецификации "малине" требуется 2,5 А, в моём случае она потребляет значительно меньше и полутора ампер хватает. Ещё один плюс интегрального стабилизатора – это простота и небольшие размеры, что позволило припаять его к обратной стороне платы:

Всё ещё кривовато, зато ничего не свисает и не торчит
Всё ещё кривовато, зато ничего не свисает и не торчит

Испытываем:

Если честно, качество меня разочаровало, я ожидал более чистой картинки.

Сравнение картинки без фильтрации, с конденсатором, LC-фильтром и интегральным стабилизатором напряжения (слева сверху – отсутствие фильтрации, справа – конденсатор, внизу слева – LC-фильтр, справа – интегральный стабилизатор):

В итоге я остановился на фильтре из двух параллельных конденсаторов: 10 мкФ (GRM31CR71C106KA12L) и 0,1 мкФ (104).

Маска Хищника:

Ожидание
И суровая действительность ?

Код

Код на питоне не претерпел особых изменений, единственное, что я добавил, – это запись картинки в файл (строки 116-118 и 276/279).

Код целиком
print('Les Wright 21 June 2023')
print('https://youtube.com/leslaboratory')
print('A Python program to read, parse and display thermal data from the Topdon TC001, Infiray P2 Pro and other compatible USB Thermal Cameras!')
print('')
print('THIS IS A PATCHED VERSION; patch created by CATCH22MANIA \'07/24 - https://www.youtube.com/@catch22mania')
print('')
print('Key bindings:')
print('')
print('c x: cycle through colormaps - 0 = raw (no hud)')
print('f w: fullscreen, windowed')
print('h : toggle HUD')
print('q : quit')
print('')
print('1 : show highest temperature')
print('2 : show centre crosshairs temperature')
print('3 4: change scale - in fullscreen: zoom in/out font')
print('5 6: +/- contrast')
print('7 8: +/- blur')
# print('9 0: floating high and low temp label threshold')
print('r t: Record and Stop')
# print('p : Snapshot')
print('')
print('check out the command line options > python ~/testing/ptc-patched-c22-014-test1.py -h')
print('')

import cv2
import numpy as np
#print(np.__version__)
import argparse
import time
import io

#256x192 General settings
width = 256 #Sensor; width for output window size (* scale)
height = 192 #sensor; height for output windows size (* scale)
scale = 3 #scale multiplier
alpha = 1.0 # Contrast control (1.0-3.0)
colormap = 0
font=cv2.FONT_HERSHEY_SIMPLEX
dispFullscreen = False
rad = 0 #blur radius
threshold = 2
hud = False
recording = False
showcrosshairs = False
showtemps = False
elapsed = "00:00:00"
snaptime = "None"


#We need to know if we are running on the Pi, because openCV behaves a little oddly on all the builds!
#https://raspberrypi.stackexchange.com/questions/5100/detect-that-a-python-program-is-running-on-the-pi
def is_raspberrypi():
    try:
        with io.open('/sys/firmware/devicetree/base/model', 'r') as m:
            if 'raspberry pi' in m.read().lower(): return True
    except Exception: pass
    return False

isPi = is_raspberrypi()

parser = argparse.ArgumentParser()
parser.add_argument("--device", type=int, default=0, help="video device number; e.g. 0, use v4l2-ctl --list-devices")
parser.add_argument("--width", type=int, default=0, help="video output width; e.g. 720, scale to width (scale factor = 1)")
parser.add_argument("--height", type=int, default=0, help="video output height; e.g. 576, scale to height (scale factor = 1)")
parser.add_argument("--fullscreen", dest="fullscreen",action='store_true', help="start in fullscreen; no argument")
parser.add_argument("--colormap", type=int, default=3, help="colormap; range: 1-22; 0 = no colormap (raw); default: 3")
parser.add_argument("--crosshairs", dest="crosshairs", action='store_true', help="show crosshairs; no argument")
parser.add_argument("--temps", dest="temps", action='store_true', help="show temperatures; no argument")
parser.add_argument("--blur", type=int, default=0, help="blur radius; default: 0")
parser.add_argument("--contrast", type=float, default=1.0, help="contrast; default: 1.0")

args = parser.parse_args()
	
if args.device:
	dev = args.device
else:
	dev = 0
if args.width:
	scale = 1
	width = args.width	
if args.height:
	scale = 1
	height = args.height	
if args.fullscreen:
	dispFullscreen = args.fullscreen
if args.colormap:
	colormap = args.colormap	
if args.blur:
	rad = args.blur	
if args.contrast:
	alpha = args.contrast		
if args.crosshairs:
	showcrosshairs = args.crosshairs
if args.temps:
	showtemps = args.temps

newWidth = width*scale 
newHeight = height*scale


cap = cv2.VideoCapture('/dev/video'+str(dev), cv2.CAP_V4L)
#pull in the video but do NOT automatically convert to RGB, else it breaks the temperature data!
#https://stackoverflow.com/questions/63108721/opencv-setting-videocap-property-to-cap-prop-convert-rgb-generates-weird-boolean
if isPi == True:
	cap.set(cv2.CAP_PROP_CONVERT_RGB, 0.0)
else:
	cap.set(cv2.CAP_PROP_CONVERT_RGB, False)
	

cv2.namedWindow('ThermalWindow',cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty('ThermalWindow',cv2.WND_PROP_FULLSCREEN,cv2.WINDOW_FULLSCREEN)
if dispFullscreen == False:
	cv2.resizeWindow('ThermalWindow', newWidth,newHeight)

current_time_str = time.strftime("%Y%m%d--%H%M%S")
fourcc = cv2.VideoWriter_fourcc(*'XVID')
out = cv2.VideoWriter(f"thermal_output-{current_time_str}.avi", fourcc, 25, (newWidth, newHeight))

while(cap.isOpened()):
	# Capture frame-by-frame
	ret, frame = cap.read()
	if ret == True:
		imdata,thdata = np.array_split(frame, 2)
		hi = thdata[96][128][0]
		lo = thdata[96][128][1]
		lo = lo*256
		rawtemp = hi+lo
		temp = (rawtemp/64)-273.15
		temp = round(temp)

		#find the max temperature in the frame
		wat = np.add(thdata[...,0], np.multiply(thdata[...,1], 256))
		maxtemp = wat[...].max()
		posmax = wat[...].argmax()
		mcol, mrow  = divmod(posmax, 256)
		maxtemp = (maxtemp/64) - 273.15
		maxtemp = round(maxtemp)
		
		# hotspot: quick n dirty for testing - scaling width, height
		mcol2 = mcol / (192 / height) 
		mrow2 = mrow / (256 / width)		
		mcol = round (mcol2)
		mrow = round (mrow2)				

		bgr = cv2.cvtColor(imdata,  cv2.COLOR_YUV2BGR_YUYV)

		bgr = cv2.resize(bgr,(newWidth,newHeight),interpolation=cv2.INTER_CUBIC)#Scale up!

		if colormap != 0: # 0 = no colormap, no blur, no contrast
				
			# contrast
			if alpha> 1.0:					
				bgr = cv2.convertScaleAbs(bgr, alpha=alpha)#Contrast
			# blur
			if rad>0:
				bgr = cv2.blur(bgr,(rad,rad))

			# apply colormap
			if colormap == 1:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_AUTUMN)
				cmapText = 'autumn'
			if colormap == 2:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_BONE)
				cmapText = 'bone'
			if colormap == 3:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_JET)
				cmapText = 'jet'			
			if colormap == 4:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_WINTER)
				cmapText = 'winter'			
			if colormap == 5:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_RAINBOW)
				cmapText = 'rainbow'			
			if colormap == 6:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_OCEAN)
				cmapText = 'ocean'			
			if colormap == 7:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_SUMMER)
				cmapText = 'summer'			
			if colormap == 8:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_SPRING)
				cmapText = 'spring'			
			if colormap == 9:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_COOL)
				cmapText = 'cool'			
			if colormap == 10:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_HSV)
				cmapText = 'HSV'			
			if colormap == 11:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_PINK)
				cmapText = 'pink'			
			if colormap == 12:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_HOT)
				cmapText = 'hot'
			if colormap == 13:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_PARULA)
				cmapText = 'parula'			
			if colormap == 14:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_MAGMA)
				cmapText = 'magma'			
			if colormap == 15:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_INFERNO)
				cmapText = 'inferno'			
			if colormap == 16:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_PLASMA)
				cmapText = 'plasma'			
			if colormap == 17:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_VIRIDIS)
				cmapText = 'viridis'			
			if colormap == 18:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_CIVIDIS)
				cmapText = 'cividis'
			if colormap == 19:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_TWILIGHT)
				cmapText = 'twilight'			
			if colormap == 20:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_TWILIGHT_SHIFTED)
				cmapText = 'twilight s.'			
			if colormap == 21:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_TURBO)
				cmapText = 'turbo'			
			if colormap == 22:
				heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_DEEPGREEN)
				cmapText = 'deepgreen'

			if showcrosshairs == True:
				# draw crosshairs
				cv2.line(heatmap,(int(newWidth/2),int(newHeight/2)+20),\
				(int(newWidth/2),int(newHeight/2)+5),(255,255,255),1) #vline 1
				cv2.line(heatmap,(int(newWidth/2),int(newHeight/2)-5),\
				(int(newWidth/2),int(newHeight/2)-20),(255,255,255),1) #vline 2
							
				cv2.line(heatmap,(int(newWidth/2)-5,int(newHeight/2)),\
				(int(newWidth/2)-20,int(newHeight/2)),(255,255,255),1) #hline 1
				cv2.line(heatmap,(int(newWidth/2)+5,int(newHeight/2)),\
				(int(newWidth/2)+20,int(newHeight/2)),(255,255,255),1) #hline 2			
			
				cv2.putText(heatmap,str(temp)+" C", (int(newWidth/2)+10, int(newHeight/2)-10),\
				cv2.FONT_HERSHEY_SIMPLEX, 0.55,(0, 0, 0), 2, cv2.LINE_AA)
				cv2.putText(heatmap,str(temp)+" C", (int(newWidth/2)+10, int(newHeight/2)-10),\
				cv2.FONT_HERSHEY_SIMPLEX, 0.55,(0, 255, 255), 1, cv2.LINE_AA)

			if hud==True:
				# display black box for our data
				cv2.rectangle(heatmap, (0, 0),(180, 55), (0,0,0), -1)
				# put text in the box
		
				cv2.putText(heatmap,'Colormap: '+str(colormap)+' ('+cmapText+')', (10, 16),\
				cv2.FONT_HERSHEY_SIMPLEX, 0.4,(0, 255, 255), 1, cv2.LINE_AA)

				cv2.putText(heatmap,'Blur: '+str(rad)+' ', (10, 30),\
				cv2.FONT_HERSHEY_SIMPLEX, 0.4,(0, 255, 255), 1, cv2.LINE_AA)

				cv2.putText(heatmap,'Contrast: '+str(alpha)+' ', (10, 44),\
				cv2.FONT_HERSHEY_SIMPLEX, 0.4,(0, 255, 255), 1, cv2.LINE_AA)
					
			if showtemps == True:
				cv2.circle(heatmap, (mrow*scale, mcol*scale), 5, (0,0,0), 1)
				cv2.putText(heatmap,str(maxtemp)+' C', ((mrow*scale)+10, (mcol*scale)-10),\
				cv2.FONT_HERSHEY_SIMPLEX, 0.55,(0,0,0), 2, cv2.LINE_AA)
				cv2.putText(heatmap,str(maxtemp)+' C', ((mrow*scale)+10, (mcol*scale)-10),\
				cv2.FONT_HERSHEY_SIMPLEX, 0.55,(0, 255, 255), 1, cv2.LINE_AA)

				cv2.line(heatmap,(int(mrow*scale),int(mcol*scale)+20),\
				(int(mrow*scale),int(mcol*scale)+5),(255,255,255),1) #vline 1			
				cv2.line(heatmap,(int(mrow*scale),int(mcol*scale)-5),\
				(int(mrow*scale),int(mcol*scale)-20),(255,255,255),1) #vline 2			
				cv2.line(heatmap,(int(mrow*scale)-5,int(mcol*scale)),\
				(int(mrow*scale)-20,int(mcol*scale)),(255,255,255),1) #hline 1
				cv2.line(heatmap,(int(mrow*scale)+5,int(mcol*scale)),\
				(int(mrow*scale)+20,int(mcol*scale)),(255,255,255),1) #hline 2

			#display image
			cv2.imshow('ThermalWindow',heatmap) # rendered image
			out.write(heatmap)
		else:
			cv2.imshow('ThermalWindow',bgr)		# "raw" frame	
			out.write(bgr)

		keyPress = cv2.waitKey(1)
		if keyPress == ord('7'): #Increase blur radius
			rad += 1
		if keyPress == ord('8'): #Decrease blur radius
			rad -= 1
			if rad <= 0:
				rad = 0

		if keyPress == ord('3'): #Increase scale
			scale += 1
			if scale >=5:
				scale = 5
			newWidth = width*scale
			newHeight = height*scale
			if dispFullscreen == False and isPi == False:
				cv2.resizeWindow('ThermalWindow', newWidth,newHeight)
		if keyPress == ord('4'): #Decrease scale
			scale -= 1
			if scale <= 1:
				scale = 1
			newWidth = width*scale
			newHeight = height*scale
			if dispFullscreen == False and isPi == False:
				cv2.resizeWindow('ThermalWindow', newWidth,newHeight)

		if keyPress == ord('f'): #enable fullscreen
			dispFullscreen = True
			cv2.namedWindow('ThermalWindow',cv2.WND_PROP_FULLSCREEN)
			cv2.setWindowProperty('ThermalWindow',cv2.WND_PROP_FULLSCREEN,cv2.WINDOW_FULLSCREEN)
		if keyPress == ord('w'): #disable fullscreen
			dispFullscreen = False
			cv2.namedWindow('ThermalWindow',cv2.WINDOW_GUI_NORMAL)
			cv2.setWindowProperty('ThermalWindow',cv2.WND_PROP_AUTOSIZE,cv2.WINDOW_GUI_NORMAL)
			cv2.resizeWindow('ThermalWindow', newWidth,newHeight)

		if keyPress == ord('5'): #contrast+
			alpha += 0.1
			alpha = round(alpha,1)#fix round error
			if alpha >= 3.0:
				alpha=3.0
		if keyPress == ord('6'): #contrast-
			alpha -= 0.1
			alpha = round(alpha,1)#fix round error
			if alpha<=0:
				alpha = 0.0
 
		if keyPress == ord('h'):
			if hud==True:
				hud=False
			elif hud==False:
				hud=True

		if keyPress == ord('c'): #c to cycle through colormaps
			colormap += 1
			if colormap  > 22:
				colormap = 0
				
		if keyPress == ord('x'): #x to cycle through colormaps
			colormap -= 1
			if colormap < 0:
				colormap = 22				

		if keyPress == ord('q'):
			break
			out.release()
			capture.release()
			cv2.destroyAllWindows()
			exit(0)

		if keyPress == ord('1'):
			if showtemps==True:
				showtemps=False
			elif showtemps==False:
				showtemps=True

		if keyPress == ord('2'):
			if showcrosshairs==True:
				showcrosshairs=False
			elif showcrosshairs==False:
				showcrosshairs=True

Чтобы получить выделение точки с наивысшей температурой в скрипте ~/starter.sh к аргументам команды дописываем --temps:

#!/bin/bash

sudo rfkill block wifi

python /home/admin/testing/ptc-patched-c22-014.py --temps --fullscreen --colormap 20 & # 256x192; options see ~/starter.sh.ptc.examples

После каждого запуска скрипта в ~ появляется файл вида thermal_output-YYYYMMDD--HHmmSS.avi, содержащий изображение с датчика. Давайте сравним его с DVR (картинка, пишущаяся на карту памяти шлема), что даст представление о влиянии помех и аналоговой передачи на первоисточник:

Здесь слева аналоговый DVR, справа – созданное скриптом изображение (первоисточник), записанное в файл. Разница видна невооруженным глазом, особенно если обратить внимание на отрисовку прицела. Небольшое отличие в высоте видео объясняется тем, что разрешение картинки слева равно разрешению рабочего стола "малины" (720х576), в то время как справа оно равно разрешению кадра (768х576).

Причёсывание

В завершение, чтобы хоть как-то облагородить внешний вид, я заказал корпус, объединив две модели:

Итогом стали две накладки из мягкого пластика, которые идеально подошли под мои задачи. К шлему их всё ещё нужно приклеивать (подойдёт двусторонний скотч или обычная изолента).

Трёхмерная модель
Распечатка
Распечатка

Готовая сборка

"Циклоп", прошу любить и жаловать

В идеале неплохо смотрелся бы зацеп, но я не стал заморачиваться и посадил всё на двусторонний скотч.

Испытание зимой

Для начала сравним картинку с InfiRay P2 Pro, полученную с помощью приложения c картинкой, созданной скриптом (в данном случае я беру по 1 кадру):

Приложение P2 Pro
Дом зимой.
Дом зимой.
Малина
Он же.
Он же.

Не знаю, как это работает, но за счёт более узкого угла обзора картинка на глаз выглядит резче и чётче. Секрета здесь нет: андроид-приложение растягивает картинку до 1080х810, в то время как "малина" честно умножает 256х192 датчика на 3 (в нашем коде scale = 3), отдавая в итоге 768х576 (что близко к разрешению шлема).

На качество картинки сильно влияет влажность (в идеале воздух должен быть сухим), поэтому не пугайтесь, если вдруг она станет размытой.

Неожиданный результат дало подключение приставки к очкам Skyzone 02O: я обнаружил почти полное отсутствие помех, что объясняется более дорогим и качественным приёмником, системой шумоподавления и т. п. OLED также даёт о себе знать. DVR напротив разочаровал, картинка выглядит сильно менее резкой, что объясняется более низким разрешением очков – 640x480 против 720х576 у EV800DM:

Единственное преимущество Skyzone в данном случае заключается в наличии цифрового HDMI-входа. Картинка будет лучше, правда, начисто пропадает возможность писать DVR и требуется переделка питания (очки требуют 7,4-22,2 В, т. е. батарею 2S-6S), следовательно, для "малины" нужен уже понижающий преобразователь напряжения.

Можно попробовать совместить лучшее из двух миров и использовать тепловизионную приставку вместо со шлемом Skyzone Cobra, работающим от одного элемента 18650, и оборудованным mini-HDMI входом.

Ну а пока вернёмся к собранным очкам. Ниже будет несколько видео в разных условиях:

На чердаке
Вертолёт
Зимний лес

Наблюдаем за людьми и машинами:

Что можно сказать насчёт использования датчика зимой? С одной стороны, низкая и однородная температура в сочетании с хорошей чувствительностью датчика (≤ 40 мК) способствуют обнаружению всех хоть немного тёплых предметов. С другой детализация оставляет желать лучшего.

Испытание летом

Включение в жаркий летний день, используется палитра Viridis (оттенки зелёного и жёлтого), на мой взгляд, одна из наиболее удобных:

Теперь попробуем более однородную в цветовом отношении (инферно) ночью:

Летом картинка получается более подробной, особенно на закате/рассвете, когда происходит охлаждение/нагрев предметов и за счёт его неравномерности у разных материалов их температура отличается сильнее, чем днём/ночью.

Таким образом, данная система позволяет организовать наблюдение в тепловизионном диапазоне, сделать трансляцию на удалённый рабочий стол, записывать видео в память "малины" и очков. Качество пока так себе, ибо разрешение датчика тепловизионных приставок для смартфонов всего 256х192 при широком угле обзора.

Альтернатива

Более простым решением является использование тепловизионных камер серии AxisFlight, предоставляющих выход CVBS (аналоговое видео), что позволяет передавать тепловизионную картинку напрямую на вход очков/шлема, что показано в видео ниже:

Преимущества:
- простота
- намного меньшее потребление мощности и большее время работы
- в случае использования SkyZone Cobra можно снять крышку, извлечь приёмник и вставить в гнездо обычный 8-штыревой разъём с шагом 2,54 мм (разводка показана в видео), что облегчает установку, улучшает прочность и делает всю сборку более компактной и модульной
- разрешение до 640х512!

Недостатки:
- цена (и шлем, и камера дороже)
- нет программных плюшек вроде прицела, смены палитр, шкалы температур и т. п.
- на данно этапе нельзя передавать картинку одновременно и в шлем, и во вай-фаю
- запись только в DVR шлема
- нет HDMI
- суженный угол обзора

На сегодня всё, спасибо за внимание и до новых встреч!

Комментарии (0)