本文分享了一个名为“Spacestills”的开源程序,可以用来查看NASA电视直播画面(静帧)。演示地址:https://replit.com/@PaoloAmoroso/spacestillsSpacestills主窗口在Replit上运行这是一个简单的系统,带有GUI,可以访问提要流并从网络下载数据。该程序仅需要350行代码,并依赖于一些开源Python库。关于该计划Spacestills会定期从Feed下载NASATV静止帧并将其显示在GUI中。该程序可以校正帧的纵横比并将它们保存为PNG格式。它会自动下载最新的帧并提供手动重新加载、禁用自动重新加载或更改下载频率的选项。Spacestillsis是一个基本版本,但它做了一些有用的事情:捕获并保存在NASATV上直播的太空事件图像。太空爱好者经常在社交网络或论坛上分享他们从NASATV手动截取的屏幕截图。Spacestills使用屏幕捕获工具节省时间,并保存图像文件以供共享。您可以在Replit在线运行Spacestills。开发环境作者用Replit开发了Spacestills。Replit是一个云上的开发、部署和协作环境,支持包括Python在内的数十种编程语言和框架。作为一个ChromeOS和云计算爱好者,我非常喜欢Replit,因为它完全在浏览器中运行,无需下载或安装任何东西。资源和依赖关系Spacestills依赖于一些外部资源和Python库。NASATVfeedStreams肯尼迪航天中心网站有一个页面,其中包含精选的NASA视频流,包括NASATV公共频道。提要流显示最新的静止帧并自动更新。每个提要都带有三种尺寸的帧,Spacestills依赖于704x408像素帧的最大NASA电视提要。最大更新频率为每45秒一次。因此,检索最新的静止帧就像从提要的URL下载JPEG图像一样简单。原始图像被垂直拉伸,看起来很奇怪。因此,该程序可以通过压缩图像并生成未失真的16:9版本来校正纵横比。Python因为PySimpleGUI需要安装Python3.6版本。第三方库Pillow:图像处理PySimpleGUI:GUI框架(Spacestills使用Tkinter后端)Request:HTTP请求完整代码fromioimportBytesIOfromdatetimeimportdatetime,timedeltafrompathlibimportPathimportrequestsfromrequests.exceptionsimportTimeoutfromPILimportImageimportPySimpleGUIassgFEED_URL='https://science.ksc.nasa.gov/shuttle/countdown/video/chan2large.jpg'#Framesizewithoutandwith16:9aspectratiocorrectionWIDTH=704HEIGHT=480HEIGHT_16_9=396#Minimum,default,andmaximumautoreloadintervalinsecondsMIN_DELTA=45DELTA=MIN_DELTAMAX_DELTA=300classStillFrame():"""Holdsastillframe.TheimageisstoredasaPNGIL.Imageandkeptin-image-Attribute:-PNG格式。PIL.ImageAstillframeoriginal:PIL.ImageOriginalframewithwhichtheinstanceisinitialized,cachedincaseofresizingtotheoriginalsizeMethods------bytes:Returntherawbytesresize:Resizethescreenshotnew_size:Calculatenewaspectratio"""def__init__(self,image):"""ConverttheimagetoPNG并缓存转换后的原始文件.Parameters--------image:PIL.ImageImagetostore"""self.image=imageself._topng()selfself.original=self.imagedef_topng(self):"""ConvertimageformatoofframetoPNG.Returns-------StillFrameFramewithimageinPNGformat"""ifnotself.image.format=='PNG':png_file=BytesIO()self.image.save(png_file,'png')png_file.seek(0)png_image=Image.open(png_file)self.image=png_imagereturnsselfdefbytes(self):"""Returnrawbytesoframeimage.Returns------bytesBytestreamoftheframeimage"""file=BytesIO()self.image.save(file,'png')file.seek(0)returnfile.read()defnew_size(self):"""Returnimagesizetoggledbetweenoriginaland16:9.Returns------2-tupleNewsize"""size=self.image.sizeoriginal_size=self.original.sizenew_size=(WIDTH,HEIGHT_16_9)ifsize==original_sizeelse(WIDTH,HEIGHT)returnnew_sizedefresize(self,new_size):"""Resizeframeimage.Parameters----------new_size:2-tupleNewsizeReturns------StillFrameFramewithimageresized"""ifnot(self.image.size==new_size):selfself.image=self.image.resize(new_size)returnsselfdefmake_blank_image(size=(WIDTH,HEIGHT)):"""Createablankimagewithabluebackground.Parameters--------size:2-tupleImagesizeReturns--------PIL.ImageBlankimage"""image=Image.new('RGB',sizesize=size,color='blue')returnimagedefdownload_image(url):"""DownloadcurrentNASATVimage.Parameters----------url:strURLtodownloadtheimagefromReturns------PIL.ImageDownloadedimageifnoerrors,otherwiseblankimage"""try:response=requests.get(url,超时=(0.5,0.5))ifresponse.status_code==200:image=Image.open(BytesIO(response.content))else:image=make_blank_image()exceptTimeout:image=make_blank_image()returnimagedefrefresh(window,resize=False,feed=FEED_URL):"""Displaythelateststillframeinwindow.Parameters---------window:sg.WindowWindowtodisplaythestilltofeed:stringFeedURLReturns------StillFrameRefreshedscreenshot"""still=StillFrame(download_image(feed))ifresize:still=change_aspect_ratio(window,still,new_size=(WIDTH,HEIGHT_16_9))else:window['-IMAGE-'].update(data=still.bytes())returnsstilldefchange_aspect_ratio(window,still,new_size=(WIDTH,HEIGHT_16_9)):"""Changetheaspectratioofthestilldisplayedinwindow.Parameters---------window:sg.WindowWindowcontainingthestillnew_size:2的变化-tupleNewsizeofthestillReturns------StillFrameFramecontainingtheresizedimage"""resized_still=still.resize(new_size)window['-IMAGE-'].update(data=resized_still.bytes())returnresized_stilldefsave(still,path):"""Savestilltoafile.Parameters--------still:StillFrameStilltosavepath:stringFilenameReturns------BooleanTrueiffilesavedwithnoerrors"""filename=Path(path)try:withopen(filename,'wb')asfile:file.write(still.bytes())saved=TrueexceptOSError:saved=Falsereturnsaveddefnext_timeout(delta):"""Returnthemomentintimerightnow+deltasecondsfromnow.Parameters--------delta:intTimeinsecondsuntilthenexttimeoutReturns------datetime.datetimeMomentintimeofthenexttimeout"""rightnow=datetime.now()returnrightnow+timedelta(seconds=delta)deftimeout_due(next_timeout):"""ReturnTrueifthenexttimeoutisdue.Parameters----------next_timeout:datetime.datetimeReturns------boolTrueifthenexttimeoutisdue"""rightnow=datetime.now()returnrightnow>=next_timeoutdefvalidate_delta(value):"""Checkifvalueisanintwithintheproperrangeforatimedelta.Parameters----------value:intTimeinsecondsuntilthenexttimeoutReturns------intTimeinsecondssuntilthenexttimeoutboolTrueiftheargumentisavalidtimedelta"""isinteger=Falsetry:isinteger(value)=type(intisexception):delta=DELTAdelta=int(value)ifisintegerelsedeltaisvalid=MIN_DELTA<=delta<=MAX_DELTAdeltadelta=deltaifisvalidelseDELTAreturndelta,isintegerandisvalidLAYOUT=[[sg.Image(key='-IMAGE-')],[sg.Checkbox('Correctaspectratio',key='-RESIZE-',enable_events=True),sg.Button('重新加载',key='-RELOAD-'),sg.Button('Save',key='-SAVE-'),sg.Exit()],[sg.Checkbox('Auto-reloadevery(seconds):',key='-AUTORELOAD-',default=True),sg.Input(DELTA,key='-DELTA-',size=(3,1),justification='right'),sg.Button('Set',key='-UPDATE_DELTA-')]]defmain(layout):"""Runeventloop.""window=sg.Window('Spacestills',layout,finalize=True)current_still=refresh(window)delta=DELTAnext_reload_time=datetime.now()+timedelta(seconds=delta)whileTrue:event,values=window.read(timeout=100)ifeventin(sg.WIN_CLOSED,'Exit'):breakelif((event=='-RELOAD-')or(values['-AUTORELOAD-']andtimeout_due(next_reload_time))):current_still=refresh(window,values['-RESIZE-'])ifvalues['-AUTORELOAD-']:next_reload_time=next_timeout(delta)elifevent=='-调整大小-':current_still=change_aspect_ratio(window,current_still,current_still.new_size())elifevent=='-SAVE-':filename=sg.popup_get_file('Filename',file_types=[('PNG','*.png')],save_as=True,title='Saveimage',default_extension='.png')iffilename:savesaved=save(current_still,filename)ifnotsaved:sg.popup_ok('Errorwhilesavingfile:',filename,title='Error')elifevent=='-UPDATE_DELTA-':#Thecurrentcycleshouldcompleteatthealreadyscheduledtime.So#don'tupdatenext_reload_timeyetbecauseit'llbetakencareofatthe#next-AUTORELOAD-or-RELOAD-event.delta,valid=validate_delta(values['-DELTA-'])ifnotvalid:window['-DELTA-'].更新(str(DELTA))window.close()delwindowif__name__=='__main__':main(LAYOUT)
