| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632663366346635663666376638663966406641664266436644664566466647664866496650665166526653665466556656665766586659666066616662666366646665666666676668666966706671667266736674667566766677667866796680668166826683668466856686668766886689669066916692669366946695669666976698669967006701670267036704670567066707670867096710671167126713671467156716671767186719672067216722672367246725672667276728672967306731673267336734673567366737673867396740674167426743674467456746674767486749675067516752675367546755675667576758675967606761676267636764676567666767676867696770677167726773677467756776677767786779678067816782678367846785678667876788678967906791679267936794679567966797679867996800680168026803680468056806680768086809681068116812681368146815681668176818681968206821682268236824682568266827682868296830683168326833683468356836683768386839684068416842684368446845684668476848684968506851685268536854685568566857685868596860686168626863686468656866686768686869687068716872687368746875687668776878687968806881688268836884688568866887688868896890689168926893689468956896689768986899690069016902690369046905690669076908690969106911691269136914691569166917691869196920692169226923692469256926692769286929693069316932693369346935693669376938693969406941694269436944694569466947694869496950695169526953695469556956695769586959696069616962696369646965696669676968696969706971697269736974697569766977697869796980698169826983698469856986698769886989699069916992699369946995699669976998699970007001700270037004700570067007700870097010701170127013701470157016701770187019702070217022702370247025702670277028702970307031703270337034703570367037703870397040704170427043704470457046704770487049705070517052705370547055705670577058705970607061706270637064706570667067706870697070707170727073707470757076707770787079708070817082708370847085708670877088708970907091709270937094709570967097709870997100710171027103710471057106710771087109711071117112711371147115711671177118711971207121712271237124712571267127712871297130713171327133713471357136713771387139714071417142714371447145714671477148714971507151715271537154715571567157715871597160716171627163716471657166716771687169717071717172717371747175717671777178717971807181718271837184718571867187718871897190719171927193719471957196719771987199720072017202720372047205720672077208720972107211721272137214721572167217721872197220722172227223722472257226722772287229723072317232723372347235723672377238723972407241724272437244724572467247724872497250725172527253725472557256725772587259726072617262726372647265726672677268726972707271727272737274727572767277727872797280728172827283728472857286728772887289729072917292729372947295729672977298729973007301730273037304730573067307730873097310731173127313731473157316731773187319732073217322732373247325732673277328732973307331733273337334733573367337733873397340734173427343734473457346734773487349735073517352735373547355735673577358735973607361736273637364736573667367736873697370737173727373737473757376737773787379738073817382738373847385738673877388738973907391739273937394739573967397739873997400740174027403740474057406740774087409741074117412741374147415741674177418741974207421742274237424742574267427742874297430743174327433743474357436743774387439744074417442744374447445744674477448744974507451745274537454745574567457745874597460746174627463746474657466746774687469747074717472747374747475747674777478747974807481748274837484748574867487748874897490749174927493749474957496749774987499750075017502750375047505750675077508750975107511751275137514751575167517751875197520752175227523752475257526752775287529753075317532753375347535753675377538753975407541754275437544754575467547754875497550755175527553755475557556755775587559756075617562756375647565756675677568756975707571757275737574757575767577757875797580758175827583758475857586758775887589759075917592759375947595759675977598759976007601760276037604760576067607760876097610761176127613761476157616761776187619762076217622762376247625762676277628762976307631763276337634763576367637763876397640764176427643764476457646764776487649765076517652765376547655765676577658765976607661766276637664766576667667766876697670767176727673767476757676767776787679768076817682768376847685768676877688768976907691769276937694769576967697769876997700770177027703770477057706770777087709771077117712771377147715771677177718771977207721772277237724772577267727772877297730773177327733773477357736773777387739774077417742774377447745774677477748774977507751775277537754775577567757775877597760776177627763776477657766776777687769777077717772777377747775777677777778777977807781778277837784778577867787778877897790779177927793779477957796779777987799780078017802780378047805780678077808780978107811781278137814781578167817781878197820782178227823782478257826782778287829783078317832783378347835783678377838783978407841784278437844784578467847784878497850785178527853785478557856785778587859786078617862786378647865786678677868786978707871787278737874787578767877787878797880788178827883788478857886788778887889789078917892789378947895789678977898789979007901790279037904790579067907790879097910791179127913791479157916791779187919792079217922792379247925792679277928792979307931793279337934793579367937793879397940794179427943794479457946794779487949795079517952795379547955795679577958795979607961796279637964796579667967796879697970797179727973797479757976797779787979798079817982798379847985798679877988798979907991799279937994799579967997799879998000800180028003800480058006800780088009801080118012801380148015801680178018801980208021802280238024802580268027802880298030803180328033803480358036803780388039804080418042804380448045804680478048804980508051805280538054805580568057805880598060806180628063806480658066806780688069807080718072807380748075807680778078807980808081808280838084808580868087808880898090809180928093809480958096809780988099810081018102810381048105810681078108810981108111811281138114811581168117811881198120812181228123812481258126812781288129813081318132813381348135813681378138813981408141814281438144814581468147814881498150815181528153815481558156815781588159816081618162816381648165816681678168816981708171817281738174817581768177817881798180818181828183818481858186818781888189819081918192819381948195819681978198819982008201820282038204820582068207820882098210821182128213821482158216821782188219822082218222822382248225822682278228822982308231823282338234823582368237823882398240824182428243824482458246824782488249825082518252825382548255825682578258825982608261826282638264826582668267826882698270827182728273827482758276827782788279828082818282828382848285828682878288828982908291829282938294829582968297829882998300830183028303830483058306830783088309831083118312831383148315831683178318831983208321832283238324832583268327832883298330833183328333833483358336833783388339834083418342834383448345834683478348834983508351835283538354835583568357835883598360836183628363836483658366836783688369837083718372837383748375837683778378837983808381838283838384838583868387838883898390839183928393839483958396839783988399840084018402840384048405840684078408840984108411841284138414841584168417841884198420842184228423842484258426842784288429843084318432843384348435843684378438843984408441844284438444844584468447844884498450845184528453845484558456845784588459846084618462846384648465846684678468846984708471847284738474847584768477847884798480848184828483848484858486848784888489849084918492849384948495849684978498849985008501850285038504850585068507850885098510851185128513851485158516851785188519852085218522852385248525852685278528852985308531853285338534853585368537853885398540854185428543854485458546854785488549855085518552855385548555855685578558855985608561856285638564856585668567856885698570857185728573857485758576857785788579858085818582858385848585858685878588858985908591859285938594859585968597859885998600860186028603860486058606860786088609861086118612861386148615861686178618861986208621862286238624862586268627862886298630863186328633863486358636863786388639864086418642864386448645864686478648864986508651865286538654865586568657865886598660866186628663866486658666866786688669867086718672867386748675867686778678867986808681868286838684868586868687868886898690869186928693869486958696869786988699870087018702870387048705870687078708870987108711871287138714871587168717871887198720872187228723872487258726872787288729873087318732873387348735873687378738873987408741874287438744874587468747874887498750875187528753875487558756875787588759876087618762876387648765876687678768876987708771877287738774877587768777877887798780878187828783878487858786878787888789879087918792879387948795879687978798879988008801880288038804880588068807880888098810881188128813881488158816881788188819882088218822882388248825882688278828882988308831883288338834883588368837883888398840884188428843884488458846884788488849885088518852885388548855885688578858885988608861886288638864886588668867886888698870887188728873887488758876887788788879888088818882888388848885888688878888888988908891889288938894889588968897889888998900890189028903890489058906890789088909891089118912891389148915891689178918891989208921892289238924892589268927892889298930893189328933893489358936893789388939894089418942894389448945894689478948894989508951895289538954895589568957895889598960896189628963896489658966896789688969897089718972897389748975897689778978897989808981898289838984898589868987898889898990899189928993899489958996899789988999900090019002900390049005900690079008900990109011901290139014901590169017901890199020902190229023902490259026902790289029903090319032903390349035903690379038903990409041904290439044904590469047904890499050905190529053905490559056905790589059906090619062906390649065906690679068906990709071907290739074907590769077907890799080908190829083908490859086908790889089909090919092909390949095909690979098909991009101910291039104910591069107910891099110911191129113911491159116911791189119912091219122912391249125912691279128912991309131913291339134913591369137913891399140914191429143914491459146914791489149915091519152915391549155915691579158915991609161916291639164916591669167916891699170917191729173917491759176917791789179918091819182918391849185918691879188918991909191919291939194919591969197919891999200920192029203920492059206920792089209921092119212921392149215921692179218921992209221922292239224922592269227922892299230923192329233923492359236923792389239924092419242924392449245924692479248924992509251925292539254925592569257925892599260926192629263926492659266926792689269927092719272927392749275927692779278927992809281928292839284928592869287928892899290929192929293929492959296929792989299930093019302930393049305930693079308930993109311931293139314931593169317931893199320932193229323932493259326932793289329933093319332933393349335933693379338933993409341934293439344934593469347934893499350935193529353935493559356935793589359936093619362936393649365936693679368936993709371937293739374937593769377937893799380938193829383938493859386938793889389939093919392939393949395939693979398939994009401940294039404940594069407940894099410941194129413941494159416941794189419942094219422942394249425942694279428942994309431943294339434943594369437943894399440944194429443944494459446944794489449945094519452945394549455945694579458945994609461946294639464946594669467946894699470947194729473947494759476947794789479948094819482948394849485948694879488948994909491949294939494949594969497949894999500950195029503950495059506950795089509951095119512951395149515951695179518951995209521952295239524952595269527952895299530953195329533953495359536953795389539954095419542954395449545954695479548954995509551955295539554955595569557955895599560956195629563956495659566956795689569957095719572957395749575957695779578957995809581958295839584958595869587958895899590959195929593959495959596959795989599960096019602960396049605960696079608960996109611961296139614961596169617961896199620962196229623962496259626962796289629963096319632963396349635963696379638963996409641964296439644964596469647964896499650965196529653965496559656965796589659966096619662966396649665966696679668966996709671967296739674967596769677967896799680968196829683968496859686968796889689969096919692969396949695969696979698969997009701970297039704970597069707970897099710971197129713971497159716971797189719972097219722972397249725972697279728972997309731973297339734973597369737973897399740974197429743974497459746974797489749975097519752975397549755975697579758975997609761976297639764976597669767976897699770977197729773977497759776977797789779978097819782978397849785978697879788978997909791979297939794979597969797979897999800980198029803980498059806980798089809981098119812981398149815981698179818981998209821982298239824982598269827982898299830983198329833983498359836983798389839984098419842984398449845984698479848984998509851985298539854985598569857985898599860986198629863986498659866986798689869987098719872987398749875987698779878987998809881988298839884988598869887988898899890989198929893989498959896989798989899990099019902990399049905990699079908990999109911991299139914991599169917991899199920992199229923992499259926992799289929993099319932993399349935993699379938993999409941994299439944994599469947994899499950995199529953995499559956995799589959996099619962996399649965996699679968996999709971997299739974997599769977997899799980998199829983998499859986998799889989999099919992999399949995999699979998999910000100011000210003100041000510006100071000810009100101001110012100131001410015100161001710018100191002010021100221002310024100251002610027100281002910030100311003210033100341003510036100371003810039100401004110042100431004410045100461004710048100491005010051100521005310054100551005610057100581005910060100611006210063100641006510066100671006810069100701007110072100731007410075100761007710078100791008010081100821008310084100851008610087100881008910090100911009210093100941009510096100971009810099101001010110102101031010410105101061010710108101091011010111101121011310114101151011610117101181011910120101211012210123101241012510126101271012810129101301013110132101331013410135101361013710138101391014010141101421014310144101451014610147101481014910150101511015210153101541015510156101571015810159101601016110162101631016410165101661016710168101691017010171101721017310174101751017610177101781017910180101811018210183101841018510186101871018810189101901019110192101931019410195101961019710198101991020010201102021020310204102051020610207102081020910210102111021210213102141021510216102171021810219102201022110222102231022410225102261022710228102291023010231102321023310234102351023610237102381023910240102411024210243102441024510246102471024810249102501025110252102531025410255102561025710258102591026010261102621026310264102651026610267102681026910270102711027210273102741027510276102771027810279102801028110282102831028410285102861028710288102891029010291102921029310294102951029610297102981029910300103011030210303103041030510306103071030810309103101031110312103131031410315103161031710318103191032010321103221032310324103251032610327103281032910330103311033210333103341033510336103371033810339103401034110342103431034410345103461034710348103491035010351103521035310354103551035610357103581035910360103611036210363103641036510366103671036810369103701037110372103731037410375103761037710378103791038010381103821038310384103851038610387103881038910390103911039210393103941039510396103971039810399104001040110402104031040410405104061040710408104091041010411104121041310414104151041610417104181041910420104211042210423104241042510426104271042810429104301043110432104331043410435104361043710438104391044010441104421044310444104451044610447104481044910450104511045210453104541045510456104571045810459104601046110462104631046410465104661046710468104691047010471104721047310474104751047610477104781047910480104811048210483104841048510486104871048810489104901049110492104931049410495104961049710498104991050010501105021050310504105051050610507105081050910510105111051210513105141051510516105171051810519105201052110522105231052410525105261052710528105291053010531105321053310534105351053610537105381053910540105411054210543105441054510546105471054810549105501055110552105531055410555105561055710558105591056010561105621056310564105651056610567105681056910570105711057210573105741057510576105771057810579105801058110582105831058410585105861058710588105891059010591105921059310594105951059610597105981059910600106011060210603106041060510606106071060810609106101061110612106131061410615106161061710618106191062010621106221062310624106251062610627106281062910630106311063210633106341063510636106371063810639106401064110642106431064410645106461064710648106491065010651106521065310654106551065610657106581065910660106611066210663106641066510666106671066810669106701067110672106731067410675 |
- #! /usr/bin/env python3
- # -*- coding: utf-8 -*-
- # tifffile.py
- # Copyright (c) 2008-2018, Christoph Gohlke
- # Copyright (c) 2008-2018, The Regents of the University of California
- # Produced at the Laboratory for Fluorescence Dynamics
- # All rights reserved.
- #
- # Redistribution and use in source and binary forms, with or without
- # modification, are permitted provided that the following conditions are met:
- #
- # * Redistributions of source code must retain the above copyright
- # notice, this list of conditions and the following disclaimer.
- # * Redistributions in binary form must reproduce the above copyright
- # notice, this list of conditions and the following disclaimer in the
- # documentation and/or other materials provided with the distribution.
- # * Neither the name of the copyright holders nor the names of any
- # contributors may be used to endorse or promote products derived
- # from this software without specific prior written permission.
- #
- # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
- # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- # POSSIBILITY OF SUCH DAMAGE.
- """Read image and meta data from (bio) TIFF(R) files. Save numpy arrays as TIFF.
- Image and metadata can be read from TIFF, BigTIFF, OME-TIFF, STK, LSM, NIH,
- SGI, ImageJ, MicroManager, FluoView, ScanImage, SEQ, GEL, and GeoTIFF files.
- Tifffile is not a general-purpose TIFF library.
- Only a subset of the TIFF specification is supported, mainly uncompressed and
- losslessly compressed 1, 8, 16, 32 and 64 bit integer, 16, 32 and 64-bit float,
- grayscale and RGB(A) images, which are commonly used in scientific imaging.
- Specifically, reading slices of image data, image trees defined via SubIFDs,
- CCITT and OJPEG compression, chroma subsampling without JPEG compression,
- or IPTC and XMP metadata are not implemented.
- TIFF(R), the tagged Image File Format, is a trademark and under control of
- Adobe Systems Incorporated. BigTIFF allows for files greater than 4 GB.
- STK, LSM, FluoView, SGI, SEQ, GEL, and OME-TIFF, are custom extensions
- defined by Molecular Devices (Universal Imaging Corporation), Carl Zeiss
- MicroImaging, Olympus, Silicon Graphics International, Media Cybernetics,
- Molecular Dynamics, and the Open Microscopy Environment consortium
- respectively.
- For command line usage run C{python -m tifffile --help}
- :Author:
- `Christoph Gohlke <https://www.lfd.uci.edu/~gohlke/>`_
- :Organization:
- Laboratory for Fluorescence Dynamics, University of California, Irvine
- :Version: 2018.06.15
- Requirements
- ------------
- * `CPython 3.6 64-bit <https://www.python.org>`_
- * `Numpy 1.14 <http://www.numpy.org>`_
- * `Matplotlib 2.2 <https://www.matplotlib.org>`_ (optional for plotting)
- * `Tifffile.c 2018.02.10 <https://www.lfd.uci.edu/~gohlke/>`_
- (recommended for faster decoding of PackBits and LZW encoded strings)
- * `Tifffile_geodb.py 2018.02.10 <https://www.lfd.uci.edu/~gohlke/>`_
- (optional enums for GeoTIFF metadata)
- * Python 2 requires 'futures', 'enum34', 'pathlib'.
- Revisions
- ---------
- 2018.06.15
- Pass 2680 tests.
- Towards reading JPEG and other compressions via imagecodecs package (WIP).
- Add function to validate TIFF using 'jhove -m TIFF-hul'.
- Save bool arrays as bilevel TIFF.
- Accept pathlib.Path as filenames.
- Move 'software' argument from TiffWriter __init__ to save.
- Raise DOS limit to 16 TB.
- Lazy load lzma and zstd compressors and decompressors.
- Add option to save IJMetadata tags.
- Return correct number of pages for truncated series (bug fix).
- Move EXIF tags to TIFF.TAG as per TIFF/EP standard.
- 2018.02.18
- Pass 2293 tests.
- Always save RowsPerStrip and Resolution tags as required by TIFF standard.
- Do not use badly typed ImageDescription.
- Coherce bad ASCII string tags to bytes.
- Tuning of __str__ functions.
- Fix reading 'undefined' tag values (bug fix).
- Read and write ZSTD compressed data.
- Use hexdump to print byte strings.
- Determine TIFF byte order from data dtype in imsave.
- Add option to specify RowsPerStrip for compressed strips.
- Allow memory map of arrays with non-native byte order.
- Attempt to handle ScanImage <= 5.1 files.
- Restore TiffPageSeries.pages sequence interface.
- Use numpy.frombuffer instead of fromstring to read from binary data.
- Parse GeoTIFF metadata.
- Add option to apply horizontal differencing before compression.
- Towards reading PerkinElmer QPTIFF (no test files).
- Do not index out of bounds data in tifffile.c unpackbits and decodelzw.
- 2017.09.29 (tentative)
- Many backwards incompatible changes improving speed and resource usage:
- Pass 2268 tests.
- Add detail argument to __str__ function. Remove info functions.
- Fix potential issue correcting offsets of large LSM files with positions.
- Remove TiffFile sequence interface; use TiffFile.pages instead.
- Do not make tag values available as TiffPage attributes.
- Use str (not bytes) type for tag and metadata strings (WIP).
- Use documented standard tag and value names (WIP).
- Use enums for some documented TIFF tag values.
- Remove 'memmap' and 'tmpfile' options; use out='memmap' instead.
- Add option to specify output in asarray functions.
- Add option to concurrently decode image strips or tiles using threads.
- Add TiffPage.asrgb function (WIP).
- Do not apply colormap in asarray.
- Remove 'colormapped', 'rgbonly', and 'scale_mdgel' options from asarray.
- Consolidate metadata in TiffFile _metadata functions.
- Remove non-tag metadata properties from TiffPage.
- Add function to convert LSM to tiled BIN files.
- Align image data in file.
- Make TiffPage.dtype a numpy.dtype.
- Add 'ndim' and 'size' properties to TiffPage and TiffPageSeries.
- Allow imsave to write non-BigTIFF files up to ~4 GB.
- Only read one page for shaped series if possible.
- Add memmap function to create memory-mapped array stored in TIFF file.
- Add option to save empty arrays to TIFF files.
- Add option to save truncated TIFF files.
- Allow single tile images to be saved contiguously.
- Add optional movie mode for files with uniform pages.
- Lazy load pages.
- Use lightweight TiffFrame for IFDs sharing properties with key TiffPage.
- Move module constants to 'TIFF' namespace (speed up module import).
- Remove 'fastij' option from TiffFile.
- Remove 'pages' parameter from TiffFile.
- Remove TIFFfile alias.
- Deprecate Python 2.
- Require enum34 and futures packages on Python 2.7.
- Remove Record class and return all metadata as dict instead.
- Add functions to parse STK, MetaSeries, ScanImage, SVS, Pilatus metadata.
- Read tags from EXIF and GPS IFDs.
- Use pformat for tag and metadata values.
- Fix reading some UIC tags (bug fix).
- Do not modify input array in imshow (bug fix).
- Fix Python implementation of unpack_ints.
- 2017.05.23
- Pass 1961 tests.
- Write correct number of SampleFormat values (bug fix).
- Use Adobe deflate code to write ZIP compressed files.
- Add option to pass tag values as packed binary data for writing.
- Defer tag validation to attribute access.
- Use property instead of lazyattr decorator for simple expressions.
- 2017.03.17
- Write IFDs and tag values on word boundaries.
- Read ScanImage metadata.
- Remove is_rgb and is_indexed attributes from TiffFile.
- Create files used by doctests.
- 2017.01.12
- Read Zeiss SEM metadata.
- Read OME-TIFF with invalid references to external files.
- Rewrite C LZW decoder (5x faster).
- Read corrupted LSM files missing EOI code in LZW stream.
- 2017.01.01
- Add option to append images to existing TIFF files.
- Read files without pages.
- Read S-FEG and Helios NanoLab tags created by FEI software.
- Allow saving Color Filter Array (CFA) images.
- Add info functions returning more information about TiffFile and TiffPage.
- Add option to read specific pages only.
- Remove maxpages argument (backwards incompatible).
- Remove test_tifffile function.
- 2016.10.28
- Pass 1944 tests.
- Improve detection of ImageJ hyperstacks.
- Read TVIPS metadata created by EM-MENU (by Marco Oster).
- Add option to disable using OME-XML metadata.
- Allow non-integer range attributes in modulo tags (by Stuart Berg).
- 2016.06.21
- Do not always memmap contiguous data in page series.
- 2016.05.13
- Add option to specify resolution unit.
- Write grayscale images with extra samples when planarconfig is specified.
- Do not write RGB color images with 2 samples.
- Reorder TiffWriter.save keyword arguments (backwards incompatible).
- 2016.04.18
- Pass 1932 tests.
- TiffWriter, imread, and imsave accept open binary file streams.
- 2016.04.13
- Correctly handle reversed fill order in 2 and 4 bps images (bug fix).
- Implement reverse_bitorder in C.
- 2016.03.18
- Fix saving additional ImageJ metadata.
- 2016.02.22
- Pass 1920 tests.
- Write 8 bytes double tag values using offset if necessary (bug fix).
- Add option to disable writing second image description tag.
- Detect tags with incorrect counts.
- Disable color mapping for LSM.
- 2015.11.13
- Read LSM 6 mosaics.
- Add option to specify directory of memory-mapped files.
- Add command line options to specify vmin and vmax values for colormapping.
- 2015.10.06
- New helper function to apply colormaps.
- Renamed is_palette attributes to is_indexed (backwards incompatible).
- Color-mapped samples are now contiguous (backwards incompatible).
- Do not color-map ImageJ hyperstacks (backwards incompatible).
- Towards reading Leica SCN.
- 2015.09.25
- Read images with reversed bit order (FillOrder is LSB2MSB).
- 2015.09.21
- Read RGB OME-TIFF.
- Warn about malformed OME-XML.
- 2015.09.16
- Detect some corrupted ImageJ metadata.
- Better axes labels for 'shaped' files.
- Do not create TiffTag for default values.
- Chroma subsampling is not supported.
- Memory-map data in TiffPageSeries if possible (optional).
- 2015.08.17
- Pass 1906 tests.
- Write ImageJ hyperstacks (optional).
- Read and write LZMA compressed data.
- Specify datetime when saving (optional).
- Save tiled and color-mapped images (optional).
- Ignore void bytecounts and offsets if possible.
- Ignore bogus image_depth tag created by ISS Vista software.
- Decode floating point horizontal differencing (not tiled).
- Save image data contiguously if possible.
- Only read first IFD from ImageJ files if possible.
- Read ImageJ 'raw' format (files larger than 4 GB).
- TiffPageSeries class for pages with compatible shape and data type.
- Try to read incomplete tiles.
- Open file dialog if no filename is passed on command line.
- Ignore errors when decoding OME-XML.
- Rename decoder functions (backwards incompatible).
- 2014.08.24
- TiffWriter class for incremental writing images.
- Simplify examples.
- 2014.08.19
- Add memmap function to FileHandle.
- Add function to determine if image data in TiffPage is memory-mappable.
- Do not close files if multifile_close parameter is False.
- 2014.08.10
- Pass 1730 tests.
- Return all extrasamples by default (backwards incompatible).
- Read data from series of pages into memory-mapped array (optional).
- Squeeze OME dimensions (backwards incompatible).
- Workaround missing EOI code in strips.
- Support image and tile depth tags (SGI extension).
- Better handling of STK/UIC tags (backwards incompatible).
- Disable color mapping for STK.
- Julian to datetime converter.
- TIFF ASCII type may be NULL separated.
- Unwrap strip offsets for LSM files greater than 4 GB.
- Correct strip byte counts in compressed LSM files.
- Skip missing files in OME series.
- Read embedded TIFF files.
- 2014.02.05
- Save rational numbers as type 5 (bug fix).
- 2013.12.20
- Keep other files in OME multi-file series closed.
- FileHandle class to abstract binary file handle.
- Disable color mapping for bad OME-TIFF produced by bio-formats.
- Read bad OME-XML produced by ImageJ when cropping.
- 2013.11.03
- Allow zlib compress data in imsave function (optional).
- Memory-map contiguous image data (optional).
- 2013.10.28
- Read MicroManager metadata and little-endian ImageJ tag.
- Save extra tags in imsave function.
- Save tags in ascending order by code (bug fix).
- 2012.10.18
- Accept file like objects (read from OIB files).
- 2012.08.21
- Rename TIFFfile to TiffFile and TIFFpage to TiffPage.
- TiffSequence class for reading sequence of TIFF files.
- Read UltraQuant tags.
- Allow float numbers as resolution in imsave function.
- 2012.08.03
- Read MD GEL tags and NIH Image header.
- 2012.07.25
- Read ImageJ tags.
- ...
- Notes
- -----
- The API is not stable yet and might change between revisions.
- Tested on little-endian platforms only.
- Other Python packages and modules for reading (bio) scientific TIFF files:
- * `python-bioformats <https://github.com/CellProfiler/python-bioformats>`_
- * `Imread <https://github.com/luispedro/imread>`_
- * `PyLibTiff <https://github.com/pearu/pylibtiff>`_
- * `ITK <https://www.itk.org>`_
- * `PyLSM <https://launchpad.net/pylsm>`_
- * `PyMca.TiffIO.py <https://github.com/vasole/pymca>`_ (same as fabio.TiffIO)
- * `BioImageXD.Readers <http://www.bioimagexd.net/>`_
- * `Cellcognition.io <http://cellcognition.org/>`_
- * `pymimage <https://github.com/ardoi/pymimage>`_
- * `pytiff <https://github.com/FZJ-INM1-BDA/pytiff>`_
- Acknowledgements
- ----------------
- * Egor Zindy, University of Manchester, for lsm_scan_info specifics.
- * Wim Lewis for a bug fix and some LSM functions.
- * Hadrien Mary for help on reading MicroManager files.
- * Christian Kliche for help writing tiled and color-mapped files.
- References
- ----------
- 1) TIFF 6.0 Specification and Supplements. Adobe Systems Incorporated.
- http://partners.adobe.com/public/developer/tiff/
- 2) TIFF File Format FAQ. http://www.awaresystems.be/imaging/tiff/faq.html
- 3) MetaMorph Stack (STK) Image File Format.
- http://support.meta.moleculardevices.com/docs/t10243.pdf
- 4) Image File Format Description LSM 5/7 Release 6.0 (ZEN 2010).
- Carl Zeiss MicroImaging GmbH. BioSciences. May 10, 2011
- 5) The OME-TIFF format.
- http://www.openmicroscopy.org/site/support/file-formats/ome-tiff
- 6) UltraQuant(r) Version 6.0 for Windows Start-Up Guide.
- http://www.ultralum.com/images%20ultralum/pdf/UQStart%20Up%20Guide.pdf
- 7) Micro-Manager File Formats.
- http://www.micro-manager.org/wiki/Micro-Manager_File_Formats
- 8) Tags for TIFF and Related Specifications. Digital Preservation.
- http://www.digitalpreservation.gov/formats/content/tiff_tags.shtml
- 9) ScanImage BigTiff Specification - ScanImage 2016.
- http://scanimage.vidriotechnologies.com/display/SI2016/
- ScanImage+BigTiff+Specification
- 10) CIPA DC-008-2016: Exchangeable image file format for digital still cameras:
- Exif Version 2.31.
- http://www.cipa.jp/std/documents/e/DC-008-Translation-2016-E.pdf
- Examples
- --------
- >>> # write numpy array to TIFF file
- >>> data = numpy.random.rand(4, 301, 219)
- >>> imsave('temp.tif', data, photometric='minisblack')
- >>> # read numpy array from TIFF file
- >>> image = imread('temp.tif')
- >>> numpy.testing.assert_array_equal(image, data)
- >>> # iterate over pages and tags in TIFF file
- >>> with TiffFile('temp.tif') as tif:
- ... images = tif.asarray()
- ... for page in tif.pages:
- ... for tag in page.tags.values():
- ... _ = tag.name, tag.value
- ... image = page.asarray()
- """
- from __future__ import division, print_function
- import sys
- import os
- import io
- import re
- import glob
- import math
- import zlib
- import time
- import json
- import enum
- import struct
- import pathlib
- import warnings
- import binascii
- import tempfile
- import datetime
- import threading
- import collections
- import multiprocessing
- import concurrent.futures
- import numpy
- # delay imports: mmap, pprint, fractions, xml, tkinter, matplotlib, lzma, zstd,
- # subprocess
- __version__ = "2018.06.15"
- __docformat__ = "restructuredtext en"
- __all__ = (
- "imsave",
- "imread",
- "imshow",
- "memmap",
- "TiffFile",
- "TiffWriter",
- "TiffSequence",
- # utility functions used by oiffile or czifile
- "FileHandle",
- "lazyattr",
- "natural_sorted",
- "decode_lzw",
- "stripnull",
- "create_output",
- "repeat_nd",
- "format_size",
- "product",
- "xml2dict",
- )
- def imread(files, **kwargs):
- """Return image data from TIFF file(s) as numpy array.
- Refer to the TiffFile class and member functions for documentation.
- Parameters
- ----------
- files : str, binary stream, or sequence
- File name, seekable binary stream, glob pattern, or sequence of
- file names.
- kwargs : dict
- Parameters 'multifile' and 'is_ome' are passed to the TiffFile class.
- The 'pattern' parameter is passed to the TiffSequence class.
- Other parameters are passed to the asarray functions.
- The first image series is returned if no arguments are provided.
- Examples
- --------
- >>> # get image from first page
- >>> imsave('temp.tif', numpy.random.rand(3, 4, 301, 219))
- >>> im = imread('temp.tif', key=0)
- >>> im.shape
- (4, 301, 219)
- >>> # get images from sequence of files
- >>> ims = imread(['temp.tif', 'temp.tif'])
- >>> ims.shape
- (2, 3, 4, 301, 219)
- """
- kwargs_file = parse_kwargs(kwargs, "multifile", "is_ome")
- kwargs_seq = parse_kwargs(kwargs, "pattern")
- if isinstance(files, basestring) and any(i in files for i in "?*"):
- files = glob.glob(files)
- if not files:
- raise ValueError("no files found")
- if not hasattr(files, "seek") and len(files) == 1:
- files = files[0]
- if isinstance(files, basestring) or hasattr(files, "seek"):
- with TiffFile(files, **kwargs_file) as tif:
- return tif.asarray(**kwargs)
- else:
- with TiffSequence(files, **kwargs_seq) as imseq:
- return imseq.asarray(**kwargs)
- def imsave(file, data=None, shape=None, dtype=None, bigsize=2**32 - 2**25, **kwargs):
- """Write numpy array to TIFF file.
- Refer to the TiffWriter class and member functions for documentation.
- Parameters
- ----------
- file : str or binary stream
- File name or writable binary stream, such as an open file or BytesIO.
- data : array_like
- Input image. The last dimensions are assumed to be image depth,
- height, width, and samples.
- If None, an empty array of the specified shape and dtype is
- saved to file.
- Unless 'byteorder' is specified in 'kwargs', the TIFF file byte order
- is determined from the data's dtype or the dtype argument.
- shape : tuple
- If 'data' is None, shape of an empty array to save to the file.
- dtype : numpy.dtype
- If 'data' is None, data-type of an empty array to save to the file.
- bigsize : int
- Create a BigTIFF file if the size of data in bytes is larger than
- this threshold and 'imagej' or 'truncate' are not enabled.
- By default, the threshold is 4 GB minus 32 MB reserved for metadata.
- Use the 'bigtiff' parameter to explicitly specify the type of
- file created.
- kwargs : dict
- Parameters 'append', 'byteorder', 'bigtiff', and 'imagej', are passed
- to TiffWriter(). Other parameters are passed to TiffWriter.save().
- Returns
- -------
- If the image data are written contiguously, return offset and bytecount
- of image data in the file.
- Examples
- --------
- >>> # save a RGB image
- >>> data = numpy.random.randint(0, 255, (256, 256, 3), 'uint8')
- >>> imsave('temp.tif', data, photometric='rgb')
- >>> # save a random array and metadata, using compression
- >>> data = numpy.random.rand(2, 5, 3, 301, 219)
- >>> imsave('temp.tif', data, compress=6, metadata={'axes': 'TZCYX'})
- """
- tifargs = parse_kwargs(kwargs, "append", "bigtiff", "byteorder", "imagej")
- if data is None:
- size = product(shape) * numpy.dtype(dtype).itemsize
- byteorder = numpy.dtype(dtype).byteorder
- else:
- try:
- size = data.nbytes
- byteorder = data.dtype.byteorder
- except Exception:
- size = 0
- byteorder = None
- if (
- size > bigsize
- and "bigtiff" not in tifargs
- and not (tifargs.get("imagej", False) or tifargs.get("truncate", False))
- ):
- tifargs["bigtiff"] = True
- if "byteorder" not in tifargs:
- tifargs["byteorder"] = byteorder
- with TiffWriter(file, **tifargs) as tif:
- return tif.save(data, shape, dtype, **kwargs)
- def memmap(filename, shape=None, dtype=None, page=None, series=0, mode="r+", **kwargs):
- """Return memory-mapped numpy array stored in TIFF file.
- Memory-mapping requires data stored in native byte order, without tiling,
- compression, predictors, etc.
- If 'shape' and 'dtype' are provided, existing files will be overwritten or
- appended to depending on the 'append' parameter.
- Otherwise the image data of a specified page or series in an existing
- file will be memory-mapped. By default, the image data of the first page
- series is memory-mapped.
- Call flush() to write any changes in the array to the file.
- Raise ValueError if the image data in the file is not memory-mappable.
- Parameters
- ----------
- filename : str
- Name of the TIFF file which stores the array.
- shape : tuple
- Shape of the empty array.
- dtype : numpy.dtype
- Data-type of the empty array.
- page : int
- Index of the page which image data to memory-map.
- series : int
- Index of the page series which image data to memory-map.
- mode : {'r+', 'r', 'c'}, optional
- The file open mode. Default is to open existing file for reading and
- writing ('r+').
- kwargs : dict
- Additional parameters passed to imsave() or TiffFile().
- Examples
- --------
- >>> # create an empty TIFF file and write to memory-mapped image
- >>> im = memmap('temp.tif', shape=(256, 256), dtype='float32')
- >>> im[255, 255] = 1.0
- >>> im.flush()
- >>> im.shape, im.dtype
- ((256, 256), dtype('float32'))
- >>> del im
- >>> # memory-map image data in a TIFF file
- >>> im = memmap('temp.tif', page=0)
- >>> im[255, 255]
- 1.0
- """
- if shape is not None and dtype is not None:
- # create a new, empty array
- kwargs.update(
- data=None,
- shape=shape,
- dtype=dtype,
- returnoffset=True,
- align=TIFF.ALLOCATIONGRANULARITY,
- )
- result = imsave(filename, **kwargs)
- if result is None:
- # TODO: fail before creating file or writing data
- raise ValueError("image data are not memory-mappable")
- offset = result[0]
- else:
- # use existing file
- with TiffFile(filename, **kwargs) as tif:
- if page is not None:
- page = tif.pages[page]
- if not page.is_memmappable:
- raise ValueError("image data are not memory-mappable")
- offset, _ = page.is_contiguous
- shape = page.shape
- dtype = page.dtype
- else:
- series = tif.series[series]
- if series.offset is None:
- raise ValueError("image data are not memory-mappable")
- shape = series.shape
- dtype = series.dtype
- offset = series.offset
- dtype = tif.byteorder + dtype.char
- return numpy.memmap(filename, dtype, mode, offset, shape, "C")
- class lazyattr(object):
- """Attribute whose value is computed on first access."""
- # TODO: help() doesn't work
- __slots__ = ("func",)
- def __init__(self, func):
- self.func = func
- # self.__name__ = func.__name__
- # self.__doc__ = func.__doc__
- # self.lock = threading.RLock()
- def __get__(self, instance, owner):
- # with self.lock:
- if instance is None:
- return self
- try:
- value = self.func(instance)
- except AttributeError as e:
- raise RuntimeError(e)
- if value is NotImplemented:
- return getattr(super(owner, instance), self.func.__name__)
- setattr(instance, self.func.__name__, value)
- return value
- class TiffWriter(object):
- """Write numpy arrays to TIFF file.
- TiffWriter instances must be closed using the 'close' method, which is
- automatically called when using the 'with' context manager.
- TiffWriter's main purpose is saving nD numpy array's as TIFF,
- not to create any possible TIFF format. Specifically, JPEG compression,
- SubIFDs, ExifIFD, or GPSIFD tags are not supported.
- Examples
- --------
- >>> # successively append images to BigTIFF file
- >>> data = numpy.random.rand(2, 5, 3, 301, 219)
- >>> with TiffWriter('temp.tif', bigtiff=True) as tif:
- ... for i in range(data.shape[0]):
- ... tif.save(data[i], compress=6, photometric='minisblack')
- """
- def __init__(self, file, bigtiff=False, byteorder=None, append=False, imagej=False):
- """Open a TIFF file for writing.
- An empty TIFF file is created if the file does not exist, else the
- file is overwritten with an empty TIFF file unless 'append'
- is true. Use bigtiff=True when creating files larger than 4 GB.
- Parameters
- ----------
- file : str, binary stream, or FileHandle
- File name or writable binary stream, such as an open file
- or BytesIO.
- bigtiff : bool
- If True, the BigTIFF format is used.
- byteorder : {'<', '>', '=', '|'}
- The endianness of the data in the file.
- By default, this is the system's native byte order.
- append : bool
- If True and 'file' is an existing standard TIFF file, image data
- and tags are appended to the file.
- Appending data may corrupt specifically formatted TIFF files
- such as LSM, STK, ImageJ, NIH, or FluoView.
- imagej : bool
- If True, write an ImageJ hyperstack compatible file.
- This format can handle data types uint8, uint16, or float32 and
- data shapes up to 6 dimensions in TZCYXS order.
- RGB images (S=3 or S=4) must be uint8.
- ImageJ's default byte order is big-endian but this implementation
- uses the system's native byte order by default.
- ImageJ does not support BigTIFF format or LZMA compression.
- The ImageJ file format is undocumented.
- """
- if append:
- # determine if file is an existing TIFF file that can be extended
- try:
- with FileHandle(file, mode="rb", size=0) as fh:
- pos = fh.tell()
- try:
- with TiffFile(fh) as tif:
- if append != "force" and any(
- getattr(tif, "is_" + a)
- for a in (
- "lsm",
- "stk",
- "imagej",
- "nih",
- "fluoview",
- "micromanager",
- )
- ):
- raise ValueError("file contains metadata")
- byteorder = tif.byteorder
- bigtiff = tif.is_bigtiff
- self._ifdoffset = tif.pages.next_page_offset
- except Exception as e:
- raise ValueError("cannot append to file: %s" % str(e))
- finally:
- fh.seek(pos)
- except (IOError, FileNotFoundError):
- append = False
- if byteorder in (None, "=", "|"):
- byteorder = "<" if sys.byteorder == "little" else ">"
- elif byteorder not in ("<", ">"):
- raise ValueError("invalid byteorder %s" % byteorder)
- if imagej and bigtiff:
- warnings.warn("writing incompatible BigTIFF ImageJ")
- self._byteorder = byteorder
- self._imagej = bool(imagej)
- self._truncate = False
- self._metadata = None
- self._colormap = None
- self._descriptionoffset = 0
- self._descriptionlen = 0
- self._descriptionlenoffset = 0
- self._tags = None
- self._shape = None # normalized shape of data in consecutive pages
- self._datashape = None # shape of data in consecutive pages
- self._datadtype = None # data type
- self._dataoffset = None # offset to data
- self._databytecounts = None # byte counts per plane
- self._tagoffsets = None # strip or tile offset tag code
- if bigtiff:
- self._bigtiff = True
- self._offsetsize = 8
- self._tagsize = 20
- self._tagnoformat = "Q"
- self._offsetformat = "Q"
- self._valueformat = "8s"
- else:
- self._bigtiff = False
- self._offsetsize = 4
- self._tagsize = 12
- self._tagnoformat = "H"
- self._offsetformat = "I"
- self._valueformat = "4s"
- if append:
- self._fh = FileHandle(file, mode="r+b", size=0)
- self._fh.seek(0, 2)
- else:
- self._fh = FileHandle(file, mode="wb", size=0)
- self._fh.write({"<": b"II", ">": b"MM"}[byteorder])
- if bigtiff:
- self._fh.write(struct.pack(byteorder + "HHH", 43, 8, 0))
- else:
- self._fh.write(struct.pack(byteorder + "H", 42))
- # first IFD
- self._ifdoffset = self._fh.tell()
- self._fh.write(struct.pack(byteorder + self._offsetformat, 0))
- def save(
- self,
- data=None,
- shape=None,
- dtype=None,
- returnoffset=False,
- photometric=None,
- planarconfig=None,
- tile=None,
- contiguous=True,
- align=16,
- truncate=False,
- compress=0,
- rowsperstrip=None,
- predictor=False,
- colormap=None,
- description=None,
- datetime=None,
- resolution=None,
- software="tifffile.py",
- metadata={},
- ijmetadata=None,
- extratags=(),
- ):
- """Write numpy array and tags to TIFF file.
- The data shape's last dimensions are assumed to be image depth,
- height (length), width, and samples.
- If a colormap is provided, the data's dtype must be uint8 or uint16
- and the data values are indices into the last dimension of the
- colormap.
- If 'shape' and 'dtype' are specified, an empty array is saved.
- This option cannot be used with compression or multiple tiles.
- Image data are written uncompressed in one strip per plane by default.
- Dimensions larger than 2 to 4 (depending on photometric mode, planar
- configuration, and SGI mode) are flattened and saved as separate pages.
- The SampleFormat and BitsPerSample tags are derived from the data type.
- Parameters
- ----------
- data : numpy.ndarray or None
- Input image array.
- shape : tuple or None
- Shape of the empty array to save. Used only if 'data' is None.
- dtype : numpy.dtype or None
- Data-type of the empty array to save. Used only if 'data' is None.
- returnoffset : bool
- If True and the image data in the file is memory-mappable, return
- the offset and number of bytes of the image data in the file.
- photometric : {'MINISBLACK', 'MINISWHITE', 'RGB', 'PALETTE', 'CFA'}
- The color space of the image data.
- By default, this setting is inferred from the data shape and the
- value of colormap.
- For CFA images, DNG tags must be specified in 'extratags'.
- planarconfig : {'CONTIG', 'SEPARATE'}
- Specifies if samples are stored contiguous or in separate planes.
- By default, this setting is inferred from the data shape.
- If this parameter is set, extra samples are used to store grayscale
- images.
- 'CONTIG': last dimension contains samples.
- 'SEPARATE': third last dimension contains samples.
- tile : tuple of int
- The shape (depth, length, width) of image tiles to write.
- If None (default), image data are written in strips.
- The tile length and width must be a multiple of 16.
- If the tile depth is provided, the SGI ImageDepth and TileDepth
- tags are used to save volume data.
- Unless a single tile is used, tiles cannot be used to write
- contiguous files.
- Few software can read the SGI format, e.g. MeVisLab.
- contiguous : bool
- If True (default) and the data and parameters are compatible with
- previous ones, if any, the image data are stored contiguously after
- the previous one. Parameters 'photometric' and 'planarconfig'
- are ignored. Parameters 'description', datetime', and 'extratags'
- are written to the first page of a contiguous series only.
- align : int
- Byte boundary on which to align the image data in the file.
- Default 16. Use mmap.ALLOCATIONGRANULARITY for memory-mapped data.
- Following contiguous writes are not aligned.
- truncate : bool
- If True, only write the first page including shape metadata if
- possible (uncompressed, contiguous, not tiled).
- Other TIFF readers will only be able to read part of the data.
- compress : int or 'LZMA', 'ZSTD'
- Values from 0 to 9 controlling the level of zlib compression.
- If 0 (default), data are written uncompressed.
- Compression cannot be used to write contiguous files.
- If 'LZMA' or 'ZSTD', LZMA or ZSTD compression is used, which is
- not available on all platforms.
- rowsperstrip : int
- The number of rows per strip used for compression.
- Uncompressed data are written in one strip per plane.
- predictor : bool
- If True, apply horizontal differencing to integer type images
- before compression.
- colormap : numpy.ndarray
- RGB color values for the corresponding data value.
- Must be of shape (3, 2**(data.itemsize*8)) and dtype uint16.
- description : str
- The subject of the image. Must be 7-bit ASCII. Cannot be used with
- the ImageJ format. Saved with the first page only.
- datetime : datetime
- Date and time of image creation in '%Y:%m:%d %H:%M:%S' format.
- If None (default), the current date and time is used.
- Saved with the first page only.
- resolution : (float, float[, str]) or ((int, int), (int, int)[, str])
- X and Y resolutions in pixels per resolution unit as float or
- rational numbers. A third, optional parameter specifies the
- resolution unit, which must be None (default for ImageJ),
- 'INCH' (default), or 'CENTIMETER'.
- software : str
- Name of the software used to create the file. Must be 7-bit ASCII.
- Saved with the first page only.
- metadata : dict
- Additional meta data to be saved along with shape information
- in JSON or ImageJ formats in an ImageDescription tag.
- If None, do not write a second ImageDescription tag.
- Strings must be 7-bit ASCII. Saved with the first page only.
- ijmetadata : dict
- Additional meta data to be saved in application specific
- IJMetadata and IJMetadataByteCounts tags. Refer to the
- imagej_metadata_tags function for valid keys and values.
- Saved with the first page only.
- extratags : sequence of tuples
- Additional tags as [(code, dtype, count, value, writeonce)].
- code : int
- The TIFF tag Id.
- dtype : str
- Data type of items in 'value' in Python struct format.
- One of B, s, H, I, 2I, b, h, i, 2i, f, d, Q, or q.
- count : int
- Number of data values. Not used for string or byte string
- values.
- value : sequence
- 'Count' values compatible with 'dtype'.
- Byte strings must contain count values of dtype packed as
- binary data.
- writeonce : bool
- If True, the tag is written to the first page only.
- """
- # TODO: refactor this function
- fh = self._fh
- byteorder = self._byteorder
- if data is None:
- if compress:
- raise ValueError("cannot save compressed empty file")
- datashape = shape
- datadtype = numpy.dtype(dtype).newbyteorder(byteorder)
- datadtypechar = datadtype.char
- else:
- data = numpy.asarray(data, byteorder + data.dtype.char, "C")
- if data.size == 0:
- raise ValueError("cannot save empty array")
- datashape = data.shape
- datadtype = data.dtype
- datadtypechar = data.dtype.char
- returnoffset = returnoffset and datadtype.isnative
- bilevel = datadtypechar == "?"
- if bilevel:
- index = -1 if datashape[-1] > 1 else -2
- datasize = product(datashape[:index])
- if datashape[index] % 8:
- datasize *= datashape[index] // 8 + 1
- else:
- datasize *= datashape[index] // 8
- else:
- datasize = product(datashape) * datadtype.itemsize
- # just append contiguous data if possible
- self._truncate = bool(truncate)
- if self._datashape:
- if (
- not contiguous
- or self._datashape[1:] != datashape
- or self._datadtype != datadtype
- or (compress and self._tags)
- or tile
- or not numpy.array_equal(colormap, self._colormap)
- ):
- # incompatible shape, dtype, compression mode, or colormap
- self._write_remaining_pages()
- self._write_image_description()
- self._truncate = False
- self._descriptionoffset = 0
- self._descriptionlenoffset = 0
- self._datashape = None
- self._colormap = None
- if self._imagej:
- raise ValueError("ImageJ does not support non-contiguous data")
- else:
- # consecutive mode
- self._datashape = (self._datashape[0] + 1,) + datashape
- if not compress:
- # write contiguous data, write IFDs/tags later
- offset = fh.tell()
- if data is None:
- fh.write_empty(datasize)
- else:
- fh.write_array(data)
- if returnoffset:
- return offset, datasize
- return
- input_shape = datashape
- tagnoformat = self._tagnoformat
- valueformat = self._valueformat
- offsetformat = self._offsetformat
- offsetsize = self._offsetsize
- tagsize = self._tagsize
- MINISBLACK = TIFF.PHOTOMETRIC.MINISBLACK
- RGB = TIFF.PHOTOMETRIC.RGB
- CFA = TIFF.PHOTOMETRIC.CFA
- PALETTE = TIFF.PHOTOMETRIC.PALETTE
- CONTIG = TIFF.PLANARCONFIG.CONTIG
- SEPARATE = TIFF.PLANARCONFIG.SEPARATE
- # parse input
- if photometric is not None:
- photometric = enumarg(TIFF.PHOTOMETRIC, photometric)
- if planarconfig:
- planarconfig = enumarg(TIFF.PLANARCONFIG, planarconfig)
- if not compress:
- compress = False
- compresstag = 1
- predictor = False
- else:
- if isinstance(compress, (tuple, list)):
- compress, compresslevel = compress
- elif isinstance(compress, int):
- compress, compresslevel = "ADOBE_DEFLATE", int(compress)
- if not 0 <= compresslevel <= 9:
- raise ValueError("invalid compression level %s" % compress)
- else:
- compresslevel = None
- compress = compress.upper()
- compresstag = enumarg(TIFF.COMPRESSION, compress)
- # prepare ImageJ format
- if self._imagej:
- if compress in ("LZMA", "ZSTD"):
- raise ValueError("ImageJ cannot handle LZMA or ZSTD compression")
- if description:
- warnings.warn("not writing description to ImageJ file")
- description = None
- volume = False
- if datadtypechar not in "BHhf":
- raise ValueError("ImageJ does not support data type %s" % datadtypechar)
- ijrgb = photometric == RGB if photometric else None
- if datadtypechar not in "B":
- ijrgb = False
- ijshape = imagej_shape(datashape, ijrgb)
- if ijshape[-1] in (3, 4):
- photometric = RGB
- if datadtypechar not in "B":
- raise ValueError(
- "ImageJ does not support data type %s "
- "for RGB" % datadtypechar
- )
- elif photometric is None:
- photometric = MINISBLACK
- planarconfig = None
- if planarconfig == SEPARATE:
- raise ValueError("ImageJ does not support planar images")
- else:
- planarconfig = CONTIG if ijrgb else None
- # define compress function
- if compress:
- if compresslevel is None:
- compressor, compresslevel = TIFF.COMPESSORS[compresstag]
- else:
- compressor, _ = TIFF.COMPESSORS[compresstag]
- compresslevel = int(compresslevel)
- if predictor:
- if datadtype.kind not in "iu":
- raise ValueError("prediction not implemented for %s" % datadtype)
- def compress(data, level=compresslevel):
- # horizontal differencing
- diff = numpy.diff(data, axis=-2)
- data = numpy.insert(diff, 0, data[..., 0, :], axis=-2)
- return compressor(data, level)
- else:
- def compress(data, level=compresslevel):
- return compressor(data, level)
- # verify colormap and indices
- if colormap is not None:
- if datadtypechar not in "BH":
- raise ValueError("invalid data dtype for palette mode")
- colormap = numpy.asarray(colormap, dtype=byteorder + "H")
- if colormap.shape != (3, 2 ** (datadtype.itemsize * 8)):
- raise ValueError("invalid color map shape")
- self._colormap = colormap
- # verify tile shape
- if tile:
- tile = tuple(int(i) for i in tile[:3])
- volume = len(tile) == 3
- if (
- len(tile) < 2
- or tile[-1] % 16
- or tile[-2] % 16
- or any(i < 1 for i in tile)
- ):
- raise ValueError("invalid tile shape")
- else:
- tile = ()
- volume = False
- # normalize data shape to 5D or 6D, depending on volume:
- # (pages, planar_samples, [depth,] height, width, contig_samples)
- datashape = reshape_nd(datashape, 3 if photometric == RGB else 2)
- shape = datashape
- ndim = len(datashape)
- samplesperpixel = 1
- extrasamples = 0
- if volume and ndim < 3:
- volume = False
- if colormap is not None:
- photometric = PALETTE
- planarconfig = None
- if photometric is None:
- photometric = MINISBLACK
- if bilevel:
- photometric = TIFF.PHOTOMETRIC.MINISWHITE
- elif planarconfig == CONTIG:
- if ndim > 2 and shape[-1] in (3, 4):
- photometric = RGB
- elif planarconfig == SEPARATE:
- if volume and ndim > 3 and shape[-4] in (3, 4):
- photometric = RGB
- elif ndim > 2 and shape[-3] in (3, 4):
- photometric = RGB
- elif ndim > 2 and shape[-1] in (3, 4):
- photometric = RGB
- elif self._imagej:
- photometric = MINISBLACK
- elif volume and ndim > 3 and shape[-4] in (3, 4):
- photometric = RGB
- elif ndim > 2 and shape[-3] in (3, 4):
- photometric = RGB
- if planarconfig and len(shape) <= (3 if volume else 2):
- planarconfig = None
- photometric = MINISBLACK
- if photometric == RGB:
- if len(shape) < 3:
- raise ValueError("not a RGB(A) image")
- if len(shape) < 4:
- volume = False
- if planarconfig is None:
- if shape[-1] in (3, 4):
- planarconfig = CONTIG
- elif shape[-4 if volume else -3] in (3, 4):
- planarconfig = SEPARATE
- elif shape[-1] > shape[-4 if volume else -3]:
- planarconfig = SEPARATE
- else:
- planarconfig = CONTIG
- if planarconfig == CONTIG:
- datashape = (-1, 1) + shape[(-4 if volume else -3) :]
- samplesperpixel = datashape[-1]
- else:
- datashape = (-1,) + shape[(-4 if volume else -3) :] + (1,)
- samplesperpixel = datashape[1]
- if samplesperpixel > 3:
- extrasamples = samplesperpixel - 3
- elif photometric == CFA:
- if len(shape) != 2:
- raise ValueError("invalid CFA image")
- volume = False
- planarconfig = None
- datashape = (-1, 1) + shape[-2:] + (1,)
- if 50706 not in (et[0] for et in extratags):
- raise ValueError("must specify DNG tags for CFA image")
- elif planarconfig and len(shape) > (3 if volume else 2):
- if planarconfig == CONTIG:
- datashape = (-1, 1) + shape[(-4 if volume else -3) :]
- samplesperpixel = datashape[-1]
- else:
- datashape = (-1,) + shape[(-4 if volume else -3) :] + (1,)
- samplesperpixel = datashape[1]
- extrasamples = samplesperpixel - 1
- else:
- planarconfig = None
- # remove trailing 1s
- while len(shape) > 2 and shape[-1] == 1:
- shape = shape[:-1]
- if len(shape) < 3:
- volume = False
- datashape = (-1, 1) + shape[(-3 if volume else -2) :] + (1,)
- # normalize shape to 6D
- assert len(datashape) in (5, 6)
- if len(datashape) == 5:
- datashape = datashape[:2] + (1,) + datashape[2:]
- if datashape[0] == -1:
- s0 = product(input_shape) // product(datashape[1:])
- datashape = (s0,) + datashape[1:]
- shape = datashape
- if data is not None:
- data = data.reshape(shape)
- if tile and not volume:
- tile = (1, tile[-2], tile[-1])
- if photometric == PALETTE:
- if samplesperpixel != 1 or extrasamples or shape[1] != 1 or shape[-1] != 1:
- raise ValueError("invalid data shape for palette mode")
- if photometric == RGB and samplesperpixel == 2:
- raise ValueError("not a RGB image (samplesperpixel=2)")
- if bilevel:
- if compress:
- raise ValueError("cannot save compressed bilevel image")
- if tile:
- raise ValueError("cannot save tiled bilevel image")
- if photometric not in (0, 1):
- raise ValueError("cannot save bilevel image as %s" % str(photometric))
- datashape = list(datashape)
- if datashape[-2] % 8:
- datashape[-2] = datashape[-2] // 8 + 1
- else:
- datashape[-2] = datashape[-2] // 8
- datashape = tuple(datashape)
- assert datasize == product(datashape)
- if data is not None:
- data = numpy.packbits(data, axis=-2)
- assert datashape[-2] == data.shape[-2]
- bytestr = (
- bytes
- if sys.version[0] == "2"
- else (lambda x: bytes(x, "ascii") if isinstance(x, str) else x)
- )
- tags = [] # list of (code, ifdentry, ifdvalue, writeonce)
- strip_or_tile = "Tile" if tile else "Strip"
- tagbytecounts = TIFF.TAG_NAMES[strip_or_tile + "ByteCounts"]
- tag_offsets = TIFF.TAG_NAMES[strip_or_tile + "Offsets"]
- self._tagoffsets = tag_offsets
- def pack(fmt, *val):
- return struct.pack(byteorder + fmt, *val)
- def addtag(code, dtype, count, value, writeonce=False):
- # Compute ifdentry & ifdvalue bytes from code, dtype, count, value
- # Append (code, ifdentry, ifdvalue, writeonce) to tags list
- code = int(TIFF.TAG_NAMES.get(code, code))
- try:
- tifftype = TIFF.DATA_DTYPES[dtype]
- except KeyError:
- raise ValueError("unknown dtype %s" % dtype)
- rawcount = count
- if dtype == "s":
- # strings
- value = bytestr(value) + b"\0"
- count = rawcount = len(value)
- rawcount = value.find(b"\0\0")
- if rawcount < 0:
- rawcount = count
- else:
- rawcount += 1 # length of string without buffer
- value = (value,)
- elif isinstance(value, bytes):
- # packed binary data
- dtsize = struct.calcsize(dtype)
- if len(value) % dtsize:
- raise ValueError("invalid packed binary data")
- count = len(value) // dtsize
- if len(dtype) > 1:
- count *= int(dtype[:-1])
- dtype = dtype[-1]
- ifdentry = [pack("HH", code, tifftype), pack(offsetformat, rawcount)]
- ifdvalue = None
- if struct.calcsize(dtype) * count <= offsetsize:
- # value(s) can be written directly
- if isinstance(value, bytes):
- ifdentry.append(pack(valueformat, value))
- elif count == 1:
- if isinstance(value, (tuple, list, numpy.ndarray)):
- value = value[0]
- ifdentry.append(pack(valueformat, pack(dtype, value)))
- else:
- ifdentry.append(pack(valueformat, pack(str(count) + dtype, *value)))
- else:
- # use offset to value(s)
- ifdentry.append(pack(offsetformat, 0))
- if isinstance(value, bytes):
- ifdvalue = value
- elif isinstance(value, numpy.ndarray):
- assert value.size == count
- assert value.dtype.char == dtype
- ifdvalue = value.tostring()
- elif isinstance(value, (tuple, list)):
- ifdvalue = pack(str(count) + dtype, *value)
- else:
- ifdvalue = pack(dtype, value)
- tags.append((code, b"".join(ifdentry), ifdvalue, writeonce))
- def rational(arg, max_denominator=1000000):
- """ "Return nominator and denominator from float or two integers."""
- from fractions import Fraction # delayed import
- try:
- f = Fraction.from_float(arg)
- except TypeError:
- f = Fraction(arg[0], arg[1])
- f = f.limit_denominator(max_denominator)
- return f.numerator, f.denominator
- if description:
- # user provided description
- addtag("ImageDescription", "s", 0, description, writeonce=True)
- # write shape and metadata to ImageDescription
- self._metadata = {} if not metadata else metadata.copy()
- if self._imagej:
- description = imagej_description(
- input_shape,
- shape[-1] in (3, 4),
- self._colormap is not None,
- **self._metadata,
- )
- elif metadata or metadata == {}:
- if self._truncate:
- self._metadata.update(truncated=True)
- description = json_description(input_shape, **self._metadata)
- else:
- description = None
- if description:
- # add 64 bytes buffer
- # the image description might be updated later with the final shape
- description = str2bytes(description, "ascii")
- description += b"\0" * 64
- self._descriptionlen = len(description)
- addtag("ImageDescription", "s", 0, description, writeonce=True)
- if software:
- addtag("Software", "s", 0, software, writeonce=True)
- if datetime is None:
- datetime = self._now()
- addtag(
- "DateTime", "s", 0, datetime.strftime("%Y:%m:%d %H:%M:%S"), writeonce=True
- )
- addtag("Compression", "H", 1, compresstag)
- if predictor:
- addtag("Predictor", "H", 1, 2)
- addtag("ImageWidth", "I", 1, shape[-2])
- addtag("ImageLength", "I", 1, shape[-3])
- if tile:
- addtag("TileWidth", "I", 1, tile[-1])
- addtag("TileLength", "I", 1, tile[-2])
- if tile[0] > 1:
- addtag("ImageDepth", "I", 1, shape[-4])
- addtag("TileDepth", "I", 1, tile[0])
- addtag("NewSubfileType", "I", 1, 0)
- if not bilevel:
- sampleformat = {"u": 1, "i": 2, "f": 3, "c": 6}[datadtype.kind]
- addtag(
- "SampleFormat", "H", samplesperpixel, (sampleformat,) * samplesperpixel
- )
- addtag("PhotometricInterpretation", "H", 1, photometric.value)
- if colormap is not None:
- addtag("ColorMap", "H", colormap.size, colormap)
- addtag("SamplesPerPixel", "H", 1, samplesperpixel)
- if bilevel:
- pass
- elif planarconfig and samplesperpixel > 1:
- addtag("PlanarConfiguration", "H", 1, planarconfig.value)
- addtag(
- "BitsPerSample",
- "H",
- samplesperpixel,
- (datadtype.itemsize * 8,) * samplesperpixel,
- )
- else:
- addtag("BitsPerSample", "H", 1, datadtype.itemsize * 8)
- if extrasamples:
- if photometric == RGB and extrasamples == 1:
- addtag("ExtraSamples", "H", 1, 1) # associated alpha channel
- else:
- addtag("ExtraSamples", "H", extrasamples, (0,) * extrasamples)
- if resolution is not None:
- addtag("XResolution", "2I", 1, rational(resolution[0]))
- addtag("YResolution", "2I", 1, rational(resolution[1]))
- if len(resolution) > 2:
- unit = resolution[2]
- unit = 1 if unit is None else enumarg(TIFF.RESUNIT, unit)
- elif self._imagej:
- unit = 1
- else:
- unit = 2
- addtag("ResolutionUnit", "H", 1, unit)
- elif not self._imagej:
- addtag("XResolution", "2I", 1, (1, 1))
- addtag("YResolution", "2I", 1, (1, 1))
- addtag("ResolutionUnit", "H", 1, 1)
- if ijmetadata:
- for t in imagej_metadata_tags(ijmetadata, byteorder):
- addtag(*t)
- contiguous = not compress
- if tile:
- # one chunk per tile per plane
- tiles = (
- (shape[2] + tile[0] - 1) // tile[0],
- (shape[3] + tile[1] - 1) // tile[1],
- (shape[4] + tile[2] - 1) // tile[2],
- )
- numtiles = product(tiles) * shape[1]
- stripbytecounts = [
- product(tile) * shape[-1] * datadtype.itemsize
- ] * numtiles
- addtag(tagbytecounts, offsetformat, numtiles, stripbytecounts)
- addtag(tag_offsets, offsetformat, numtiles, [0] * numtiles)
- contiguous = contiguous and product(tiles) == 1
- if not contiguous:
- # allocate tile buffer
- chunk = numpy.empty(tile + (shape[-1],), dtype=datadtype)
- elif contiguous:
- # one strip per plane
- if bilevel:
- stripbytecounts = [product(datashape[2:])] * shape[1]
- else:
- stripbytecounts = [product(datashape[2:]) * datadtype.itemsize] * shape[
- 1
- ]
- addtag(tagbytecounts, offsetformat, shape[1], stripbytecounts)
- addtag(tag_offsets, offsetformat, shape[1], [0] * shape[1])
- addtag("RowsPerStrip", "I", 1, shape[-3])
- else:
- # compress rowsperstrip or ~64 KB chunks
- rowsize = product(shape[-2:]) * datadtype.itemsize
- if rowsperstrip is None:
- rowsperstrip = 65536 // rowsize
- if rowsperstrip < 1:
- rowsperstrip = 1
- elif rowsperstrip > shape[-3]:
- rowsperstrip = shape[-3]
- addtag("RowsPerStrip", "I", 1, rowsperstrip)
- numstrips = (shape[-3] + rowsperstrip - 1) // rowsperstrip
- numstrips *= shape[1]
- stripbytecounts = [0] * numstrips
- addtag(tagbytecounts, offsetformat, numstrips, [0] * numstrips)
- addtag(tag_offsets, offsetformat, numstrips, [0] * numstrips)
- if data is None and not contiguous:
- raise ValueError("cannot write non-contiguous empty file")
- # add extra tags from user
- for t in extratags:
- addtag(*t)
- # TODO: check TIFFReadDirectoryCheckOrder warning in files containing
- # multiple tags of same code
- # the entries in an IFD must be sorted in ascending order by tag code
- tags = sorted(tags, key=lambda x: x[0])
- if not (self._bigtiff or self._imagej) and (fh.tell() + datasize > 2**31 - 1):
- raise ValueError("data too large for standard TIFF file")
- # if not compressed or multi-tiled, write the first IFD and then
- # all data contiguously; else, write all IFDs and data interleaved
- for pageindex in range(1 if contiguous else shape[0]):
- # update pointer at ifd_offset
- pos = fh.tell()
- if pos % 2:
- # location of IFD must begin on a word boundary
- fh.write(b"\0")
- pos += 1
- fh.seek(self._ifdoffset)
- fh.write(pack(offsetformat, pos))
- fh.seek(pos)
- # write ifdentries
- fh.write(pack(tagnoformat, len(tags)))
- tag_offset = fh.tell()
- fh.write(b"".join(t[1] for t in tags))
- self._ifdoffset = fh.tell()
- fh.write(pack(offsetformat, 0)) # offset to next IFD
- # write tag values and patch offsets in ifdentries, if necessary
- for tagindex, tag in enumerate(tags):
- if tag[2]:
- pos = fh.tell()
- if pos % 2:
- # tag value is expected to begin on word boundary
- fh.write(b"\0")
- pos += 1
- fh.seek(tag_offset + tagindex * tagsize + offsetsize + 4)
- fh.write(pack(offsetformat, pos))
- fh.seek(pos)
- if tag[0] == tag_offsets:
- stripoffsetsoffset = pos
- elif tag[0] == tagbytecounts:
- strip_bytecounts_offset = pos
- elif tag[0] == 270 and tag[2].endswith(b"\0\0\0\0"):
- # image description buffer
- self._descriptionoffset = pos
- self._descriptionlenoffset = tag_offset + tagindex * tagsize + 4
- fh.write(tag[2])
- # write image data
- data_offset = fh.tell()
- skip = align - data_offset % align
- fh.seek(skip, 1)
- data_offset += skip
- if contiguous:
- if data is None:
- fh.write_empty(datasize)
- else:
- fh.write_array(data)
- elif tile:
- if data is None:
- fh.write_empty(numtiles * stripbytecounts[0])
- else:
- stripindex = 0
- for plane in data[pageindex]:
- for tz in range(tiles[0]):
- for ty in range(tiles[1]):
- for tx in range(tiles[2]):
- c0 = min(tile[0], shape[2] - tz * tile[0])
- c1 = min(tile[1], shape[3] - ty * tile[1])
- c2 = min(tile[2], shape[4] - tx * tile[2])
- chunk[c0:, c1:, c2:] = 0
- chunk[:c0, :c1, :c2] = plane[
- tz * tile[0] : tz * tile[0] + c0,
- ty * tile[1] : ty * tile[1] + c1,
- tx * tile[2] : tx * tile[2] + c2,
- ]
- if compress:
- t = compress(chunk)
- fh.write(t)
- stripbytecounts[stripindex] = len(t)
- stripindex += 1
- else:
- fh.write_array(chunk)
- fh.flush()
- elif compress:
- # write one strip per rowsperstrip
- assert data.shape[2] == 1 # not handling depth
- numstrips = (shape[-3] + rowsperstrip - 1) // rowsperstrip
- stripindex = 0
- for plane in data[pageindex]:
- for i in range(numstrips):
- strip = plane[0, i * rowsperstrip : (i + 1) * rowsperstrip]
- strip = compress(strip)
- fh.write(strip)
- stripbytecounts[stripindex] = len(strip)
- stripindex += 1
- # update strip/tile offsets and bytecounts if necessary
- pos = fh.tell()
- for tagindex, tag in enumerate(tags):
- if tag[0] == tag_offsets: # strip/tile offsets
- if tag[2]:
- fh.seek(stripoffsetsoffset)
- strip_offset = data_offset
- for size in stripbytecounts:
- fh.write(pack(offsetformat, strip_offset))
- strip_offset += size
- else:
- fh.seek(tag_offset + tagindex * tagsize + offsetsize + 4)
- fh.write(pack(offsetformat, data_offset))
- elif tag[0] == tagbytecounts: # strip/tile bytecounts
- if compress:
- if tag[2]:
- fh.seek(strip_bytecounts_offset)
- for size in stripbytecounts:
- fh.write(pack(offsetformat, size))
- else:
- fh.seek(tag_offset + tagindex * tagsize + offsetsize + 4)
- fh.write(pack(offsetformat, stripbytecounts[0]))
- break
- fh.seek(pos)
- fh.flush()
- # remove tags that should be written only once
- if pageindex == 0:
- tags = [tag for tag in tags if not tag[-1]]
- self._shape = shape
- self._datashape = (1,) + input_shape
- self._datadtype = datadtype
- self._dataoffset = data_offset
- self._databytecounts = stripbytecounts
- if contiguous:
- # write remaining IFDs/tags later
- self._tags = tags
- # return offset and size of image data
- if returnoffset:
- return data_offset, sum(stripbytecounts)
- def _write_remaining_pages(self):
- """Write outstanding IFDs and tags to file."""
- if not self._tags or self._truncate:
- return
- fh = self._fh
- fhpos = fh.tell()
- if fhpos % 2:
- fh.write(b"\0")
- fhpos += 1
- byteorder = self._byteorder
- offsetformat = self._offsetformat
- offsetsize = self._offsetsize
- tagnoformat = self._tagnoformat
- tagsize = self._tagsize
- dataoffset = self._dataoffset
- pagedatasize = sum(self._databytecounts)
- pageno = self._shape[0] * self._datashape[0] - 1
- def pack(fmt, *val):
- return struct.pack(byteorder + fmt, *val)
- # construct template IFD in memory
- # need to patch offsets to next IFD and data before writing to disk
- ifd = io.BytesIO()
- ifd.write(pack(tagnoformat, len(self._tags)))
- tagoffset = ifd.tell()
- ifd.write(b"".join(t[1] for t in self._tags))
- ifdoffset = ifd.tell()
- ifd.write(pack(offsetformat, 0)) # offset to next IFD
- # tag values
- for tagindex, tag in enumerate(self._tags):
- offset2value = tagoffset + tagindex * tagsize + offsetsize + 4
- if tag[2]:
- pos = ifd.tell()
- if pos % 2: # tag value is expected to begin on word boundary
- ifd.write(b"\0")
- pos += 1
- ifd.seek(offset2value)
- try:
- ifd.write(pack(offsetformat, pos + fhpos))
- except Exception: # struct.error
- if self._imagej:
- warnings.warn("truncating ImageJ file")
- self._truncate = True
- return
- raise ValueError("data too large for non-BigTIFF file")
- ifd.seek(pos)
- ifd.write(tag[2])
- if tag[0] == self._tagoffsets:
- # save strip/tile offsets for later updates
- stripoffset2offset = offset2value
- stripoffset2value = pos
- elif tag[0] == self._tagoffsets:
- # save strip/tile offsets for later updates
- stripoffset2offset = None
- stripoffset2value = offset2value
- # size to word boundary
- if ifd.tell() % 2:
- ifd.write(b"\0")
- # check if all IFDs fit in file
- pos = fh.tell()
- if not self._bigtiff and pos + ifd.tell() * pageno > 2**32 - 256:
- if self._imagej:
- warnings.warn("truncating ImageJ file")
- self._truncate = True
- return
- raise ValueError("data too large for non-BigTIFF file")
- # TODO: assemble IFD chain in memory
- for _ in range(pageno):
- # update pointer at IFD offset
- pos = fh.tell()
- fh.seek(self._ifdoffset)
- fh.write(pack(offsetformat, pos))
- fh.seek(pos)
- self._ifdoffset = pos + ifdoffset
- # update strip/tile offsets in IFD
- dataoffset += pagedatasize # offset to image data
- if stripoffset2offset is None:
- ifd.seek(stripoffset2value)
- ifd.write(pack(offsetformat, dataoffset))
- else:
- ifd.seek(stripoffset2offset)
- ifd.write(pack(offsetformat, pos + stripoffset2value))
- ifd.seek(stripoffset2value)
- stripoffset = dataoffset
- for size in self._databytecounts:
- ifd.write(pack(offsetformat, stripoffset))
- stripoffset += size
- # write IFD entry
- fh.write(ifd.getvalue())
- self._tags = None
- self._datadtype = None
- self._dataoffset = None
- self._databytecounts = None
- # do not reset _shape or _data_shape
- def _write_image_description(self):
- """Write meta data to ImageDescription tag."""
- if (
- not self._datashape
- or self._datashape[0] == 1
- or self._descriptionoffset <= 0
- ):
- return
- colormapped = self._colormap is not None
- if self._imagej:
- isrgb = self._shape[-1] in (3, 4)
- description = imagej_description(
- self._datashape, isrgb, colormapped, **self._metadata
- )
- else:
- description = json_description(self._datashape, **self._metadata)
- # rewrite description and its length to file
- description = description.encode("utf-8")
- description = description[: self._descriptionlen - 1]
- pos = self._fh.tell()
- self._fh.seek(self._descriptionoffset)
- self._fh.write(description)
- self._fh.seek(self._descriptionlenoffset)
- self._fh.write(
- struct.pack(self._byteorder + self._offsetformat, len(description) + 1)
- )
- self._fh.seek(pos)
- self._descriptionoffset = 0
- self._descriptionlenoffset = 0
- self._descriptionlen = 0
- def _now(self):
- """Return current date and time."""
- return datetime.datetime.now()
- def close(self):
- """Write remaining pages and close file handle."""
- if not self._truncate:
- self._write_remaining_pages()
- self._write_image_description()
- self._fh.close()
- def __enter__(self):
- return self
- def __exit__(self, exc_type, exc_value, traceback):
- self.close()
- class TiffFile(object):
- """Read image and metadata from TIFF file.
- TiffFile instances must be closed using the 'close' method, which is
- automatically called when using the 'with' context manager.
- Attributes
- ----------
- pages : TiffPages
- Sequence of TIFF pages in file.
- series : list of TiffPageSeries
- Sequences of closely related TIFF pages. These are computed
- from OME, LSM, ImageJ, etc. metadata or based on similarity
- of page properties such as shape, dtype, and compression.
- byteorder : '>', '<'
- The endianness of data in the file.
- '>': big-endian (Motorola).
- '>': little-endian (Intel).
- is_flag : bool
- If True, file is of a certain format.
- Flags are: bigtiff, movie, shaped, ome, imagej, stk, lsm, fluoview,
- nih, vista, 'micromanager, metaseries, mdgel, mediacy, tvips, fei,
- sem, scn, svs, scanimage, andor, epics, pilatus, qptiff.
- All attributes are read-only.
- Examples
- --------
- >>> # read image array from TIFF file
- >>> imsave('temp.tif', numpy.random.rand(5, 301, 219))
- >>> with TiffFile('temp.tif') as tif:
- ... data = tif.asarray()
- >>> data.shape
- (5, 301, 219)
- """
- def __init__(
- self,
- arg,
- name=None,
- offset=None,
- size=None,
- multifile=True,
- movie=None,
- **kwargs,
- ):
- """Initialize instance from file.
- Parameters
- ----------
- arg : str or open file
- Name of file or open file object.
- The file objects are closed in TiffFile.close().
- name : str
- Optional name of file in case 'arg' is a file handle.
- offset : int
- Optional start position of embedded file. By default, this is
- the current file position.
- size : int
- Optional size of embedded file. By default, this is the number
- of bytes from the 'offset' to the end of the file.
- multifile : bool
- If True (default), series may include pages from multiple files.
- Currently applies to OME-TIFF only.
- movie : bool
- If True, assume that later pages differ from first page only by
- data offsets and byte counts. Significantly increases speed and
- reduces memory usage when reading movies with thousands of pages.
- Enabling this for non-movie files will result in data corruption
- or crashes. Python 3 only.
- kwargs : bool
- 'is_ome': If False, disable processing of OME-XML metadata.
- """
- if "fastij" in kwargs:
- del kwargs["fastij"]
- raise DeprecationWarning("the fastij option will be removed")
- for key, value in kwargs.items():
- if key[:3] == "is_" and key[3:] in TIFF.FILE_FLAGS:
- if value is not None and not value:
- setattr(self, key, bool(value))
- else:
- raise TypeError("unexpected keyword argument: %s" % key)
- fh = FileHandle(arg, mode="rb", name=name, offset=offset, size=size)
- self._fh = fh
- self._multifile = bool(multifile)
- self._files = {fh.name: self} # cache of TiffFiles
- try:
- fh.seek(0)
- try:
- byteorder = {b"II": "<", b"MM": ">"}[fh.read(2)]
- except KeyError:
- raise ValueError("not a TIFF file")
- sys_byteorder = {"big": ">", "little": "<"}[sys.byteorder]
- self.isnative = byteorder == sys_byteorder
- version = struct.unpack(byteorder + "H", fh.read(2))[0]
- if version == 43:
- # BigTiff
- self.is_bigtiff = True
- offsetsize, zero = struct.unpack(byteorder + "HH", fh.read(4))
- if zero or offsetsize != 8:
- raise ValueError("invalid BigTIFF file")
- self.byteorder = byteorder
- self.offsetsize = 8
- self.offsetformat = byteorder + "Q"
- self.tagnosize = 8
- self.tagnoformat = byteorder + "Q"
- self.tagsize = 20
- self.tagformat1 = byteorder + "HH"
- self.tagformat2 = byteorder + "Q8s"
- elif version == 42:
- self.is_bigtiff = False
- self.byteorder = byteorder
- self.offsetsize = 4
- self.offsetformat = byteorder + "I"
- self.tagnosize = 2
- self.tagnoformat = byteorder + "H"
- self.tagsize = 12
- self.tagformat1 = byteorder + "HH"
- self.tagformat2 = byteorder + "I4s"
- else:
- raise ValueError("invalid TIFF file")
- # file handle is at offset to offset to first page
- self.pages = TiffPages(self)
- if self.is_lsm and (
- self.filehandle.size >= 2**32
- or self.pages[0].compression != 1
- or self.pages[1].compression != 1
- ):
- self._lsm_load_pages()
- self._lsm_fix_strip_offsets()
- self._lsm_fix_strip_bytecounts()
- elif movie:
- self.pages.useframes = True
- except Exception:
- fh.close()
- raise
- @property
- def filehandle(self):
- """Return file handle."""
- return self._fh
- @property
- def filename(self):
- """Return name of file handle."""
- return self._fh.name
- @lazyattr
- def fstat(self):
- """Return status of file handle as stat_result object."""
- try:
- return os.fstat(self._fh.fileno())
- except Exception: # io.UnsupportedOperation
- return None
- def close(self):
- """Close open file handle(s)."""
- for tif in self._files.values():
- tif.filehandle.close()
- self._files = {}
- def asarray(self, key=None, series=None, out=None, validate=True, maxworkers=1):
- """Return image data from multiple TIFF pages as numpy array.
- By default, the data from the first series is returned.
- Parameters
- ----------
- key : int, slice, or sequence of page indices
- Defines which pages to return as array.
- series : int or TiffPageSeries
- Defines which series of pages to return as array.
- out : numpy.ndarray, str, or file-like object; optional
- Buffer where image data will be saved.
- If None (default), a new array will be created.
- If numpy.ndarray, a writable array of compatible dtype and shape.
- If 'memmap', directly memory-map the image data in the TIFF file
- if possible; else create a memory-mapped array in a temporary file.
- If str or open file, the file name or file object used to
- create a memory-map to an array stored in a binary file on disk.
- validate : bool
- If True (default), validate various tags.
- Passed to TiffPage.asarray().
- maxworkers : int
- Maximum number of threads to concurrently get data from pages.
- Default is 1. If None, up to half the CPU cores are used.
- Reading data from file is limited to a single thread.
- Using multiple threads can significantly speed up this function
- if the bottleneck is decoding compressed data, e.g. in case of
- large LZW compressed LSM files.
- If the bottleneck is I/O or pure Python code, using multiple
- threads might be detrimental.
- """
- if not self.pages:
- return numpy.array([])
- if key is None and series is None:
- series = 0
- if series is not None:
- try:
- series = self.series[series]
- except (KeyError, TypeError):
- pass
- pages = series._pages
- else:
- pages = self.pages
- if key is None:
- pass
- elif isinstance(key, inttypes):
- pages = [pages[key]]
- elif isinstance(key, slice):
- pages = pages[key]
- elif isinstance(key, collections.Iterable):
- pages = [pages[k] for k in key]
- else:
- raise TypeError("key must be an int, slice, or sequence")
- if not pages:
- raise ValueError("no pages selected")
- if self.is_nih:
- result = stack_pages(pages, out=out, maxworkers=maxworkers, squeeze=False)
- elif key is None and series and series.offset:
- typecode = self.byteorder + series.dtype.char
- if out == "memmap" and pages[0].is_memmappable:
- result = self.filehandle.memmap_array(
- typecode, series.shape, series.offset
- )
- else:
- if out is not None:
- out = create_output(out, series.shape, series.dtype)
- self.filehandle.seek(series.offset)
- result = self.filehandle.read_array(
- typecode, product(series.shape), out=out, native=True
- )
- elif len(pages) == 1:
- result = pages[0].asarray(out=out, validate=validate)
- else:
- result = stack_pages(pages, out=out, maxworkers=maxworkers)
- if result is None:
- return
- if key is None:
- try:
- result.shape = series.shape
- except ValueError:
- try:
- warnings.warn(
- "failed to reshape %s to %s" % (result.shape, series.shape)
- )
- # try series of expected shapes
- result.shape = (-1,) + series.shape
- except ValueError:
- # revert to generic shape
- result.shape = (-1,) + pages[0].shape
- elif len(pages) == 1:
- result.shape = pages[0].shape
- else:
- result.shape = (-1,) + pages[0].shape
- return result
- @lazyattr
- def series(self):
- """Return related pages as TiffPageSeries.
- Side effect: after calling this function, TiffFile.pages might contain
- TiffPage and TiffFrame instances.
- """
- if not self.pages:
- return []
- useframes = self.pages.useframes
- keyframe = self.pages.keyframe
- series = []
- for name in "ome imagej lsm fluoview nih mdgel shaped".split():
- if getattr(self, "is_" + name, False):
- series = getattr(self, "_%s_series" % name)()
- break
- self.pages.useframes = useframes
- self.pages.keyframe = keyframe
- if not series:
- series = self._generic_series()
- # remove empty series, e.g. in MD Gel files
- series = [s for s in series if sum(s.shape) > 0]
- for i, s in enumerate(series):
- s.index = i
- return series
- def _generic_series(self):
- """Return image series in file."""
- if self.pages.useframes:
- # movie mode
- page = self.pages[0]
- shape = page.shape
- axes = page.axes
- if len(self.pages) > 1:
- shape = (len(self.pages),) + shape
- axes = "I" + axes
- return [
- TiffPageSeries(self.pages[:], shape, page.dtype, axes, stype="movie")
- ]
- self.pages.clear(False)
- self.pages.load()
- result = []
- keys = []
- series = {}
- compressions = TIFF.DECOMPESSORS
- for page in self.pages:
- if not page.shape:
- continue
- key = page.shape + (page.axes, page.compression in compressions)
- if key in series:
- series[key].append(page)
- else:
- keys.append(key)
- series[key] = [page]
- for key in keys:
- pages = series[key]
- page = pages[0]
- shape = page.shape
- axes = page.axes
- if len(pages) > 1:
- shape = (len(pages),) + shape
- axes = "I" + axes
- result.append(
- TiffPageSeries(pages, shape, page.dtype, axes, stype="Generic")
- )
- return result
- def _shaped_series(self):
- """Return image series in "shaped" file."""
- pages = self.pages
- pages.useframes = True
- lenpages = len(pages)
- def append_series(series, pages, axes, shape, reshape, name, truncated):
- page = pages[0]
- if not axes:
- shape = page.shape
- axes = page.axes
- if len(pages) > 1:
- shape = (len(pages),) + shape
- axes = "Q" + axes
- size = product(shape)
- resize = product(reshape)
- if page.is_contiguous and resize > size and resize % size == 0:
- if truncated is None:
- truncated = True
- axes = "Q" + axes
- shape = (resize // size,) + shape
- try:
- axes = reshape_axes(axes, shape, reshape)
- shape = reshape
- except ValueError as e:
- warnings.warn(str(e))
- series.append(
- TiffPageSeries(
- pages,
- shape,
- page.dtype,
- axes,
- name=name,
- stype="Shaped",
- truncated=truncated,
- )
- )
- keyframe = axes = shape = reshape = name = None
- series = []
- index = 0
- while True:
- if index >= lenpages:
- break
- # new keyframe; start of new series
- pages.keyframe = index
- keyframe = pages[index]
- if not keyframe.is_shaped:
- warnings.warn("invalid shape metadata or corrupted file")
- return
- # read metadata
- axes = None
- shape = None
- metadata = json_description_metadata(keyframe.is_shaped)
- name = metadata.get("name", "")
- reshape = metadata["shape"]
- truncated = metadata.get("truncated", None)
- if "axes" in metadata:
- axes = metadata["axes"]
- if len(axes) == len(reshape):
- shape = reshape
- else:
- axes = ""
- warnings.warn("axes do not match shape")
- # skip pages if possible
- spages = [keyframe]
- size = product(reshape)
- npages, mod = divmod(size, product(keyframe.shape))
- if mod:
- warnings.warn("series shape does not match page shape")
- return
- if 1 < npages <= lenpages - index:
- size *= keyframe._dtype.itemsize
- if truncated:
- npages = 1
- elif (
- keyframe.is_final
- and keyframe.offset + size < pages[index + 1].offset
- ):
- truncated = False
- else:
- # need to read all pages for series
- truncated = False
- for j in range(index + 1, index + npages):
- page = pages[j]
- page.keyframe = keyframe
- spages.append(page)
- append_series(series, spages, axes, shape, reshape, name, truncated)
- index += npages
- return series
- def _imagej_series(self):
- """Return image series in ImageJ file."""
- # ImageJ's dimension order is always TZCYXS
- # TODO: fix loading of color, composite, or palette images
- self.pages.useframes = True
- self.pages.keyframe = 0
- ij = self.imagej_metadata
- pages = self.pages
- page = pages[0]
- def is_hyperstack():
- # ImageJ hyperstack store all image metadata in the first page and
- # image data are stored contiguously before the second page, if any
- if not page.is_final:
- return False
- images = ij.get("images", 0)
- if images <= 1:
- return False
- offset, count = page.is_contiguous
- if (
- count != product(page.shape) * page.bitspersample // 8
- or offset + count * images > self.filehandle.size
- ):
- raise ValueError()
- # check that next page is stored after data
- if len(pages) > 1 and offset + count * images > pages[1].offset:
- return False
- return True
- try:
- hyperstack = is_hyperstack()
- except ValueError:
- warnings.warn("invalid ImageJ metadata or corrupted file")
- return
- if hyperstack:
- # no need to read other pages
- pages = [page]
- else:
- self.pages.load()
- shape = []
- axes = []
- if "frames" in ij:
- shape.append(ij["frames"])
- axes.append("T")
- if "slices" in ij:
- shape.append(ij["slices"])
- axes.append("Z")
- if "channels" in ij and not (
- page.photometric == 2 and not ij.get("hyperstack", False)
- ):
- shape.append(ij["channels"])
- axes.append("C")
- remain = ij.get("images", len(pages)) // (product(shape) if shape else 1)
- if remain > 1:
- shape.append(remain)
- axes.append("I")
- if page.axes[0] == "I":
- # contiguous multiple images
- shape.extend(page.shape[1:])
- axes.extend(page.axes[1:])
- elif page.axes[:2] == "SI":
- # color-mapped contiguous multiple images
- shape = page.shape[0:1] + tuple(shape) + page.shape[2:]
- axes = list(page.axes[0]) + axes + list(page.axes[2:])
- else:
- shape.extend(page.shape)
- axes.extend(page.axes)
- truncated = (
- hyperstack
- and len(self.pages) == 1
- and page.is_contiguous[1] != product(shape) * page.bitspersample // 8
- )
- return [
- TiffPageSeries(
- pages, shape, page.dtype, axes, stype="ImageJ", truncated=truncated
- )
- ]
- def _fluoview_series(self):
- """Return image series in FluoView file."""
- self.pages.useframes = True
- self.pages.keyframe = 0
- self.pages.load()
- mm = self.fluoview_metadata
- mmhd = list(reversed(mm["Dimensions"]))
- axes = "".join(
- TIFF.MM_DIMENSIONS.get(i[0].upper(), "Q") for i in mmhd if i[1] > 1
- )
- shape = tuple(int(i[1]) for i in mmhd if i[1] > 1)
- return [
- TiffPageSeries(
- self.pages,
- shape,
- self.pages[0].dtype,
- axes,
- name=mm["ImageName"],
- stype="FluoView",
- )
- ]
- def _mdgel_series(self):
- """Return image series in MD Gel file."""
- # only a single page, scaled according to metadata in second page
- self.pages.useframes = False
- self.pages.keyframe = 0
- self.pages.load()
- md = self.mdgel_metadata
- if md["FileTag"] in (2, 128):
- dtype = numpy.dtype("float32")
- scale = md["ScalePixel"]
- scale = scale[0] / scale[1] # rational
- if md["FileTag"] == 2:
- # squary root data format
- def transform(a):
- return a.astype("float32") ** 2 * scale
- else:
- def transform(a):
- return a.astype("float32") * scale
- else:
- transform = None
- page = self.pages[0]
- return [
- TiffPageSeries(
- [page], page.shape, dtype, page.axes, transform=transform, stype="MDGel"
- )
- ]
- def _nih_series(self):
- """Return image series in NIH file."""
- self.pages.useframes = True
- self.pages.keyframe = 0
- self.pages.load()
- page0 = self.pages[0]
- if len(self.pages) == 1:
- shape = page0.shape
- axes = page0.axes
- else:
- shape = (len(self.pages),) + page0.shape
- axes = "I" + page0.axes
- return [TiffPageSeries(self.pages, shape, page0.dtype, axes, stype="NIH")]
- def _ome_series(self):
- """Return image series in OME-TIFF file(s)."""
- from xml.etree import cElementTree as etree # delayed import
- omexml = self.pages[0].description
- try:
- root = etree.fromstring(omexml)
- except etree.ParseError as e:
- # TODO: test badly encoded OME-XML
- warnings.warn("ome-xml: %s" % e)
- try:
- # might work on Python 2
- omexml = omexml.decode("utf-8", "ignore").encode("utf-8")
- root = etree.fromstring(omexml)
- except Exception:
- return
- self.pages.useframes = True
- self.pages.keyframe = 0
- self.pages.load()
- uuid = root.attrib.get("UUID", None)
- self._files = {uuid: self}
- dirname = self._fh.dirname
- modulo = {}
- series = []
- for element in root:
- if element.tag.endswith("BinaryOnly"):
- # TODO: load OME-XML from master or companion file
- warnings.warn("ome-xml: not an ome-tiff master file")
- break
- if element.tag.endswith("StructuredAnnotations"):
- for annot in element:
- if not annot.attrib.get("Namespace", "").endswith("modulo"):
- continue
- for value in annot:
- for modul in value:
- for along in modul:
- if not along.tag[:-1].endswith("Along"):
- continue
- axis = along.tag[-1]
- newaxis = along.attrib.get("Type", "other")
- newaxis = TIFF.AXES_LABELS[newaxis]
- if "Start" in along.attrib:
- step = float(along.attrib.get("Step", 1))
- start = float(along.attrib["Start"])
- stop = float(along.attrib["End"]) + step
- labels = numpy.arange(start, stop, step)
- else:
- labels = [
- label.text
- for label in along
- if label.tag.endswith("Label")
- ]
- modulo[axis] = (newaxis, labels)
- if not element.tag.endswith("Image"):
- continue
- attr = element.attrib
- name = attr.get("Name", None)
- for pixels in element:
- if not pixels.tag.endswith("Pixels"):
- continue
- attr = pixels.attrib
- dtype = attr.get("PixelType", None)
- axes = "".join(reversed(attr["DimensionOrder"]))
- shape = list(int(attr["Size" + ax]) for ax in axes)
- size = product(shape[:-2])
- ifds = None
- spp = 1 # samples per pixel
- # FIXME: this implementation assumes the last two
- # dimensions are stored in tiff pages (shape[:-2]).
- # Apparently that is not always the case.
- for data in pixels:
- if data.tag.endswith("Channel"):
- attr = data.attrib
- if ifds is None:
- spp = int(attr.get("SamplesPerPixel", spp))
- ifds = [None] * (size // spp)
- elif int(attr.get("SamplesPerPixel", 1)) != spp:
- raise ValueError("cannot handle differing SamplesPerPixel")
- continue
- if ifds is None:
- ifds = [None] * (size // spp)
- if not data.tag.endswith("TiffData"):
- continue
- attr = data.attrib
- ifd = int(attr.get("IFD", 0))
- num = int(attr.get("NumPlanes", 1 if "IFD" in attr else 0))
- num = int(attr.get("PlaneCount", num))
- idx = [int(attr.get("First" + ax, 0)) for ax in axes[:-2]]
- try:
- idx = numpy.ravel_multi_index(idx, shape[:-2])
- except ValueError:
- # ImageJ produces invalid ome-xml when cropping
- warnings.warn("ome-xml: invalid TiffData index")
- continue
- for uuid in data:
- if not uuid.tag.endswith("UUID"):
- continue
- if uuid.text not in self._files:
- if not self._multifile:
- # abort reading multifile OME series
- # and fall back to generic series
- return []
- fname = uuid.attrib["FileName"]
- try:
- tif = TiffFile(os.path.join(dirname, fname))
- tif.pages.useframes = True
- tif.pages.keyframe = 0
- tif.pages.load()
- except (IOError, FileNotFoundError, ValueError):
- warnings.warn("ome-xml: failed to read '%s'" % fname)
- break
- self._files[uuid.text] = tif
- tif.close()
- pages = self._files[uuid.text].pages
- try:
- for i in range(num if num else len(pages)):
- ifds[idx + i] = pages[ifd + i]
- except IndexError:
- warnings.warn("ome-xml: index out of range")
- # only process first UUID
- break
- else:
- pages = self.pages
- try:
- for i in range(num if num else len(pages)):
- ifds[idx + i] = pages[ifd + i]
- except IndexError:
- warnings.warn("ome-xml: index out of range")
- if all(i is None for i in ifds):
- # skip images without data
- continue
- # set a keyframe on all IFDs
- keyframe = None
- for i in ifds:
- # try find a TiffPage
- if i and i == i.keyframe:
- keyframe = i
- break
- if not keyframe:
- # reload a TiffPage from file
- for i, keyframe in enumerate(ifds):
- if keyframe:
- keyframe.parent.pages.keyframe = keyframe.index
- keyframe = keyframe.parent.pages[keyframe.index]
- ifds[i] = keyframe
- break
- for i in ifds:
- if i is not None:
- i.keyframe = keyframe
- dtype = keyframe.dtype
- series.append(
- TiffPageSeries(
- ifds, shape, dtype, axes, parent=self, name=name, stype="OME"
- )
- )
- for serie in series:
- shape = list(serie.shape)
- for axis, (newaxis, labels) in modulo.items():
- i = serie.axes.index(axis)
- size = len(labels)
- if shape[i] == size:
- serie.axes = serie.axes.replace(axis, newaxis, 1)
- else:
- shape[i] //= size
- shape.insert(i + 1, size)
- serie.axes = serie.axes.replace(axis, axis + newaxis, 1)
- serie.shape = tuple(shape)
- # squeeze dimensions
- for serie in series:
- serie.shape, serie.axes = squeeze_axes(serie.shape, serie.axes)
- return series
- def _lsm_series(self):
- """Return main image series in LSM file. Skip thumbnails."""
- lsmi = self.lsm_metadata
- axes = TIFF.CZ_LSMINFO_SCANTYPE[lsmi["ScanType"]]
- if self.pages[0].photometric == 2: # RGB; more than one channel
- axes = axes.replace("C", "").replace("XY", "XYC")
- if lsmi.get("DimensionP", 0) > 1:
- axes += "P"
- if lsmi.get("DimensionM", 0) > 1:
- axes += "M"
- axes = axes[::-1]
- shape = tuple(int(lsmi[TIFF.CZ_LSMINFO_DIMENSIONS[i]]) for i in axes)
- name = lsmi.get("Name", "")
- self.pages.keyframe = 0
- pages = self.pages[::2]
- dtype = pages[0].dtype
- series = [TiffPageSeries(pages, shape, dtype, axes, name=name, stype="LSM")]
- if self.pages[1].is_reduced:
- self.pages.keyframe = 1
- pages = self.pages[1::2]
- dtype = pages[0].dtype
- cp, i = 1, 0
- while cp < len(pages) and i < len(shape) - 2:
- cp *= shape[i]
- i += 1
- shape = shape[:i] + pages[0].shape
- axes = axes[:i] + "CYX"
- series.append(
- TiffPageSeries(pages, shape, dtype, axes, name=name, stype="LSMreduced")
- )
- return series
- def _lsm_load_pages(self):
- """Load all pages from LSM file."""
- self.pages.cache = True
- self.pages.useframes = True
- # second series: thumbnails
- self.pages.keyframe = 1
- keyframe = self.pages[1]
- for page in self.pages[1::2]:
- page.keyframe = keyframe
- # first series: data
- self.pages.keyframe = 0
- keyframe = self.pages[0]
- for page in self.pages[::2]:
- page.keyframe = keyframe
- def _lsm_fix_strip_offsets(self):
- """Unwrap strip offsets for LSM files greater than 4 GB.
- Each series and position require separate unwrapping (undocumented).
- """
- if self.filehandle.size < 2**32:
- return
- pages = self.pages
- npages = len(pages)
- series = self.series[0]
- axes = series.axes
- # find positions
- positions = 1
- for i in 0, 1:
- if series.axes[i] in "PM":
- positions *= series.shape[i]
- # make time axis first
- if positions > 1:
- ntimes = 0
- for i in 1, 2:
- if axes[i] == "T":
- ntimes = series.shape[i]
- break
- if ntimes:
- div, mod = divmod(npages, 2 * positions * ntimes)
- assert mod == 0
- shape = (positions, ntimes, div, 2)
- indices = numpy.arange(product(shape)).reshape(shape)
- indices = numpy.moveaxis(indices, 1, 0)
- else:
- indices = numpy.arange(npages).reshape(-1, 2)
- # images of reduced page might be stored first
- if pages[0].dataoffsets[0] > pages[1].dataoffsets[0]:
- indices = indices[..., ::-1]
- # unwrap offsets
- wrap = 0
- previousoffset = 0
- for i in indices.flat:
- page = pages[i]
- dataoffsets = []
- for currentoffset in page.dataoffsets:
- if currentoffset < previousoffset:
- wrap += 2**32
- dataoffsets.append(currentoffset + wrap)
- previousoffset = currentoffset
- page.dataoffsets = tuple(dataoffsets)
- def _lsm_fix_strip_bytecounts(self):
- """Set databytecounts to size of compressed data.
- The StripByteCounts tag in LSM files contains the number of bytes
- for the uncompressed data.
- """
- pages = self.pages
- if pages[0].compression == 1:
- return
- # sort pages by first strip offset
- pages = sorted(pages, key=lambda p: p.dataoffsets[0])
- npages = len(pages) - 1
- for i, page in enumerate(pages):
- if page.index % 2:
- continue
- offsets = page.dataoffsets
- bytecounts = page.databytecounts
- if i < npages:
- lastoffset = pages[i + 1].dataoffsets[0]
- else:
- # LZW compressed strips might be longer than uncompressed
- lastoffset = min(offsets[-1] + 2 * bytecounts[-1], self._fh.size)
- offsets = offsets + (lastoffset,)
- page.databytecounts = tuple(
- offsets[j + 1] - offsets[j] for j in range(len(bytecounts))
- )
- def __getattr__(self, name):
- """Return 'is_flag' attributes from first page."""
- if name[3:] in TIFF.FILE_FLAGS:
- if not self.pages:
- return False
- value = bool(getattr(self.pages[0], name))
- setattr(self, name, value)
- return value
- raise AttributeError(
- "'%s' object has no attribute '%s'" % (self.__class__.__name__, name)
- )
- def __enter__(self):
- return self
- def __exit__(self, exc_type, exc_value, traceback):
- self.close()
- def __str__(self, detail=0, width=79):
- """Return string containing information about file.
- The detail parameter specifies the level of detail returned:
- 0: file only.
- 1: all series, first page of series and its tags.
- 2: large tag values and file metadata.
- 3: all pages.
- """
- info = [
- "TiffFile '%s'",
- format_size(self._fh.size),
- {"<": "LittleEndian", ">": "BigEndian"}[self.byteorder],
- ]
- if self.is_bigtiff:
- info.append("BigTiff")
- info.append("|".join(f.upper() for f in self.flags))
- if len(self.pages) > 1:
- info.append("%i Pages" % len(self.pages))
- if len(self.series) > 1:
- info.append("%i Series" % len(self.series))
- if len(self._files) > 1:
- info.append("%i Files" % (len(self._files)))
- info = " ".join(info)
- info = info.replace(" ", " ").replace(" ", " ")
- info = info % snipstr(self._fh.name, max(12, width + 2 - len(info)))
- if detail <= 0:
- return info
- info = [info]
- info.append("\n".join(str(s) for s in self.series))
- if detail >= 3:
- info.extend(
- (
- TiffPage.__str__(p, detail=detail, width=width)
- for p in self.pages
- if p is not None
- )
- )
- else:
- info.extend(
- (
- TiffPage.__str__(s.pages[0], detail=detail, width=width)
- for s in self.series
- if s.pages[0] is not None
- )
- )
- if detail >= 2:
- for name in sorted(self.flags):
- if hasattr(self, name + "_metadata"):
- m = getattr(self, name + "_metadata")
- if m:
- info.append(
- "%s_METADATA\n%s"
- % (
- name.upper(),
- pformat(m, width=width, height=detail * 12),
- )
- )
- return "\n\n".join(info).replace("\n\n\n", "\n\n")
- @lazyattr
- def flags(self):
- """Return set of file flags."""
- return set(
- name.lower()
- for name in sorted(TIFF.FILE_FLAGS)
- if getattr(self, "is_" + name)
- )
- @lazyattr
- def is_mdgel(self):
- """File has MD Gel format."""
- try:
- return self.pages[0].is_mdgel or self.pages[1].is_mdgel
- except IndexError:
- return False
- @property
- def is_movie(self):
- """Return if file is a movie."""
- return self.pages.useframes
- @lazyattr
- def shaped_metadata(self):
- """Return Tifffile metadata from JSON descriptions as dicts."""
- if not self.is_shaped:
- return
- return tuple(
- json_description_metadata(s.pages[0].is_shaped)
- for s in self.series
- if s.stype.lower() == "shaped"
- )
- @lazyattr
- def ome_metadata(self):
- """Return OME XML as dict."""
- # TODO: remove this or return XML?
- if not self.is_ome:
- return
- return xml2dict(self.pages[0].description)["OME"]
- @lazyattr
- def qptiff_metadata(self):
- """Return PerkinElmer-QPI-ImageDescription XML element as dict."""
- if not self.is_qptiff:
- return
- root = "PerkinElmer-QPI-ImageDescription"
- xml = self.pages[0].description.replace(" " + root + " ", root)
- return xml2dict(xml)[root]
- @lazyattr
- def lsm_metadata(self):
- """Return LSM metadata from CZ_LSMINFO tag as dict."""
- if not self.is_lsm:
- return
- return self.pages[0].tags["CZ_LSMINFO"].value
- @lazyattr
- def stk_metadata(self):
- """Return STK metadata from UIC tags as dict."""
- if not self.is_stk:
- return
- page = self.pages[0]
- tags = page.tags
- result = {}
- result["NumberPlanes"] = tags["UIC2tag"].count
- if page.description:
- result["PlaneDescriptions"] = page.description.split("\0")
- # result['plane_descriptions'] = stk_description_metadata(
- # page.image_description)
- if "UIC1tag" in tags:
- result.update(tags["UIC1tag"].value)
- if "UIC3tag" in tags:
- result.update(tags["UIC3tag"].value) # wavelengths
- if "UIC4tag" in tags:
- result.update(tags["UIC4tag"].value) # override uic1 tags
- uic2tag = tags["UIC2tag"].value
- result["ZDistance"] = uic2tag["ZDistance"]
- result["TimeCreated"] = uic2tag["TimeCreated"]
- result["TimeModified"] = uic2tag["TimeModified"]
- try:
- result["DatetimeCreated"] = numpy.array(
- [
- julian_datetime(*dt)
- for dt in zip(uic2tag["DateCreated"], uic2tag["TimeCreated"])
- ],
- dtype="datetime64[ns]",
- )
- result["DatetimeModified"] = numpy.array(
- [
- julian_datetime(*dt)
- for dt in zip(uic2tag["DateModified"], uic2tag["TimeModified"])
- ],
- dtype="datetime64[ns]",
- )
- except ValueError as e:
- warnings.warn("stk_metadata: %s" % e)
- return result
- @lazyattr
- def imagej_metadata(self):
- """Return consolidated ImageJ metadata as dict."""
- if not self.is_imagej:
- return
- page = self.pages[0]
- result = imagej_description_metadata(page.is_imagej)
- if "IJMetadata" in page.tags:
- try:
- result.update(page.tags["IJMetadata"].value)
- except Exception:
- pass
- return result
- @lazyattr
- def fluoview_metadata(self):
- """Return consolidated FluoView metadata as dict."""
- if not self.is_fluoview:
- return
- result = {}
- page = self.pages[0]
- result.update(page.tags["MM_Header"].value)
- # TODO: read stamps from all pages
- result["Stamp"] = page.tags["MM_Stamp"].value
- # skip parsing image description; not reliable
- # try:
- # t = fluoview_description_metadata(page.image_description)
- # if t is not None:
- # result['ImageDescription'] = t
- # except Exception as e:
- # warnings.warn(
- # "failed to read FluoView image description: %s" % e)
- return result
- @lazyattr
- def nih_metadata(self):
- """Return NIH Image metadata from NIHImageHeader tag as dict."""
- if not self.is_nih:
- return
- return self.pages[0].tags["NIHImageHeader"].value
- @lazyattr
- def fei_metadata(self):
- """Return FEI metadata from SFEG or HELIOS tags as dict."""
- if not self.is_fei:
- return
- tags = self.pages[0].tags
- if "FEI_SFEG" in tags:
- return tags["FEI_SFEG"].value
- if "FEI_HELIOS" in tags:
- return tags["FEI_HELIOS"].value
- @lazyattr
- def sem_metadata(self):
- """Return SEM metadata from CZ_SEM tag as dict."""
- if not self.is_sem:
- return
- return self.pages[0].tags["CZ_SEM"].value
- @lazyattr
- def mdgel_metadata(self):
- """Return consolidated metadata from MD GEL tags as dict."""
- for page in self.pages[:2]:
- if "MDFileTag" in page.tags:
- tags = page.tags
- break
- else:
- return
- result = {}
- for code in range(33445, 33453):
- name = TIFF.TAGS[code]
- if name not in tags:
- continue
- result[name[2:]] = tags[name].value
- return result
- @lazyattr
- def andor_metadata(self):
- """Return Andor tags as dict."""
- return self.pages[0].andor_tags
- @lazyattr
- def epics_metadata(self):
- """Return EPICS areaDetector tags as dict."""
- return self.pages[0].epics_tags
- @lazyattr
- def tvips_metadata(self):
- """Return TVIPS tag as dict."""
- if not self.is_tvips:
- return
- return self.pages[0].tags["TVIPS"].value
- @lazyattr
- def metaseries_metadata(self):
- """Return MetaSeries metadata from image description as dict."""
- if not self.is_metaseries:
- return
- return metaseries_description_metadata(self.pages[0].description)
- @lazyattr
- def pilatus_metadata(self):
- """Return Pilatus metadata from image description as dict."""
- if not self.is_pilatus:
- return
- return pilatus_description_metadata(self.pages[0].description)
- @lazyattr
- def micromanager_metadata(self):
- """Return consolidated MicroManager metadata as dict."""
- if not self.is_micromanager:
- return
- # from file header
- result = read_micromanager_metadata(self._fh)
- # from tag
- result.update(self.pages[0].tags["MicroManagerMetadata"].value)
- return result
- @lazyattr
- def scanimage_metadata(self):
- """Return ScanImage non-varying frame and ROI metadata as dict."""
- if not self.is_scanimage:
- return
- result = {}
- try:
- framedata, roidata = read_scanimage_metadata(self._fh)
- result["FrameData"] = framedata
- result.update(roidata)
- except ValueError:
- pass
- # TODO: scanimage_artist_metadata
- try:
- result["Description"] = scanimage_description_metadata(
- self.pages[0].description
- )
- except Exception as e:
- warnings.warn("scanimage_description_metadata failed: %s" % e)
- return result
- @property
- def geotiff_metadata(self):
- """Return GeoTIFF metadata from first page as dict."""
- if not self.is_geotiff:
- return
- return self.pages[0].geotiff_tags
- class TiffPages(object):
- """Sequence of TIFF image file directories."""
- def __init__(self, parent):
- """Initialize instance from file. Read first TiffPage from file.
- The file position must be at an offset to an offset to a TiffPage.
- """
- self.parent = parent
- self.pages = [] # cache of TiffPages, TiffFrames, or their offsets
- self.complete = False # True if offsets to all pages were read
- self._tiffpage = TiffPage # class for reading tiff pages
- self._keyframe = None
- self._cache = True
- # read offset to first page
- fh = parent.filehandle
- self._nextpageoffset = fh.tell()
- offset = struct.unpack(parent.offsetformat, fh.read(parent.offsetsize))[0]
- if offset == 0:
- # warnings.warn('file contains no pages')
- self.complete = True
- return
- if offset >= fh.size:
- warnings.warn("invalid page offset (%i)" % offset)
- self.complete = True
- return
- # always read and cache first page
- fh.seek(offset)
- page = TiffPage(parent, index=0)
- self.pages.append(page)
- self._keyframe = page
- @property
- def cache(self):
- """Return if pages/frames are currently being cached."""
- return self._cache
- @cache.setter
- def cache(self, value):
- """Enable or disable caching of pages/frames. Clear cache if False."""
- value = bool(value)
- if self._cache and not value:
- self.clear()
- self._cache = value
- @property
- def useframes(self):
- """Return if currently using TiffFrame (True) or TiffPage (False)."""
- return self._tiffpage == TiffFrame and TiffFrame is not TiffPage
- @useframes.setter
- def useframes(self, value):
- """Set to use TiffFrame (True) or TiffPage (False)."""
- self._tiffpage = TiffFrame if value else TiffPage
- @property
- def keyframe(self):
- """Return index of current keyframe."""
- return self._keyframe.index
- @keyframe.setter
- def keyframe(self, index):
- """Set current keyframe. Load TiffPage from file if necessary."""
- if self._keyframe.index == index:
- return
- if self.complete or 0 <= index < len(self.pages):
- page = self.pages[index]
- if isinstance(page, TiffPage):
- self._keyframe = page
- return
- elif isinstance(page, TiffFrame):
- # remove existing frame
- self.pages[index] = page.offset
- # load TiffPage from file
- useframes = self.useframes
- self._tiffpage = TiffPage
- self._keyframe = self[index]
- self.useframes = useframes
- @property
- def next_page_offset(self):
- """Return offset where offset to a new page can be stored."""
- if not self.complete:
- self._seek(-1)
- return self._nextpageoffset
- def load(self):
- """Read all remaining pages from file."""
- fh = self.parent.filehandle
- keyframe = self._keyframe
- pages = self.pages
- if not self.complete:
- self._seek(-1)
- for i, page in enumerate(pages):
- if isinstance(page, inttypes):
- fh.seek(page)
- page = self._tiffpage(self.parent, index=i, keyframe=keyframe)
- pages[i] = page
- def clear(self, fully=True):
- """Delete all but first page from cache. Set keyframe to first page."""
- pages = self.pages
- if not self._cache or len(pages) < 1:
- return
- self._keyframe = pages[0]
- if fully:
- # delete all but first TiffPage/TiffFrame
- for i, page in enumerate(pages[1:]):
- if not isinstance(page, inttypes):
- pages[i + 1] = page.offset
- elif TiffFrame is not TiffPage:
- # delete only TiffFrames
- for i, page in enumerate(pages):
- if isinstance(page, TiffFrame):
- pages[i] = page.offset
- def _seek(self, index, maxpages=2**22):
- """Seek file to offset of specified page."""
- pages = self.pages
- if not pages:
- return
- fh = self.parent.filehandle
- if fh.closed:
- raise RuntimeError("FileHandle is closed")
- if self.complete or 0 <= index < len(pages):
- page = pages[index]
- offset = page if isinstance(page, inttypes) else page.offset
- fh.seek(offset)
- return
- offsetformat = self.parent.offsetformat
- offsetsize = self.parent.offsetsize
- tagnoformat = self.parent.tagnoformat
- tagnosize = self.parent.tagnosize
- tagsize = self.parent.tagsize
- unpack = struct.unpack
- page = pages[-1]
- offset = page if isinstance(page, inttypes) else page.offset
- while len(pages) < maxpages:
- # read offsets to pages from file until index is reached
- fh.seek(offset)
- # skip tags
- try:
- tagno = unpack(tagnoformat, fh.read(tagnosize))[0]
- if tagno > 4096:
- raise ValueError("suspicious number of tags")
- except Exception:
- warnings.warn("corrupted tag list at offset %i" % offset)
- del pages[-1]
- self.complete = True
- break
- self._nextpageoffset = offset + tagnosize + tagno * tagsize
- fh.seek(self._nextpageoffset)
- # read offset to next page
- offset = unpack(offsetformat, fh.read(offsetsize))[0]
- if offset == 0:
- self.complete = True
- break
- if offset >= fh.size:
- warnings.warn("invalid page offset (%i)" % offset)
- self.complete = True
- break
- pages.append(offset)
- if 0 <= index < len(pages):
- break
- if index >= len(pages):
- raise IndexError("list index out of range")
- page = pages[index]
- fh.seek(page if isinstance(page, inttypes) else page.offset)
- def __bool__(self):
- """Return True if file contains any pages."""
- return len(self.pages) > 0
- def __len__(self):
- """Return number of pages in file."""
- if not self.complete:
- self._seek(-1)
- return len(self.pages)
- def __getitem__(self, key):
- """Return specified page(s) from cache or file."""
- pages = self.pages
- if not pages:
- raise IndexError("list index out of range")
- if key == 0:
- return pages[key]
- if isinstance(key, slice):
- start, stop, _ = key.indices(2**31 - 1)
- if not self.complete and max(stop, start) > len(pages):
- self._seek(-1)
- return [self[i] for i in range(*key.indices(len(pages)))]
- if self.complete and key >= len(pages):
- raise IndexError("list index out of range")
- try:
- page = pages[key]
- except IndexError:
- page = 0
- if not isinstance(page, inttypes):
- return page
- self._seek(key)
- page = self._tiffpage(self.parent, index=key, keyframe=self._keyframe)
- if self._cache:
- pages[key] = page
- return page
- def __iter__(self):
- """Return iterator over all pages."""
- i = 0
- while True:
- try:
- yield self[i]
- i += 1
- except IndexError:
- break
- class TiffPage(object):
- """TIFF image file directory (IFD).
- Attributes
- ----------
- index : int
- Index of page in file.
- dtype : numpy.dtype or None
- Data type (native byte order) of the image in IFD.
- shape : tuple
- Dimensions of the image in IFD.
- axes : str
- Axes label codes:
- 'X' width, 'Y' height, 'S' sample, 'I' image series|page|plane,
- 'Z' depth, 'C' color|em-wavelength|channel, 'E' ex-wavelength|lambda,
- 'T' time, 'R' region|tile, 'A' angle, 'P' phase, 'H' lifetime,
- 'L' exposure, 'V' event, 'Q' unknown, '_' missing
- tags : dict
- Dictionary of tags in IFD. {tag.name: TiffTag}
- colormap : numpy.ndarray
- Color look up table, if exists.
- All attributes are read-only.
- Notes
- -----
- The internal, normalized '_shape' attribute is 6 dimensional:
- 0 : number planes/images (stk, ij).
- 1 : planar samplesperpixel.
- 2 : imagedepth Z (sgi).
- 3 : imagelength Y.
- 4 : imagewidth X.
- 5 : contig samplesperpixel.
- """
- # default properties; will be updated from tags
- imagewidth = 0
- imagelength = 0
- imagedepth = 1
- tilewidth = 0
- tilelength = 0
- tiledepth = 1
- bitspersample = 1
- samplesperpixel = 1
- sampleformat = 1
- rowsperstrip = 2**32 - 1
- compression = 1
- planarconfig = 1
- fillorder = 1
- photometric = 0
- predictor = 1
- extrasamples = 1
- colormap = None
- software = ""
- description = ""
- description1 = ""
- def __init__(self, parent, index, keyframe=None):
- """Initialize instance from file.
- The file handle position must be at offset to a valid IFD.
- """
- self.parent = parent
- self.index = index
- self.shape = ()
- self._shape = ()
- self.dtype = None
- self._dtype = None
- self.axes = ""
- self.tags = {}
- self.dataoffsets = ()
- self.databytecounts = ()
- # read TIFF IFD structure and its tags from file
- fh = parent.filehandle
- self.offset = fh.tell() # offset to this IFD
- try:
- tagno = struct.unpack(parent.tagnoformat, fh.read(parent.tagnosize))[0]
- if tagno > 4096:
- raise ValueError("suspicious number of tags")
- except Exception:
- raise ValueError("corrupted tag list at offset %i" % self.offset)
- tagsize = parent.tagsize
- data = fh.read(tagsize * tagno)
- tags = self.tags
- index = -tagsize
- for _ in range(tagno):
- index += tagsize
- try:
- tag = TiffTag(self.parent, data[index : index + tagsize])
- except TiffTag.Error as e:
- warnings.warn(str(e))
- continue
- tagname = tag.name
- if tagname not in tags:
- name = tagname
- tags[name] = tag
- else:
- # some files contain multiple tags with same code
- # e.g. MicroManager files contain two ImageDescription tags
- i = 1
- while True:
- name = "%s%i" % (tagname, i)
- if name not in tags:
- tags[name] = tag
- break
- name = TIFF.TAG_ATTRIBUTES.get(name, "")
- if name:
- if name[:3] in "sof des" and not isinstance(tag.value, str):
- pass # wrong string type for software, description
- else:
- setattr(self, name, tag.value)
- if not tags:
- return # found in FIBICS
- # consolidate private tags; remove them from self.tags
- if self.is_andor:
- self.andor_tags
- elif self.is_epics:
- self.epics_tags
- if self.is_lsm or (self.index and self.parent.is_lsm):
- # correct non standard LSM bitspersample tags
- self.tags["BitsPerSample"]._fix_lsm_bitspersample(self)
- if self.is_vista or (self.index and self.parent.is_vista):
- # ISS Vista writes wrong ImageDepth tag
- self.imagedepth = 1
- if self.is_stk and "UIC1tag" in tags and not tags["UIC1tag"].value:
- # read UIC1tag now that plane count is known
- uic1tag = tags["UIC1tag"]
- fh.seek(uic1tag.valueoffset)
- tags["UIC1tag"].value = read_uic1tag(
- fh,
- self.parent.byteorder,
- uic1tag.dtype,
- uic1tag.count,
- None,
- tags["UIC2tag"].count,
- )
- if "IJMetadata" in tags:
- # decode IJMetadata tag
- try:
- tags["IJMetadata"].value = imagej_metadata(
- tags["IJMetadata"].value,
- tags["IJMetadataByteCounts"].value,
- self.parent.byteorder,
- )
- except Exception as e:
- warnings.warn(str(e))
- if "BitsPerSample" in tags:
- tag = tags["BitsPerSample"]
- if tag.count == 1:
- self.bitspersample = tag.value
- else:
- # LSM might list more items than samplesperpixel
- value = tag.value[: self.samplesperpixel]
- if any((v - value[0] for v in value)):
- self.bitspersample = value
- else:
- self.bitspersample = value[0]
- if "SampleFormat" in tags:
- tag = tags["SampleFormat"]
- if tag.count == 1:
- self.sampleformat = tag.value
- else:
- value = tag.value[: self.samplesperpixel]
- if any((v - value[0] for v in value)):
- self.sampleformat = value
- else:
- self.sampleformat = value[0]
- if "ImageLength" in tags:
- if "RowsPerStrip" not in tags or tags["RowsPerStrip"].count > 1:
- self.rowsperstrip = self.imagelength
- # self.stripsperimage = int(math.floor(
- # float(self.imagelength + self.rowsperstrip - 1) /
- # self.rowsperstrip))
- # determine dtype
- dtype = self.sampleformat, self.bitspersample
- dtype = TIFF.SAMPLE_DTYPES.get(dtype, None)
- if dtype is not None:
- dtype = numpy.dtype(dtype)
- self.dtype = self._dtype = dtype
- # determine shape of data
- imagelength = self.imagelength
- imagewidth = self.imagewidth
- imagedepth = self.imagedepth
- samplesperpixel = self.samplesperpixel
- if self.is_stk:
- assert self.imagedepth == 1
- uictag = tags["UIC2tag"].value
- planes = tags["UIC2tag"].count
- if self.planarconfig == 1:
- self._shape = (planes, 1, 1, imagelength, imagewidth, samplesperpixel)
- if samplesperpixel == 1:
- self.shape = (planes, imagelength, imagewidth)
- self.axes = "YX"
- else:
- self.shape = (planes, imagelength, imagewidth, samplesperpixel)
- self.axes = "YXS"
- else:
- self._shape = (planes, samplesperpixel, 1, imagelength, imagewidth, 1)
- if samplesperpixel == 1:
- self.shape = (planes, imagelength, imagewidth)
- self.axes = "YX"
- else:
- self.shape = (planes, samplesperpixel, imagelength, imagewidth)
- self.axes = "SYX"
- # detect type of series
- if planes == 1:
- self.shape = self.shape[1:]
- elif numpy.all(uictag["ZDistance"] != 0):
- self.axes = "Z" + self.axes
- elif numpy.all(numpy.diff(uictag["TimeCreated"]) != 0):
- self.axes = "T" + self.axes
- else:
- self.axes = "I" + self.axes
- elif self.photometric == 2 or samplesperpixel > 1: # PHOTOMETRIC.RGB
- if self.planarconfig == 1:
- self._shape = (
- 1,
- 1,
- imagedepth,
- imagelength,
- imagewidth,
- samplesperpixel,
- )
- if imagedepth == 1:
- self.shape = (imagelength, imagewidth, samplesperpixel)
- self.axes = "YXS"
- else:
- self.shape = (imagedepth, imagelength, imagewidth, samplesperpixel)
- self.axes = "ZYXS"
- else:
- self._shape = (
- 1,
- samplesperpixel,
- imagedepth,
- imagelength,
- imagewidth,
- 1,
- )
- if imagedepth == 1:
- self.shape = (samplesperpixel, imagelength, imagewidth)
- self.axes = "SYX"
- else:
- self.shape = (samplesperpixel, imagedepth, imagelength, imagewidth)
- self.axes = "SZYX"
- else:
- self._shape = (1, 1, imagedepth, imagelength, imagewidth, 1)
- if imagedepth == 1:
- self.shape = (imagelength, imagewidth)
- self.axes = "YX"
- else:
- self.shape = (imagedepth, imagelength, imagewidth)
- self.axes = "ZYX"
- # dataoffsets and databytecounts
- if "TileOffsets" in tags:
- self.dataoffsets = tags["TileOffsets"].value
- elif "StripOffsets" in tags:
- self.dataoffsets = tags["StripOffsets"].value
- else:
- self.dataoffsets = (0,)
- if "TileByteCounts" in tags:
- self.databytecounts = tags["TileByteCounts"].value
- elif "StripByteCounts" in tags:
- self.databytecounts = tags["StripByteCounts"].value
- else:
- self.databytecounts = (product(self.shape) * (self.bitspersample // 8),)
- if self.compression != 1:
- warnings.warn("required ByteCounts tag is missing")
- assert len(self.shape) == len(self.axes)
- def asarray(
- self,
- out=None,
- squeeze=True,
- lock=None,
- reopen=True,
- maxsize=2**44,
- validate=True,
- ):
- """Read image data from file and return as numpy array.
- Raise ValueError if format is unsupported.
- Parameters
- ----------
- out : numpy.ndarray, str, or file-like object; optional
- Buffer where image data will be saved.
- If None (default), a new array will be created.
- If numpy.ndarray, a writable array of compatible dtype and shape.
- If 'memmap', directly memory-map the image data in the TIFF file
- if possible; else create a memory-mapped array in a temporary file.
- If str or open file, the file name or file object used to
- create a memory-map to an array stored in a binary file on disk.
- squeeze : bool
- If True, all length-1 dimensions (except X and Y) are
- squeezed out from the array.
- If False, the shape of the returned array might be different from
- the page.shape.
- lock : {RLock, NullContext}
- A reentrant lock used to synchronize reads from file.
- If None (default), the lock of the parent's filehandle is used.
- reopen : bool
- If True (default) and the parent file handle is closed, the file
- is temporarily re-opened and closed if no exception occurs.
- maxsize: int or None
- Maximum size of data before a ValueError is raised.
- Can be used to catch DOS. Default: 16 TB.
- validate : bool
- If True (default), validate various parameters.
- If None, only validate parameters and return None.
- """
- self_ = self
- self = self.keyframe # self or keyframe
- if not self._shape or product(self._shape) == 0:
- return
- tags = self.tags
- if validate or validate is None:
- if maxsize and product(self._shape) > maxsize:
- raise ValueError("data are too large %s" % str(self._shape))
- if self.dtype is None:
- raise ValueError(
- "data type not supported: %s%i"
- % (self.sampleformat, self.bitspersample)
- )
- if self.compression not in TIFF.DECOMPESSORS:
- raise ValueError("cannot decompress %s" % self.compression.name)
- if "SampleFormat" in tags:
- tag = tags["SampleFormat"]
- if tag.count != 1 and any((i - tag.value[0] for i in tag.value)):
- raise ValueError("sample formats do not match %s" % tag.value)
- if self.is_chroma_subsampled and (
- self.compression != 7 or self.planarconfig == 2
- ):
- raise NotImplementedError("chroma subsampling not supported")
- if validate is None:
- return
- fh = self_.parent.filehandle
- lock = fh.lock if lock is None else lock
- with lock:
- closed = fh.closed
- if closed:
- if reopen:
- fh.open()
- else:
- raise IOError("file handle is closed")
- dtype = self._dtype
- shape = self._shape
- imagewidth = self.imagewidth
- imagelength = self.imagelength
- imagedepth = self.imagedepth
- bitspersample = self.bitspersample
- typecode = self.parent.byteorder + dtype.char
- lsb2msb = self.fillorder == 2
- offsets, bytecounts = self_.offsets_bytecounts
- istiled = self.is_tiled
- if istiled:
- tilewidth = self.tilewidth
- tilelength = self.tilelength
- tiledepth = self.tiledepth
- tw = (imagewidth + tilewidth - 1) // tilewidth
- tl = (imagelength + tilelength - 1) // tilelength
- td = (imagedepth + tiledepth - 1) // tiledepth
- shape = (
- shape[0],
- shape[1],
- td * tiledepth,
- tl * tilelength,
- tw * tilewidth,
- shape[-1],
- )
- tileshape = (tiledepth, tilelength, tilewidth, shape[-1])
- runlen = tilewidth
- else:
- runlen = imagewidth
- if self.planarconfig == 1:
- runlen *= self.samplesperpixel
- if out == "memmap" and self.is_memmappable:
- with lock:
- result = fh.memmap_array(typecode, shape, offset=offsets[0])
- elif self.is_contiguous:
- if out is not None:
- out = create_output(out, shape, dtype)
- with lock:
- fh.seek(offsets[0])
- result = fh.read_array(typecode, product(shape), out=out)
- if out is None and not result.dtype.isnative:
- # swap byte order and dtype without copy
- result.byteswap(True)
- result = result.view(result.dtype.newbyteorder())
- if lsb2msb:
- reverse_bitorder(result)
- else:
- result = create_output(out, shape, dtype)
- decompress = TIFF.DECOMPESSORS[self.compression]
- if self.compression == 7: # COMPRESSION.JPEG
- if bitspersample not in (8, 12):
- raise ValueError("unsupported JPEG precision %i" % bitspersample)
- if "JPEGTables" in tags:
- table = tags["JPEGTables"].value
- else:
- table = b""
- unpack = identityfunc
- colorspace = TIFF.PHOTOMETRIC(self.photometric).name
- def decompress(
- x,
- func=decompress,
- table=table,
- bitspersample=bitspersample,
- colorspace=colorspace,
- ):
- return func(x, table, bitspersample, colorspace).reshape(-1)
- elif bitspersample in (8, 16, 32, 64, 128):
- if (bitspersample * runlen) % 8:
- raise ValueError("data and sample size mismatch")
- def unpack(x, typecode=typecode):
- if self.predictor == 3: # PREDICTOR.FLOATINGPOINT
- # the floating point horizontal differencing decoder
- # needs the raw byte order
- typecode = dtype.char
- try:
- # read only numpy array
- return numpy.frombuffer(x, typecode)
- except ValueError:
- # strips may be missing EOI
- # warnings.warn('unpack: %s' % e)
- xlen = (len(x) // (bitspersample // 8)) * (bitspersample // 8)
- return numpy.frombuffer(x[:xlen], typecode)
- elif isinstance(bitspersample, tuple):
- def unpack(x, typecode=typecode, bitspersample=bitspersample):
- return unpack_rgb(x, typecode, bitspersample)
- else:
- def unpack(
- x, typecode=typecode, bitspersample=bitspersample, runlen=runlen
- ):
- return unpack_ints(x, typecode, bitspersample, runlen)
- if istiled:
- writable = None
- tw, tl, td, pl = 0, 0, 0, 0
- for tile in buffered_read(fh, lock, offsets, bytecounts):
- if lsb2msb:
- tile = reverse_bitorder(tile)
- tile = decompress(tile)
- tile = unpack(tile)
- try:
- tile.shape = tileshape
- except ValueError:
- # incomplete tiles; see gdal issue #1179
- warnings.warn("invalid tile data")
- t = numpy.zeros(tileshape, dtype).reshape(-1)
- s = min(tile.size, t.size)
- t[:s] = tile[:s]
- tile = t.reshape(tileshape)
- if self.predictor == 2: # PREDICTOR.HORIZONTAL
- if writable is None:
- writable = tile.flags["WRITEABLE"]
- if writable:
- numpy.cumsum(tile, axis=-2, dtype=dtype, out=tile)
- else:
- tile = numpy.cumsum(tile, axis=-2, dtype=dtype)
- elif self.predictor == 3: # PREDICTOR.FLOATINGPOINT
- raise NotImplementedError()
- result[
- 0,
- pl,
- td : td + tiledepth,
- tl : tl + tilelength,
- tw : tw + tilewidth,
- :,
- ] = tile
- del tile
- tw += tilewidth
- if tw >= shape[4]:
- tw, tl = 0, tl + tilelength
- if tl >= shape[3]:
- tl, td = 0, td + tiledepth
- if td >= shape[2]:
- td, pl = 0, pl + 1
- result = result[..., :imagedepth, :imagelength, :imagewidth, :]
- else:
- strip_size = self.rowsperstrip * self.imagewidth
- if self.planarconfig == 1:
- strip_size *= self.samplesperpixel
- result = result.reshape(-1)
- index = 0
- for strip in buffered_read(fh, lock, offsets, bytecounts):
- if lsb2msb:
- strip = reverse_bitorder(strip)
- strip = decompress(strip)
- strip = unpack(strip)
- size = min(result.size, strip.size, strip_size, result.size - index)
- result[index : index + size] = strip[:size]
- del strip
- index += size
- result.shape = self._shape
- if self.predictor != 1 and not (istiled and not self.is_contiguous):
- if self.parent.is_lsm and self.compression == 1:
- pass # work around bug in LSM510 software
- elif self.predictor == 2: # PREDICTOR.HORIZONTAL
- numpy.cumsum(result, axis=-2, dtype=dtype, out=result)
- elif self.predictor == 3: # PREDICTOR.FLOATINGPOINT
- result = decode_floats(result)
- if squeeze:
- try:
- result.shape = self.shape
- except ValueError:
- warnings.warn(
- "failed to reshape from %s to %s"
- % (str(result.shape), str(self.shape))
- )
- if closed:
- # TODO: file should remain open if an exception occurred above
- fh.close()
- return result
- def asrgb(
- self,
- uint8=False,
- alpha=None,
- colormap=None,
- dmin=None,
- dmax=None,
- *args,
- **kwargs,
- ):
- """Return image data as RGB(A).
- Work in progress.
- """
- data = self.asarray(*args, **kwargs)
- self = self.keyframe # self or keyframe
- photometric = self.photometric
- PHOTOMETRIC = TIFF.PHOTOMETRIC
- if photometric == PHOTOMETRIC.PALETTE:
- colormap = self.colormap
- if colormap.shape[1] < 2**self.bitspersample or self.dtype.char not in "BH":
- raise ValueError("cannot apply colormap")
- if uint8:
- if colormap.max() > 255:
- colormap >>= 8
- colormap = colormap.astype("uint8")
- if "S" in self.axes:
- data = data[..., 0] if self.planarconfig == 1 else data[0]
- data = apply_colormap(data, colormap)
- elif photometric == PHOTOMETRIC.RGB:
- if "ExtraSamples" in self.tags:
- if alpha is None:
- alpha = TIFF.EXTRASAMPLE
- extrasamples = self.extrasamples
- if self.tags["ExtraSamples"].count == 1:
- extrasamples = (extrasamples,)
- for i, exs in enumerate(extrasamples):
- if exs in alpha:
- if self.planarconfig == 1:
- data = data[..., [0, 1, 2, 3 + i]]
- else:
- data = data[:, [0, 1, 2, 3 + i]]
- break
- else:
- if self.planarconfig == 1:
- data = data[..., :3]
- else:
- data = data[:, :3]
- # TODO: convert to uint8?
- elif photometric == PHOTOMETRIC.MINISBLACK:
- raise NotImplementedError()
- elif photometric == PHOTOMETRIC.MINISWHITE:
- raise NotImplementedError()
- elif photometric == PHOTOMETRIC.SEPARATED:
- raise NotImplementedError()
- else:
- raise NotImplementedError()
- return data
- def aspage(self):
- return self
- @property
- def keyframe(self):
- return self
- @keyframe.setter
- def keyframe(self, index):
- return
- @lazyattr
- def offsets_bytecounts(self):
- """Return simplified offsets and bytecounts."""
- if self.is_contiguous:
- offset, byte_count = self.is_contiguous
- return [offset], [byte_count]
- return clean_offsets_counts(self.dataoffsets, self.databytecounts)
- @lazyattr
- def is_contiguous(self):
- """Return offset and size of contiguous data, else None.
- Excludes prediction and fill_order.
- """
- if self.compression != 1 or self.bitspersample not in (8, 16, 32, 64):
- return
- if "TileWidth" in self.tags:
- if (
- self.imagewidth != self.tilewidth
- or self.imagelength % self.tilelength
- or self.tilewidth % 16
- or self.tilelength % 16
- ):
- return
- if (
- "ImageDepth" in self.tags
- and "TileDepth" in self.tags
- and (
- self.imagelength != self.tilelength
- or self.imagedepth % self.tiledepth
- )
- ):
- return
- offsets = self.dataoffsets
- bytecounts = self.databytecounts
- if len(offsets) == 1:
- return offsets[0], bytecounts[0]
- if self.is_stk or all(
- (
- offsets[i] + bytecounts[i] == offsets[i + 1] or bytecounts[i + 1] == 0
- ) # no data/ignore offset
- for i in range(len(offsets) - 1)
- ):
- return offsets[0], sum(bytecounts)
- @lazyattr
- def is_final(self):
- """Return if page's image data are stored in final form.
- Excludes byte-swapping.
- """
- return (
- self.is_contiguous
- and self.fillorder == 1
- and self.predictor == 1
- and not self.is_chroma_subsampled
- )
- @lazyattr
- def is_memmappable(self):
- """Return if page's image data in file can be memory-mapped."""
- return (
- self.parent.filehandle.is_file
- and self.is_final
- and
- # (self.bitspersample == 8 or self.parent.isnative) and
- self.is_contiguous[0] % self.dtype.itemsize == 0
- ) # aligned?
- def __str__(self, detail=0, width=79):
- """Return string containing information about page."""
- if self.keyframe != self:
- return TiffFrame.__str__(self, detail)
- attr = ""
- for name in ("memmappable", "final", "contiguous"):
- attr = getattr(self, "is_" + name)
- if attr:
- attr = name.upper()
- break
- info = " ".join(
- s
- for s in (
- "x".join(str(i) for i in self.shape),
- "%s%s"
- % (TIFF.SAMPLEFORMAT(self.sampleformat).name, self.bitspersample),
- "|".join(
- i
- for i in (
- TIFF.PHOTOMETRIC(self.photometric).name,
- "TILED" if self.is_tiled else "",
- self.compression.name if self.compression != 1 else "",
- self.planarconfig.name if self.planarconfig != 1 else "",
- self.predictor.name if self.predictor != 1 else "",
- self.fillorder.name if self.fillorder != 1 else "",
- )
- if i
- ),
- attr,
- "|".join((f.upper() for f in self.flags)),
- )
- if s
- )
- info = "TiffPage %i @%i %s" % (self.index, self.offset, info)
- if detail <= 0:
- return info
- info = [info]
- tags = self.tags
- tlines = []
- vlines = []
- for tag in sorted(tags.values(), key=lambda x: x.code):
- value = tag.__str__(width=width + 1)
- tlines.append(value[:width].strip())
- if detail > 1 and len(value) > width:
- name = tag.name.upper()
- if detail <= 2 and ("COUNTS" in name or "OFFSETS" in name):
- value = pformat(tag.value, width=width, height=detail * 4)
- else:
- value = pformat(tag.value, width=width, height=detail * 12)
- vlines.append("%s\n%s" % (tag.name, value))
- info.append("\n".join(tlines))
- if detail > 1:
- info.append("\n\n".join(vlines))
- if detail > 3:
- try:
- info.append(
- "DATA\n%s" % pformat(self.asarray(), width=width, height=detail * 8)
- )
- except Exception:
- pass
- return "\n\n".join(info)
- @lazyattr
- def flags(self):
- """Return set of flags."""
- return set(
- (
- name.lower()
- for name in sorted(TIFF.FILE_FLAGS)
- if getattr(self, "is_" + name)
- )
- )
- @property
- def ndim(self):
- """Return number of array dimensions."""
- return len(self.shape)
- @property
- def size(self):
- """Return number of elements in array."""
- return product(self.shape)
- @lazyattr
- def andor_tags(self):
- """Return consolidated metadata from Andor tags as dict.
- Remove Andor tags from self.tags.
- """
- if not self.is_andor:
- return
- tags = self.tags
- result = {"Id": tags["AndorId"].value}
- for tag in list(self.tags.values()):
- code = tag.code
- if not 4864 < code < 5031:
- continue
- value = tag.value
- name = tag.name[5:] if len(tag.name) > 5 else tag.name
- result[name] = value
- del tags[tag.name]
- return result
- @lazyattr
- def epics_tags(self):
- """Return consolidated metadata from EPICS areaDetector tags as dict.
- Remove areaDetector tags from self.tags.
- """
- if not self.is_epics:
- return
- result = {}
- tags = self.tags
- for tag in list(self.tags.values()):
- code = tag.code
- if not 65000 <= code < 65500:
- continue
- value = tag.value
- if code == 65000:
- result["timeStamp"] = datetime.datetime.fromtimestamp(float(value))
- elif code == 65001:
- result["uniqueID"] = int(value)
- elif code == 65002:
- result["epicsTSSec"] = int(value)
- elif code == 65003:
- result["epicsTSNsec"] = int(value)
- else:
- key, value = value.split(":", 1)
- result[key] = astype(value)
- del tags[tag.name]
- return result
- @lazyattr
- def geotiff_tags(self):
- """Return consolidated metadata from GeoTIFF tags as dict."""
- if not self.is_geotiff:
- return
- tags = self.tags
- gkd = tags["GeoKeyDirectoryTag"].value
- if gkd[0] != 1:
- warnings.warn("invalid GeoKeyDirectoryTag")
- return {}
- result = {
- "KeyDirectoryVersion": gkd[0],
- "KeyRevision": gkd[1],
- "KeyRevisionMinor": gkd[2],
- # 'NumberOfKeys': gkd[3],
- }
- # deltags = ['GeoKeyDirectoryTag']
- geokeys = TIFF.GEO_KEYS
- geocodes = TIFF.GEO_CODES
- for index in range(gkd[3]):
- keyid, tagid, count, offset = gkd[4 + index * 4 : index * 4 + 8]
- keyid = geokeys.get(keyid, keyid)
- if tagid == 0:
- value = offset
- else:
- tagname = TIFF.TAGS[tagid]
- # deltags.append(tagname)
- value = tags[tagname].value[offset : offset + count]
- if tagid == 34737 and count > 1 and value[-1] == "|":
- value = value[:-1]
- value = value if count > 1 else value[0]
- if keyid in geocodes:
- try:
- value = geocodes[keyid](value)
- except Exception:
- pass
- result[keyid] = value
- if "IntergraphMatrixTag" in tags:
- value = tags["IntergraphMatrixTag"].value
- value = numpy.array(value)
- if len(value) == 16:
- value = value.reshape((4, 4)).tolist()
- result["IntergraphMatrix"] = value
- if "ModelPixelScaleTag" in tags:
- value = numpy.array(tags["ModelPixelScaleTag"].value).tolist()
- result["ModelPixelScale"] = value
- if "ModelTiepointTag" in tags:
- value = tags["ModelTiepointTag"].value
- value = numpy.array(value).reshape((-1, 6)).squeeze().tolist()
- result["ModelTiepoint"] = value
- if "ModelTransformationTag" in tags:
- value = tags["ModelTransformationTag"].value
- value = numpy.array(value).reshape((4, 4)).tolist()
- result["ModelTransformation"] = value
- elif False:
- # if 'ModelPixelScaleTag' in tags and 'ModelTiepointTag' in tags:
- sx, sy, sz = tags["ModelPixelScaleTag"].value
- tiepoints = tags["ModelTiepointTag"].value
- transforms = []
- for tp in range(0, len(tiepoints), 6):
- i, j, k, x, y, z = tiepoints[tp : tp + 6]
- transforms.append(
- [
- [sx, 0.0, 0.0, x - i * sx],
- [0.0, -sy, 0.0, y + j * sy],
- [0.0, 0.0, sz, z - k * sz],
- [0.0, 0.0, 0.0, 1.0],
- ]
- )
- if len(tiepoints) == 6:
- transforms = transforms[0]
- result["ModelTransformation"] = transforms
- if "RPCCoefficientTag" in tags:
- rpcc = tags["RPCCoefficientTag"].value
- result["RPCCoefficient"] = {
- "ERR_BIAS": rpcc[0],
- "ERR_RAND": rpcc[1],
- "LINE_OFF": rpcc[2],
- "SAMP_OFF": rpcc[3],
- "LAT_OFF": rpcc[4],
- "LONG_OFF": rpcc[5],
- "HEIGHT_OFF": rpcc[6],
- "LINE_SCALE": rpcc[7],
- "SAMP_SCALE": rpcc[8],
- "LAT_SCALE": rpcc[9],
- "LONG_SCALE": rpcc[10],
- "HEIGHT_SCALE": rpcc[11],
- "LINE_NUM_COEFF": rpcc[12:33],
- "LINE_DEN_COEFF ": rpcc[33:53],
- "SAMP_NUM_COEFF": rpcc[53:73],
- "SAMP_DEN_COEFF": rpcc[73:],
- }
- return result
- @property
- def is_tiled(self):
- """Page contains tiled image."""
- return "TileWidth" in self.tags
- @property
- def is_reduced(self):
- """Page is reduced image of another image."""
- return "NewSubfileType" in self.tags and self.tags["NewSubfileType"].value & 1
- @property
- def is_chroma_subsampled(self):
- """Page contains chroma subsampled image."""
- return "YCbCrSubSampling" in self.tags and self.tags[
- "YCbCrSubSampling"
- ].value != (1, 1)
- @lazyattr
- def is_imagej(self):
- """Return ImageJ description if exists, else None."""
- for description in (self.description, self.description1):
- if not description:
- return
- if description[:7] == "ImageJ=":
- return description
- @lazyattr
- def is_shaped(self):
- """Return description containing array shape if exists, else None."""
- for description in (self.description, self.description1):
- if not description:
- return
- if description[:1] == "{" and '"shape":' in description:
- return description
- if description[:6] == "shape=":
- return description
- @property
- def is_mdgel(self):
- """Page contains MDFileTag tag."""
- return "MDFileTag" in self.tags
- @property
- def is_mediacy(self):
- """Page contains Media Cybernetics Id tag."""
- return "MC_Id" in self.tags and self.tags["MC_Id"].value[:7] == b"MC TIFF"
- @property
- def is_stk(self):
- """Page contains UIC2Tag tag."""
- return "UIC2tag" in self.tags
- @property
- def is_lsm(self):
- """Page contains CZ_LSMINFO tag."""
- return "CZ_LSMINFO" in self.tags
- @property
- def is_fluoview(self):
- """Page contains FluoView MM_STAMP tag."""
- return "MM_Stamp" in self.tags
- @property
- def is_nih(self):
- """Page contains NIH image header."""
- return "NIHImageHeader" in self.tags
- @property
- def is_sgi(self):
- """Page contains SGI image and tile depth tags."""
- return "ImageDepth" in self.tags and "TileDepth" in self.tags
- @property
- def is_vista(self):
- """Software tag is 'ISS Vista'."""
- return self.software == "ISS Vista"
- @property
- def is_metaseries(self):
- """Page contains MDS MetaSeries metadata in ImageDescription tag."""
- if self.index > 1 or self.software != "MetaSeries":
- return False
- d = self.description
- return d.startswith("<MetaData>") and d.endswith("</MetaData>")
- @property
- def is_ome(self):
- """Page contains OME-XML in ImageDescription tag."""
- if self.index > 1 or not self.description:
- return False
- d = self.description
- return d[:14] == "<?xml version=" and d[-6:] == "</OME>"
- @property
- def is_scn(self):
- """Page contains Leica SCN XML in ImageDescription tag."""
- if self.index > 1 or not self.description:
- return False
- d = self.description
- return d[:14] == "<?xml version=" and d[-6:] == "</scn>"
- @property
- def is_micromanager(self):
- """Page contains Micro-Manager metadata."""
- return "MicroManagerMetadata" in self.tags
- @property
- def is_andor(self):
- """Page contains Andor Technology tags."""
- return "AndorId" in self.tags
- @property
- def is_pilatus(self):
- """Page contains Pilatus tags."""
- return self.software[:8] == "TVX TIFF" and self.description[:2] == "# "
- @property
- def is_epics(self):
- """Page contains EPICS areaDetector tags."""
- return (
- self.description == "EPICS areaDetector"
- or self.software == "EPICS areaDetector"
- )
- @property
- def is_tvips(self):
- """Page contains TVIPS metadata."""
- return "TVIPS" in self.tags
- @property
- def is_fei(self):
- """Page contains SFEG or HELIOS metadata."""
- return "FEI_SFEG" in self.tags or "FEI_HELIOS" in self.tags
- @property
- def is_sem(self):
- """Page contains Zeiss SEM metadata."""
- return "CZ_SEM" in self.tags
- @property
- def is_svs(self):
- """Page contains Aperio metadata."""
- return self.description[:20] == "Aperio Image Library"
- @property
- def is_scanimage(self):
- """Page contains ScanImage metadata."""
- return (
- self.description[:12] == "state.config"
- or self.software[:22] == "SI.LINE_FORMAT_VERSION"
- or "scanimage.SI." in self.description[-256:]
- )
- @property
- def is_qptiff(self):
- """Page contains PerkinElmer tissue images metadata."""
- # The ImageDescription tag contains XML with a top-level
- # <PerkinElmer-QPI-ImageDescription> element
- return self.software[:15] == "PerkinElmer-QPI"
- @property
- def is_geotiff(self):
- """Page contains GeoTIFF metadata."""
- return "GeoKeyDirectoryTag" in self.tags
- class TiffFrame(object):
- """Lightweight TIFF image file directory (IFD).
- Only a limited number of tag values are read from file, e.g. StripOffsets,
- and StripByteCounts. Other tag values are assumed to be identical with a
- specified TiffPage instance, the keyframe.
- TiffFrame is intended to reduce resource usage and speed up reading data
- from file, not for introspection of metadata.
- Not compatible with Python 2.
- """
- __slots__ = (
- "keyframe",
- "parent",
- "index",
- "offset",
- "dataoffsets",
- "databytecounts",
- )
- is_mdgel = False
- tags = {}
- def __init__(self, parent, index, keyframe):
- """Read specified tags from file.
- The file handle position must be at the offset to a valid IFD.
- """
- self.keyframe = keyframe
- self.parent = parent
- self.index = index
- self.dataoffsets = None
- self.databytecounts = None
- unpack = struct.unpack
- fh = parent.filehandle
- self.offset = fh.tell()
- try:
- tagno = unpack(parent.tagnoformat, fh.read(parent.tagnosize))[0]
- if tagno > 4096:
- raise ValueError("suspicious number of tags")
- except Exception:
- raise ValueError("corrupted page list at offset %i" % self.offset)
- # tags = {}
- tagcodes = {273, 279, 324, 325} # TIFF.FRAME_TAGS
- tagsize = parent.tagsize
- codeformat = parent.tagformat1[:2]
- data = fh.read(tagsize * tagno)
- index = -tagsize
- for _ in range(tagno):
- index += tagsize
- code = unpack(codeformat, data[index : index + 2])[0]
- if code not in tagcodes:
- continue
- try:
- tag = TiffTag(parent, data[index : index + tagsize])
- except TiffTag.Error as e:
- warnings.warn(str(e))
- continue
- if code == 273 or code == 324:
- setattr(self, "dataoffsets", tag.value)
- elif code == 279 or code == 325:
- setattr(self, "databytecounts", tag.value)
- # elif code == 270:
- # tagname = tag.name
- # if tagname not in tags:
- # tags[tagname] = bytes2str(tag.value)
- # elif 'ImageDescription1' not in tags:
- # tags['ImageDescription1'] = bytes2str(tag.value)
- # else:
- # tags[tag.name] = tag.value
- def aspage(self):
- """Return TiffPage from file."""
- self.parent.filehandle.seek(self.offset)
- return TiffPage(self.parent, index=self.index, keyframe=None)
- def asarray(self, *args, **kwargs):
- """Read image data from file and return as numpy array."""
- # TODO: fix TypeError on Python 2
- # "TypeError: unbound method asarray() must be called with TiffPage
- # instance as first argument (got TiffFrame instance instead)"
- kwargs["validate"] = False
- return TiffPage.asarray(self, *args, **kwargs)
- def asrgb(self, *args, **kwargs):
- """Read image data from file and return RGB image as numpy array."""
- kwargs["validate"] = False
- return TiffPage.asrgb(self, *args, **kwargs)
- @property
- def offsets_bytecounts(self):
- """Return simplified offsets and bytecounts."""
- if self.keyframe.is_contiguous:
- return self.dataoffsets[:1], self.keyframe.is_contiguous[1:]
- return clean_offsets_counts(self.dataoffsets, self.databytecounts)
- @property
- def is_contiguous(self):
- """Return offset and size of contiguous data, else None."""
- if self.keyframe.is_contiguous:
- return self.dataoffsets[0], self.keyframe.is_contiguous[1]
- @property
- def is_memmappable(self):
- """Return if page's image data in file can be memory-mapped."""
- return self.keyframe.is_memmappable
- def __getattr__(self, name):
- """Return attribute from keyframe."""
- if name in TIFF.FRAME_ATTRS:
- return getattr(self.keyframe, name)
- # this error could be raised because an AttributeError was
- # raised inside a @property function
- raise AttributeError(
- "'%s' object has no attribute '%s'" % (self.__class__.__name__, name)
- )
- def __str__(self, detail=0):
- """Return string containing information about frame."""
- info = " ".join(
- s for s in ("x".join(str(i) for i in self.shape), str(self.dtype))
- )
- return "TiffFrame %i @%i %s" % (self.index, self.offset, info)
- class TiffTag(object):
- """TIFF tag structure.
- Attributes
- ----------
- name : string
- Name of tag.
- code : int
- Decimal code of tag.
- dtype : str
- Datatype of tag data. One of TIFF DATA_FORMATS.
- count : int
- Number of values.
- value : various types
- Tag data as Python object.
- ImageSourceData : int
- Location of value in file.
- All attributes are read-only.
- """
- __slots__ = ("code", "count", "dtype", "value", "valueoffset")
- class Error(Exception):
- pass
- def __init__(self, parent, tagheader, **kwargs):
- """Initialize instance from tag header."""
- fh = parent.filehandle
- byteorder = parent.byteorder
- unpack = struct.unpack
- offsetsize = parent.offsetsize
- self.valueoffset = fh.tell() + offsetsize + 4
- code, type_ = unpack(parent.tagformat1, tagheader[:4])
- count, value = unpack(parent.tagformat2, tagheader[4:])
- try:
- dtype = TIFF.DATA_FORMATS[type_]
- except KeyError:
- raise TiffTag.Error("unknown tag data type %i" % type_)
- fmt = "%s%i%s" % (byteorder, count * int(dtype[0]), dtype[1])
- size = struct.calcsize(fmt)
- if size > offsetsize or code in TIFF.TAG_READERS:
- self.valueoffset = offset = unpack(parent.offsetformat, value)[0]
- if offset < 8 or offset > fh.size - size:
- raise TiffTag.Error("invalid tag value offset")
- # if offset % 2:
- # warnings.warn('tag value does not begin on word boundary')
- fh.seek(offset)
- if code in TIFF.TAG_READERS:
- readfunc = TIFF.TAG_READERS[code]
- value = readfunc(fh, byteorder, dtype, count, offsetsize)
- elif type_ == 7 or (count > 1 and dtype[-1] == "B"):
- value = read_bytes(fh, byteorder, dtype, count, offsetsize)
- elif code in TIFF.TAGS or dtype[-1] == "s":
- value = unpack(fmt, fh.read(size))
- else:
- value = read_numpy(fh, byteorder, dtype, count, offsetsize)
- elif dtype[-1] == "B" or type_ == 7:
- value = value[:size]
- else:
- value = unpack(fmt, value[:size])
- process = (
- code not in TIFF.TAG_READERS and code not in TIFF.TAG_TUPLE and type_ != 7
- )
- if process and dtype[-1] == "s" and isinstance(value[0], bytes):
- # TIFF ASCII fields can contain multiple strings,
- # each terminated with a NUL
- value = value[0]
- try:
- value = bytes2str(stripascii(value).strip())
- except UnicodeDecodeError:
- warnings.warn("tag %i: coercing invalid ASCII to bytes" % code)
- dtype = "1B"
- else:
- if code in TIFF.TAG_ENUM:
- t = TIFF.TAG_ENUM[code]
- try:
- value = tuple(t(v) for v in value)
- except ValueError as e:
- warnings.warn(str(e))
- if process:
- if len(value) == 1:
- value = value[0]
- self.code = code
- self.dtype = dtype
- self.count = count
- self.value = value
- @property
- def name(self):
- return TIFF.TAGS.get(self.code, str(self.code))
- def _fix_lsm_bitspersample(self, parent):
- """Correct LSM bitspersample tag.
- Old LSM writers may use a separate region for two 16-bit values,
- although they fit into the tag value element of the tag.
- """
- if self.code == 258 and self.count == 2:
- # TODO: test this case; need example file
- warnings.warn("correcting LSM bitspersample tag")
- tof = parent.offsetformat[parent.offsetsize]
- self.valueoffset = struct.unpack(tof, self._value)[0]
- parent.filehandle.seek(self.valueoffset)
- self.value = struct.unpack("<HH", parent.filehandle.read(4))
- def __str__(self, detail=0, width=79):
- """Return string containing information about tag."""
- height = 1 if detail <= 0 else 8 * detail
- tcode = "%i%s" % (self.count * int(self.dtype[0]), self.dtype[1])
- line = (
- "TiffTag %i %s %s @%i "
- % (self.code, self.name, tcode, self.valueoffset)[:width]
- )
- if self.code in TIFF.TAG_ENUM:
- if self.count == 1:
- value = TIFF.TAG_ENUM[self.code](self.value).name
- else:
- value = pformat(tuple(v.name for v in self.value))
- else:
- value = pformat(self.value, width=width, height=height)
- if detail <= 0:
- line += value
- line = line[:width]
- else:
- line += "\n" + value
- return line
- # Added to produce cleaner exceptions if tifffile unexpectedly fails to open the
- # file. See this comment (and the following) for details:
- # https://github.com/imageio/imageio/commit/bdbe699bbcda4223b0b6bd4d7474f84bbe34af09#r64068747
- class TiffFileError(ValueError):
- pass
- class TiffPageSeries(object):
- """Series of TIFF pages with compatible shape and data type.
- Attributes
- ----------
- pages : list of TiffPage
- Sequence of TiffPages in series.
- dtype : numpy.dtype
- Data type (native byte order) of the image array in series.
- shape : tuple
- Dimensions of the image array in series.
- axes : str
- Labels of axes in shape. See TiffPage.axes.
- offset : int or None
- Position of image data in file if memory-mappable, else None.
- """
- def __init__(
- self,
- pages,
- shape,
- dtype,
- axes,
- parent=None,
- name=None,
- transform=None,
- stype=None,
- truncated=False,
- ):
- """Initialize instance."""
- self.index = 0
- self._pages = pages # might contain only first of contiguous pages
- self.shape = tuple(shape)
- self.axes = "".join(axes)
- self.dtype = numpy.dtype(dtype)
- self.stype = stype if stype else ""
- self.name = name if name else ""
- self.transform = transform
- if parent:
- self.parent = parent
- elif pages:
- self.parent = pages[0].parent
- else:
- self.parent = None
- if len(pages) == 1 and not truncated:
- self._len = int(product(self.shape) // product(pages[0].shape))
- else:
- self._len = len(pages)
- def asarray(self, out=None):
- """Return image data from series of TIFF pages as numpy array."""
- if self.parent:
- result = self.parent.asarray(series=self, out=out)
- if self.transform is not None:
- result = self.transform(result)
- return result
- @lazyattr
- def offset(self):
- """Return offset to series data in file, if any."""
- if not self._pages:
- return
- pos = 0
- for page in self._pages:
- if page is None:
- return
- if not page.is_final:
- return
- if not pos:
- pos = page.is_contiguous[0] + page.is_contiguous[1]
- continue
- if pos != page.is_contiguous[0]:
- return
- pos += page.is_contiguous[1]
- page = self._pages[0]
- offset = page.is_contiguous[0]
- if (page.is_imagej or page.is_shaped) and len(self._pages) == 1:
- # truncated files
- return offset
- if pos == offset + product(self.shape) * self.dtype.itemsize:
- return offset
- @property
- def ndim(self):
- """Return number of array dimensions."""
- return len(self.shape)
- @property
- def size(self):
- """Return number of elements in array."""
- return int(product(self.shape))
- @property
- def pages(self):
- """Return sequence of all pages in series."""
- # a workaround to keep the old interface working
- return self
- def __len__(self):
- """Return number of TiffPages in series."""
- return self._len
- def __getitem__(self, key):
- """Return specified TiffPage."""
- if len(self._pages) == 1 and 0 < key < self._len:
- index = self._pages[0].index
- return self.parent.pages[index + key]
- return self._pages[key]
- def __iter__(self):
- """Return iterator over TiffPages in series."""
- if len(self._pages) == self._len:
- for page in self._pages:
- yield page
- else:
- pages = self.parent.pages
- index = self._pages[0].index
- for i in range(self._len):
- yield pages[index + i]
- def __str__(self):
- """Return string with information about series."""
- s = " ".join(
- s
- for s in (
- snipstr("'%s'" % self.name, 20) if self.name else "",
- "x".join(str(i) for i in self.shape),
- str(self.dtype),
- self.axes,
- self.stype,
- "%i Pages" % len(self.pages),
- ("Offset=%i" % self.offset) if self.offset else "",
- )
- if s
- )
- return "TiffPageSeries %i %s" % (self.index, s)
- class TiffSequence(object):
- """Sequence of TIFF files.
- The image data in all files must match shape, dtype, etc.
- Attributes
- ----------
- files : list
- List of file names.
- shape : tuple
- Shape of image sequence. Excludes shape of image array.
- axes : str
- Labels of axes in shape.
- Examples
- --------
- >>> # read image stack from sequence of TIFF files
- >>> imsave('temp_C001T001.tif', numpy.random.rand(64, 64))
- >>> imsave('temp_C001T002.tif', numpy.random.rand(64, 64))
- >>> tifs = TiffSequence('temp_C001*.tif')
- >>> tifs.shape
- (1, 2)
- >>> tifs.axes
- 'CT'
- >>> data = tifs.asarray()
- >>> data.shape
- (1, 2, 64, 64)
- """
- _patterns = {
- "axes": r"""
- # matches Olympus OIF and Leica TIFF series
- _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))
- _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
- _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
- _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
- _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
- _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
- _?(?:(q|l|p|a|c|t|x|y|z|ch|tp)(\d{1,4}))?
- """
- }
- class ParseError(Exception):
- pass
- def __init__(self, files, imread=TiffFile, pattern="axes", *args, **kwargs):
- """Initialize instance from multiple files.
- Parameters
- ----------
- files : str, pathlib.Path, or sequence thereof
- Glob pattern or sequence of file names.
- Binary streams are not supported.
- imread : function or class
- Image read function or class with asarray function returning numpy
- array from single file.
- pattern : str
- Regular expression pattern that matches axes names and sequence
- indices in file names.
- By default, the pattern matches Olympus OIF and Leica TIFF series.
- """
- if isinstance(files, pathlib.Path):
- files = str(files)
- if isinstance(files, basestring):
- files = natural_sorted(glob.glob(files))
- files = list(files)
- if not files:
- raise ValueError("no files found")
- if isinstance(files[0], pathlib.Path):
- files = [str(pathlib.Path(f)) for f in files]
- elif not isinstance(files[0], basestring):
- raise ValueError("not a file name")
- self.files = files
- if hasattr(imread, "asarray"):
- # redefine imread
- _imread = imread
- def imread(fname, *args, **kwargs):
- with _imread(fname) as im:
- return im.asarray(*args, **kwargs)
- self.imread = imread
- self.pattern = self._patterns.get(pattern, pattern)
- try:
- self._parse()
- if not self.axes:
- self.axes = "I"
- except self.ParseError:
- self.axes = "I"
- self.shape = (len(files),)
- self._startindex = (0,)
- self._indices = tuple((i,) for i in range(len(files)))
- def __str__(self):
- """Return string with information about image sequence."""
- return "\n".join(
- [
- self.files[0],
- " size: %i" % len(self.files),
- " axes: %s" % self.axes,
- " shape: %s" % str(self.shape),
- ]
- )
- def __len__(self):
- return len(self.files)
- def __enter__(self):
- return self
- def __exit__(self, exc_type, exc_value, traceback):
- self.close()
- def close(self):
- pass
- def asarray(self, out=None, *args, **kwargs):
- """Read image data from all files and return as numpy array.
- The args and kwargs parameters are passed to the imread function.
- Raise IndexError or ValueError if image shapes do not match.
- """
- im = self.imread(self.files[0], *args, **kwargs)
- shape = self.shape + im.shape
- result = create_output(out, shape, dtype=im.dtype)
- result = result.reshape(-1, *im.shape)
- for index, fname in zip(self._indices, self.files):
- index = [i - j for i, j in zip(index, self._startindex)]
- index = numpy.ravel_multi_index(index, self.shape)
- im = self.imread(fname, *args, **kwargs)
- result[index] = im
- result.shape = shape
- return result
- def _parse(self):
- """Get axes and shape from file names."""
- if not self.pattern:
- raise self.ParseError("invalid pattern")
- pattern = re.compile(self.pattern, re.IGNORECASE | re.VERBOSE)
- matches = pattern.findall(self.files[0])
- if not matches:
- raise self.ParseError("pattern does not match file names")
- matches = matches[-1]
- if len(matches) % 2:
- raise self.ParseError("pattern does not match axis name and index")
- axes = "".join(m for m in matches[::2] if m)
- if not axes:
- raise self.ParseError("pattern does not match file names")
- indices = []
- for fname in self.files:
- matches = pattern.findall(fname)[-1]
- if axes != "".join(m for m in matches[::2] if m):
- raise ValueError("axes do not match within the image sequence")
- indices.append([int(m) for m in matches[1::2] if m])
- shape = tuple(numpy.max(indices, axis=0))
- startindex = tuple(numpy.min(indices, axis=0))
- shape = tuple(i - j + 1 for i, j in zip(shape, startindex))
- if product(shape) != len(self.files):
- warnings.warn("files are missing. Missing data are zeroed")
- self.axes = axes.upper()
- self.shape = shape
- self._indices = indices
- self._startindex = startindex
- class FileHandle(object):
- """Binary file handle.
- A limited, special purpose file handler that can:
- * handle embedded files (for CZI within CZI files)
- * re-open closed files (for multi-file formats, such as OME-TIFF)
- * read and write numpy arrays and records from file like objects
- Only 'rb' and 'wb' modes are supported. Concurrently reading and writing
- of the same stream is untested.
- When initialized from another file handle, do not use it unless this
- FileHandle is closed.
- Attributes
- ----------
- name : str
- Name of the file.
- path : str
- Absolute path to file.
- size : int
- Size of file in bytes.
- is_file : bool
- If True, file has a filno and can be memory-mapped.
- All attributes are read-only.
- """
- __slots__ = (
- "_fh",
- "_file",
- "_mode",
- "_name",
- "_dir",
- "_lock",
- "_offset",
- "_size",
- "_close",
- "is_file",
- )
- def __init__(self, file, mode="rb", name=None, offset=None, size=None):
- """Initialize file handle from file name or another file handle.
- Parameters
- ----------
- file : str, pathlib.Path, binary stream, or FileHandle
- File name or seekable binary stream, such as an open file
- or BytesIO.
- mode : str
- File open mode in case 'file' is a file name. Must be 'rb' or 'wb'.
- name : str
- Optional name of file in case 'file' is a binary stream.
- offset : int
- Optional start position of embedded file. By default, this is
- the current file position.
- size : int
- Optional size of embedded file. By default, this is the number
- of bytes from the 'offset' to the end of the file.
- """
- self._file = file
- self._fh = None
- self._mode = mode
- self._name = name
- self._dir = ""
- self._offset = offset
- self._size = size
- self._close = True
- self.is_file = False
- self._lock = NullContext()
- self.open()
- def open(self):
- """Open or re-open file."""
- if self._fh:
- return # file is open
- if isinstance(self._file, pathlib.Path):
- self._file = str(self._file)
- if isinstance(self._file, basestring):
- # file name
- self._file = os.path.realpath(self._file)
- self._dir, self._name = os.path.split(self._file)
- self._fh = open(self._file, self._mode)
- self._close = True
- if self._offset is None:
- self._offset = 0
- elif isinstance(self._file, FileHandle):
- # FileHandle
- self._fh = self._file._fh
- if self._offset is None:
- self._offset = 0
- self._offset += self._file._offset
- self._close = False
- if not self._name:
- if self._offset:
- name, ext = os.path.splitext(self._file._name)
- self._name = "%s@%i%s" % (name, self._offset, ext)
- else:
- self._name = self._file._name
- if self._mode and self._mode != self._file._mode:
- raise ValueError("FileHandle has wrong mode")
- self._mode = self._file._mode
- self._dir = self._file._dir
- elif hasattr(self._file, "seek"):
- # binary stream: open file, BytesIO
- try:
- self._file.tell()
- except Exception:
- raise ValueError("binary stream is not seekable")
- self._fh = self._file
- if self._offset is None:
- self._offset = self._file.tell()
- self._close = False
- if not self._name:
- try:
- self._dir, self._name = os.path.split(self._fh.name)
- except AttributeError:
- self._name = "Unnamed binary stream"
- try:
- self._mode = self._fh.mode
- except AttributeError:
- pass
- else:
- raise ValueError(
- "The first parameter must be a file name, "
- "seekable binary stream, or FileHandle"
- )
- if self._offset:
- self._fh.seek(self._offset)
- if self._size is None:
- pos = self._fh.tell()
- self._fh.seek(self._offset, 2)
- self._size = self._fh.tell()
- self._fh.seek(pos)
- try:
- self._fh.fileno()
- self.is_file = True
- except Exception:
- self.is_file = False
- def read(self, size=-1):
- """Read 'size' bytes from file, or until EOF is reached."""
- if size < 0 and self._offset:
- size = self._size
- return self._fh.read(size)
- def write(self, bytestring):
- """Write bytestring to file."""
- return self._fh.write(bytestring)
- def flush(self):
- """Flush write buffers if applicable."""
- return self._fh.flush()
- def memmap_array(self, dtype, shape, offset=0, mode="r", order="C"):
- """Return numpy.memmap of data stored in file."""
- if not self.is_file:
- raise ValueError("Cannot memory-map file without fileno")
- return numpy.memmap(
- self._fh,
- dtype=dtype,
- mode=mode,
- offset=self._offset + offset,
- shape=shape,
- order=order,
- )
- def read_array(
- self, dtype, count=-1, sep="", chunksize=2**25, out=None, native=False
- ):
- """Return numpy array from file.
- Work around numpy issue #2230, "numpy.fromfile does not accept
- StringIO object" https://github.com/numpy/numpy/issues/2230.
- """
- fh = self._fh
- dtype = numpy.dtype(dtype)
- size = self._size if count < 0 else count * dtype.itemsize
- if out is None:
- try:
- result = numpy.fromfile(fh, dtype, count, sep)
- except IOError:
- # ByteIO
- data = fh.read(size)
- result = numpy.frombuffer(data, dtype, count).copy()
- if native and not result.dtype.isnative:
- # swap byte order and dtype without copy
- result.byteswap(True)
- result = result.view(result.dtype.newbyteorder())
- return result
- # Read data from file in chunks and copy to output array
- shape = out.shape
- size = min(out.nbytes, size)
- out = out.reshape(-1)
- index = 0
- while size > 0:
- data = fh.read(min(chunksize, size))
- datasize = len(data)
- if datasize == 0:
- break
- size -= datasize
- data = numpy.frombuffer(data, dtype)
- out[index : index + data.size] = data
- index += data.size
- if hasattr(out, "flush"):
- out.flush()
- return out.reshape(shape)
- def read_record(self, dtype, shape=1, byteorder=None):
- """Return numpy record from file."""
- rec = numpy.rec
- try:
- record = rec.fromfile(self._fh, dtype, shape, byteorder=byteorder)
- except Exception:
- dtype = numpy.dtype(dtype)
- if shape is None:
- shape = self._size // dtype.itemsize
- size = product(sequence(shape)) * dtype.itemsize
- data = self._fh.read(size)
- record = rec.fromstring(data, dtype, shape, byteorder=byteorder)
- return record[0] if shape == 1 else record
- def write_empty(self, size):
- """Append size bytes to file. Position must be at end of file."""
- if size < 1:
- return
- self._fh.seek(size - 1, 1)
- self._fh.write(b"\x00")
- def write_array(self, data):
- """Write numpy array to binary file."""
- try:
- data.tofile(self._fh)
- except Exception:
- # BytesIO
- self._fh.write(data.tostring())
- def tell(self):
- """Return file's current position."""
- return self._fh.tell() - self._offset
- def seek(self, offset, whence=0):
- """Set file's current position."""
- if self._offset:
- if whence == 0:
- self._fh.seek(self._offset + offset, whence)
- return
- elif whence == 2 and self._size > 0:
- self._fh.seek(self._offset + self._size + offset, 0)
- return
- self._fh.seek(offset, whence)
- def close(self):
- """Close file."""
- if self._close and self._fh:
- self._fh.close()
- self._fh = None
- def __enter__(self):
- return self
- def __exit__(self, exc_type, exc_value, traceback):
- self.close()
- def __getattr__(self, name):
- """Return attribute from underlying file object."""
- if self._offset:
- warnings.warn("FileHandle: '%s' not implemented for embedded files" % name)
- return getattr(self._fh, name)
- @property
- def name(self):
- return self._name
- @property
- def dirname(self):
- return self._dir
- @property
- def path(self):
- return os.path.join(self._dir, self._name)
- @property
- def size(self):
- return self._size
- @property
- def closed(self):
- return self._fh is None
- @property
- def lock(self):
- return self._lock
- @lock.setter
- def lock(self, value):
- self._lock = threading.RLock() if value else NullContext()
- class NullContext(object):
- """Null context manager.
- >>> with NullContext():
- ... pass
- """
- def __enter__(self):
- return self
- def __exit__(self, exc_type, exc_value, traceback):
- pass
- class OpenFileCache(object):
- """Keep files open."""
- __slots__ = ("files", "past", "lock", "size")
- def __init__(self, size, lock=None):
- """Initialize open file cache."""
- self.past = [] # FIFO of opened files
- self.files = {} # refcounts of opened files
- self.lock = NullContext() if lock is None else lock
- self.size = int(size)
- def open(self, filehandle):
- """Re-open file if necessary."""
- with self.lock:
- if filehandle in self.files:
- self.files[filehandle] += 1
- elif filehandle.closed:
- filehandle.open()
- self.files[filehandle] = 1
- self.past.append(filehandle)
- def close(self, filehandle):
- """Close opened file if no longer used."""
- with self.lock:
- if filehandle in self.files:
- self.files[filehandle] -= 1
- # trim the file cache
- index = 0
- size = len(self.past)
- while size > self.size and index < size:
- filehandle = self.past[index]
- if self.files[filehandle] == 0:
- filehandle.close()
- del self.files[filehandle]
- del self.past[index]
- size -= 1
- else:
- index += 1
- def clear(self):
- """Close all opened files if not in use."""
- with self.lock:
- for filehandle, refcount in list(self.files.items()):
- if refcount == 0:
- filehandle.close()
- del self.files[filehandle]
- del self.past[self.past.index(filehandle)]
- class LazyConst(object):
- """Class whose attributes are computed on first access from its methods."""
- def __init__(self, cls):
- self._cls = cls
- self.__doc__ = getattr(cls, "__doc__")
- def __getattr__(self, name):
- func = getattr(self._cls, name)
- if not callable(func):
- return func
- try:
- value = func()
- except TypeError:
- # Python 2 unbound method
- value = func.__func__()
- setattr(self, name, value)
- return value
- @LazyConst
- class TIFF(object):
- """Namespace for module constants."""
- def TAGS():
- # TIFF tag codes and names from TIFF6, TIFF/EP, EXIF, and other specs
- return {
- 11: "ProcessingSoftware",
- 254: "NewSubfileType",
- 255: "SubfileType",
- 256: "ImageWidth",
- 257: "ImageLength",
- 258: "BitsPerSample",
- 259: "Compression",
- 262: "PhotometricInterpretation",
- 263: "Thresholding",
- 264: "CellWidth",
- 265: "CellLength",
- 266: "FillOrder",
- 269: "DocumentName",
- 270: "ImageDescription",
- 271: "Make",
- 272: "Model",
- 273: "StripOffsets",
- 274: "Orientation",
- 277: "SamplesPerPixel",
- 278: "RowsPerStrip",
- 279: "StripByteCounts",
- 280: "MinSampleValue",
- 281: "MaxSampleValue",
- 282: "XResolution",
- 283: "YResolution",
- 284: "PlanarConfiguration",
- 285: "PageName",
- 286: "XPosition",
- 287: "YPosition",
- 288: "FreeOffsets",
- 289: "FreeByteCounts",
- 290: "GrayResponseUnit",
- 291: "GrayResponseCurve",
- 292: "T4Options",
- 293: "T6Options",
- 296: "ResolutionUnit",
- 297: "PageNumber",
- 300: "ColorResponseUnit",
- 301: "TransferFunction",
- 305: "Software",
- 306: "DateTime",
- 315: "Artist",
- 316: "HostComputer",
- 317: "Predictor",
- 318: "WhitePoint",
- 319: "PrimaryChromaticities",
- 320: "ColorMap",
- 321: "HalftoneHints",
- 322: "TileWidth",
- 323: "TileLength",
- 324: "TileOffsets",
- 325: "TileByteCounts",
- 326: "BadFaxLines",
- 327: "CleanFaxData",
- 328: "ConsecutiveBadFaxLines",
- 330: "SubIFDs",
- 332: "InkSet",
- 333: "InkNames",
- 334: "NumberOfInks",
- 336: "DotRange",
- 337: "TargetPrinter",
- 338: "ExtraSamples",
- 339: "SampleFormat",
- 340: "SMinSampleValue",
- 341: "SMaxSampleValue",
- 342: "TransferRange",
- 343: "ClipPath",
- 344: "XClipPathUnits",
- 345: "YClipPathUnits",
- 346: "Indexed",
- 347: "JPEGTables",
- 351: "OPIProxy",
- 400: "GlobalParametersIFD",
- 401: "ProfileType",
- 402: "FaxProfile",
- 403: "CodingMethods",
- 404: "VersionYear",
- 405: "ModeNumber",
- 433: "Decode",
- 434: "DefaultImageColor",
- 435: "T82Options",
- 437: "JPEGTables_", # 347
- 512: "JPEGProc",
- 513: "JPEGInterchangeFormat",
- 514: "JPEGInterchangeFormatLength",
- 515: "JPEGRestartInterval",
- 517: "JPEGLosslessPredictors",
- 518: "JPEGPointTransforms",
- 519: "JPEGQTables",
- 520: "JPEGDCTables",
- 521: "JPEGACTables",
- 529: "YCbCrCoefficients",
- 530: "YCbCrSubSampling",
- 531: "YCbCrPositioning",
- 532: "ReferenceBlackWhite",
- 559: "StripRowCounts",
- 700: "XMP", # XMLPacket
- 769: "GDIGamma", # GDI+
- 770: "ICCProfileDescriptor", # GDI+
- 771: "SRGBRenderingIntent", # GDI+
- 800: "ImageTitle", # GDI+
- 999: "USPTO_Miscellaneous",
- 4864: "AndorId", # TODO: Andor Technology 4864 - 5030
- 4869: "AndorTemperature",
- 4876: "AndorExposureTime",
- 4878: "AndorKineticCycleTime",
- 4879: "AndorAccumulations",
- 4881: "AndorAcquisitionCycleTime",
- 4882: "AndorReadoutTime",
- 4884: "AndorPhotonCounting",
- 4885: "AndorEmDacLevel",
- 4890: "AndorFrames",
- 4896: "AndorHorizontalFlip",
- 4897: "AndorVerticalFlip",
- 4898: "AndorClockwise",
- 4899: "AndorCounterClockwise",
- 4904: "AndorVerticalClockVoltage",
- 4905: "AndorVerticalShiftSpeed",
- 4907: "AndorPreAmpSetting",
- 4908: "AndorCameraSerial",
- 4911: "AndorActualTemperature",
- 4912: "AndorBaselineClamp",
- 4913: "AndorPrescans",
- 4914: "AndorModel",
- 4915: "AndorChipSizeX",
- 4916: "AndorChipSizeY",
- 4944: "AndorBaselineOffset",
- 4966: "AndorSoftwareVersion",
- 18246: "Rating",
- 18247: "XP_DIP_XML",
- 18248: "StitchInfo",
- 18249: "RatingPercent",
- 20481: "ResolutionXUnit", # GDI+
- 20482: "ResolutionYUnit", # GDI+
- 20483: "ResolutionXLengthUnit", # GDI+
- 20484: "ResolutionYLengthUnit", # GDI+
- 20485: "PrintFlags", # GDI+
- 20486: "PrintFlagsVersion", # GDI+
- 20487: "PrintFlagsCrop", # GDI+
- 20488: "PrintFlagsBleedWidth", # GDI+
- 20489: "PrintFlagsBleedWidthScale", # GDI+
- 20490: "HalftoneLPI", # GDI+
- 20491: "HalftoneLPIUnit", # GDI+
- 20492: "HalftoneDegree", # GDI+
- 20493: "HalftoneShape", # GDI+
- 20494: "HalftoneMisc", # GDI+
- 20495: "HalftoneScreen", # GDI+
- 20496: "JPEGQuality", # GDI+
- 20497: "GridSize", # GDI+
- 20498: "ThumbnailFormat", # GDI+
- 20499: "ThumbnailWidth", # GDI+
- 20500: "ThumbnailHeight", # GDI+
- 20501: "ThumbnailColorDepth", # GDI+
- 20502: "ThumbnailPlanes", # GDI+
- 20503: "ThumbnailRawBytes", # GDI+
- 20504: "ThumbnailSize", # GDI+
- 20505: "ThumbnailCompressedSize", # GDI+
- 20506: "ColorTransferFunction", # GDI+
- 20507: "ThumbnailData",
- 20512: "ThumbnailImageWidth", # GDI+
- 20513: "ThumbnailImageHeight", # GDI+
- 20514: "ThumbnailBitsPerSample", # GDI+
- 20515: "ThumbnailCompression",
- 20516: "ThumbnailPhotometricInterp", # GDI+
- 20517: "ThumbnailImageDescription", # GDI+
- 20518: "ThumbnailEquipMake", # GDI+
- 20519: "ThumbnailEquipModel", # GDI+
- 20520: "ThumbnailStripOffsets", # GDI+
- 20521: "ThumbnailOrientation", # GDI+
- 20522: "ThumbnailSamplesPerPixel", # GDI+
- 20523: "ThumbnailRowsPerStrip", # GDI+
- 20524: "ThumbnailStripBytesCount", # GDI+
- 20525: "ThumbnailResolutionX",
- 20526: "ThumbnailResolutionY",
- 20527: "ThumbnailPlanarConfig", # GDI+
- 20528: "ThumbnailResolutionUnit",
- 20529: "ThumbnailTransferFunction",
- 20530: "ThumbnailSoftwareUsed", # GDI+
- 20531: "ThumbnailDateTime", # GDI+
- 20532: "ThumbnailArtist", # GDI+
- 20533: "ThumbnailWhitePoint", # GDI+
- 20534: "ThumbnailPrimaryChromaticities", # GDI+
- 20535: "ThumbnailYCbCrCoefficients", # GDI+
- 20536: "ThumbnailYCbCrSubsampling", # GDI+
- 20537: "ThumbnailYCbCrPositioning",
- 20538: "ThumbnailRefBlackWhite", # GDI+
- 20539: "ThumbnailCopyRight", # GDI+
- 20545: "InteroperabilityIndex",
- 20546: "InteroperabilityVersion",
- 20624: "LuminanceTable",
- 20625: "ChrominanceTable",
- 20736: "FrameDelay", # GDI+
- 20737: "LoopCount", # GDI+
- 20738: "GlobalPalette", # GDI+
- 20739: "IndexBackground", # GDI+
- 20740: "IndexTransparent", # GDI+
- 20752: "PixelUnit", # GDI+
- 20753: "PixelPerUnitX", # GDI+
- 20754: "PixelPerUnitY", # GDI+
- 20755: "PaletteHistogram", # GDI+
- 28672: "SonyRawFileType", # Sony ARW
- 28722: "VignettingCorrParams", # Sony ARW
- 28725: "ChromaticAberrationCorrParams", # Sony ARW
- 28727: "DistortionCorrParams", # Sony ARW
- # Private tags >= 32768
- 32781: "ImageID",
- 32931: "WangTag1",
- 32932: "WangAnnotation",
- 32933: "WangTag3",
- 32934: "WangTag4",
- 32953: "ImageReferencePoints",
- 32954: "RegionXformTackPoint",
- 32955: "WarpQuadrilateral",
- 32956: "AffineTransformMat",
- 32995: "Matteing",
- 32996: "DataType",
- 32997: "ImageDepth",
- 32998: "TileDepth",
- 33300: "ImageFullWidth",
- 33301: "ImageFullLength",
- 33302: "TextureFormat",
- 33303: "TextureWrapModes",
- 33304: "FieldOfViewCotangent",
- 33305: "MatrixWorldToScreen",
- 33306: "MatrixWorldToCamera",
- 33405: "Model2",
- 33421: "CFARepeatPatternDim",
- 33422: "CFAPattern",
- 33423: "BatteryLevel",
- 33424: "KodakIFD",
- 33434: "ExposureTime",
- 33437: "FNumber",
- 33432: "Copyright",
- 33445: "MDFileTag",
- 33446: "MDScalePixel",
- 33447: "MDColorTable",
- 33448: "MDLabName",
- 33449: "MDSampleInfo",
- 33450: "MDPrepDate",
- 33451: "MDPrepTime",
- 33452: "MDFileUnits",
- 33550: "ModelPixelScaleTag",
- 33589: "AdventScale",
- 33590: "AdventRevision",
- 33628: "UIC1tag", # Metamorph Universal Imaging Corp STK
- 33629: "UIC2tag",
- 33630: "UIC3tag",
- 33631: "UIC4tag",
- 33723: "IPTCNAA",
- 33858: "ExtendedTagsOffset", # DEFF points IFD with private tags
- 33918: "IntergraphPacketData", # INGRPacketDataTag
- 33919: "IntergraphFlagRegisters", # INGRFlagRegisters
- 33920: "IntergraphMatrixTag", # IrasBTransformationMatrix
- 33921: "INGRReserved",
- 33922: "ModelTiepointTag",
- 33923: "LeicaMagic",
- 34016: "Site",
- 34017: "ColorSequence",
- 34018: "IT8Header",
- 34019: "RasterPadding",
- 34020: "BitsPerRunLength",
- 34021: "BitsPerExtendedRunLength",
- 34022: "ColorTable",
- 34023: "ImageColorIndicator",
- 34024: "BackgroundColorIndicator",
- 34025: "ImageColorValue",
- 34026: "BackgroundColorValue",
- 34027: "PixelIntensityRange",
- 34028: "TransparencyIndicator",
- 34029: "ColorCharacterization",
- 34030: "HCUsage",
- 34031: "TrapIndicator",
- 34032: "CMYKEquivalent",
- 34118: "CZ_SEM", # Zeiss SEM
- 34152: "AFCP_IPTC",
- 34232: "PixelMagicJBIGOptions",
- 34263: "JPLCartoIFD",
- 34122: "IPLAB", # number of images
- 34264: "ModelTransformationTag",
- 34306: "WB_GRGBLevels", # Leaf MOS
- 34310: "LeafData",
- 34361: "MM_Header",
- 34362: "MM_Stamp",
- 34363: "MM_Unknown",
- 34377: "ImageResources", # Photoshop
- 34386: "MM_UserBlock",
- 34412: "CZ_LSMINFO",
- 34665: "ExifTag",
- 34675: "InterColorProfile", # ICCProfile
- 34680: "FEI_SFEG", #
- 34682: "FEI_HELIOS", #
- 34683: "FEI_TITAN", #
- 34687: "FXExtensions",
- 34688: "MultiProfiles",
- 34689: "SharedData",
- 34690: "T88Options",
- 34710: "MarCCD", # offset to MarCCD header
- 34732: "ImageLayer",
- 34735: "GeoKeyDirectoryTag",
- 34736: "GeoDoubleParamsTag",
- 34737: "GeoAsciiParamsTag",
- 34750: "JBIGOptions",
- 34821: "PIXTIFF", # ? Pixel Translations Inc
- 34850: "ExposureProgram",
- 34852: "SpectralSensitivity",
- 34853: "GPSTag", # GPSIFD
- 34855: "ISOSpeedRatings",
- 34856: "OECF",
- 34857: "Interlace",
- 34858: "TimeZoneOffset",
- 34859: "SelfTimerMode",
- 34864: "SensitivityType",
- 34865: "StandardOutputSensitivity",
- 34866: "RecommendedExposureIndex",
- 34867: "ISOSpeed",
- 34868: "ISOSpeedLatitudeyyy",
- 34869: "ISOSpeedLatitudezzz",
- 34908: "HylaFAXFaxRecvParams",
- 34909: "HylaFAXFaxSubAddress",
- 34910: "HylaFAXFaxRecvTime",
- 34911: "FaxDcs",
- 34929: "FedexEDR",
- 34954: "LeafSubIFD",
- 34959: "Aphelion1",
- 34960: "Aphelion2",
- 34961: "AphelionInternal", # ADCIS
- 36864: "ExifVersion",
- 36867: "DateTimeOriginal",
- 36868: "DateTimeDigitized",
- 36873: "GooglePlusUploadCode",
- 36880: "OffsetTime",
- 36881: "OffsetTimeOriginal",
- 36882: "OffsetTimeDigitized",
- # TODO: Pilatus/CHESS/TV6 36864..37120 conflicting with Exif tags
- # 36864: 'TVX ?',
- # 36865: 'TVX_NumExposure',
- # 36866: 'TVX_NumBackground',
- # 36867: 'TVX_ExposureTime',
- # 36868: 'TVX_BackgroundTime',
- # 36870: 'TVX ?',
- # 36873: 'TVX_SubBpp',
- # 36874: 'TVX_SubWide',
- # 36875: 'TVX_SubHigh',
- # 36876: 'TVX_BlackLevel',
- # 36877: 'TVX_DarkCurrent',
- # 36878: 'TVX_ReadNoise',
- # 36879: 'TVX_DarkCurrentNoise',
- # 36880: 'TVX_BeamMonitor',
- # 37120: 'TVX_UserVariables', # A/D values
- 37121: "ComponentsConfiguration",
- 37122: "CompressedBitsPerPixel",
- 37377: "ShutterSpeedValue",
- 37378: "ApertureValue",
- 37379: "BrightnessValue",
- 37380: "ExposureBiasValue",
- 37381: "MaxApertureValue",
- 37382: "SubjectDistance",
- 37383: "MeteringMode",
- 37384: "LightSource",
- 37385: "Flash",
- 37386: "FocalLength",
- 37387: "FlashEnergy_", # 37387
- 37388: "SpatialFrequencyResponse_", # 37388
- 37389: "Noise",
- 37390: "FocalPlaneXResolution",
- 37391: "FocalPlaneYResolution",
- 37392: "FocalPlaneResolutionUnit",
- 37393: "ImageNumber",
- 37394: "SecurityClassification",
- 37395: "ImageHistory",
- 37396: "SubjectLocation",
- 37397: "ExposureIndex",
- 37398: "TIFFEPStandardID",
- 37399: "SensingMethod",
- 37434: "CIP3DataFile",
- 37435: "CIP3Sheet",
- 37436: "CIP3Side",
- 37439: "StoNits",
- 37500: "MakerNote",
- 37510: "UserComment",
- 37520: "SubsecTime",
- 37521: "SubsecTimeOriginal",
- 37522: "SubsecTimeDigitized",
- 37679: "MODIText", # Microsoft Office Document Imaging
- 37680: "MODIOLEPropertySetStorage",
- 37681: "MODIPositioning",
- 37706: "TVIPS", # offset to TemData structure
- 37707: "TVIPS1",
- 37708: "TVIPS2", # same TemData structure as undefined
- 37724: "ImageSourceData", # Photoshop
- 37888: "Temperature",
- 37889: "Humidity",
- 37890: "Pressure",
- 37891: "WaterDepth",
- 37892: "Acceleration",
- 37893: "CameraElevationAngle",
- 40001: "MC_IpWinScal", # Media Cybernetics
- 40100: "MC_IdOld",
- 40965: "InteroperabilityTag", # InteropOffset
- 40091: "XPTitle",
- 40092: "XPComment",
- 40093: "XPAuthor",
- 40094: "XPKeywords",
- 40095: "XPSubject",
- 40960: "FlashpixVersion",
- 40961: "ColorSpace",
- 40962: "PixelXDimension",
- 40963: "PixelYDimension",
- 40964: "RelatedSoundFile",
- 40976: "SamsungRawPointersOffset",
- 40977: "SamsungRawPointersLength",
- 41217: "SamsungRawByteOrder",
- 41218: "SamsungRawUnknown",
- 41483: "FlashEnergy",
- 41484: "SpatialFrequencyResponse",
- 41485: "Noise_", # 37389
- 41486: "FocalPlaneXResolution_", # 37390
- 41487: "FocalPlaneYResolution_", # 37391
- 41488: "FocalPlaneResolutionUnit_", # 37392
- 41489: "ImageNumber_", # 37393
- 41490: "SecurityClassification_", # 37394
- 41491: "ImageHistory_", # 37395
- 41492: "SubjectLocation_", # 37395
- 41493: "ExposureIndex_ ", # 37397
- 41494: "TIFF-EPStandardID",
- 41495: "SensingMethod_", # 37399
- 41728: "FileSource",
- 41729: "SceneType",
- 41730: "CFAPattern_", # 33422
- 41985: "CustomRendered",
- 41986: "ExposureMode",
- 41987: "WhiteBalance",
- 41988: "DigitalZoomRatio",
- 41989: "FocalLengthIn35mmFilm",
- 41990: "SceneCaptureType",
- 41991: "GainControl",
- 41992: "Contrast",
- 41993: "Saturation",
- 41994: "Sharpness",
- 41995: "DeviceSettingDescription",
- 41996: "SubjectDistanceRange",
- 42016: "ImageUniqueID",
- 42032: "CameraOwnerName",
- 42033: "BodySerialNumber",
- 42034: "LensSpecification",
- 42035: "LensMake",
- 42036: "LensModel",
- 42037: "LensSerialNumber",
- 42112: "GDAL_METADATA",
- 42113: "GDAL_NODATA",
- 42240: "Gamma",
- 43314: "NIHImageHeader",
- 44992: "ExpandSoftware",
- 44993: "ExpandLens",
- 44994: "ExpandFilm",
- 44995: "ExpandFilterLens",
- 44996: "ExpandScanner",
- 44997: "ExpandFlashLamp",
- 48129: "PixelFormat", # HDP and WDP
- 48130: "Transformation",
- 48131: "Uncompressed",
- 48132: "ImageType",
- 48256: "ImageWidth_", # 256
- 48257: "ImageHeight_",
- 48258: "WidthResolution",
- 48259: "HeightResolution",
- 48320: "ImageOffset",
- 48321: "ImageByteCount",
- 48322: "AlphaOffset",
- 48323: "AlphaByteCount",
- 48324: "ImageDataDiscard",
- 48325: "AlphaDataDiscard",
- 50215: "OceScanjobDescription",
- 50216: "OceApplicationSelector",
- 50217: "OceIdentificationNumber",
- 50218: "OceImageLogicCharacteristics",
- 50255: "Annotations",
- 50288: "MC_Id", # Media Cybernetics
- 50289: "MC_XYPosition",
- 50290: "MC_ZPosition",
- 50291: "MC_XYCalibration",
- 50292: "MC_LensCharacteristics",
- 50293: "MC_ChannelName",
- 50294: "MC_ExcitationWavelength",
- 50295: "MC_TimeStamp",
- 50296: "MC_FrameProperties",
- 50341: "PrintImageMatching",
- 50495: "PCO_RAW", # TODO: PCO CamWare
- 50547: "OriginalFileName",
- 50560: "USPTO_OriginalContentType", # US Patent Office
- 50561: "USPTO_RotationCode",
- 50656: "CR2CFAPattern",
- 50706: "DNGVersion", # DNG 50706 .. 51112
- 50707: "DNGBackwardVersion",
- 50708: "UniqueCameraModel",
- 50709: "LocalizedCameraModel",
- 50710: "CFAPlaneColor",
- 50711: "CFALayout",
- 50712: "LinearizationTable",
- 50713: "BlackLevelRepeatDim",
- 50714: "BlackLevel",
- 50715: "BlackLevelDeltaH",
- 50716: "BlackLevelDeltaV",
- 50717: "WhiteLevel",
- 50718: "DefaultScale",
- 50719: "DefaultCropOrigin",
- 50720: "DefaultCropSize",
- 50721: "ColorMatrix1",
- 50722: "ColorMatrix2",
- 50723: "CameraCalibration1",
- 50724: "CameraCalibration2",
- 50725: "ReductionMatrix1",
- 50726: "ReductionMatrix2",
- 50727: "AnalogBalance",
- 50728: "AsShotNeutral",
- 50729: "AsShotWhiteXY",
- 50730: "BaselineExposure",
- 50731: "BaselineNoise",
- 50732: "BaselineSharpness",
- 50733: "BayerGreenSplit",
- 50734: "LinearResponseLimit",
- 50735: "CameraSerialNumber",
- 50736: "LensInfo",
- 50737: "ChromaBlurRadius",
- 50738: "AntiAliasStrength",
- 50739: "ShadowScale",
- 50740: "DNGPrivateData",
- 50741: "MakerNoteSafety",
- 50752: "RawImageSegmentation",
- 50778: "CalibrationIlluminant1",
- 50779: "CalibrationIlluminant2",
- 50780: "BestQualityScale",
- 50781: "RawDataUniqueID",
- 50784: "AliasLayerMetadata",
- 50827: "OriginalRawFileName",
- 50828: "OriginalRawFileData",
- 50829: "ActiveArea",
- 50830: "MaskedAreas",
- 50831: "AsShotICCProfile",
- 50832: "AsShotPreProfileMatrix",
- 50833: "CurrentICCProfile",
- 50834: "CurrentPreProfileMatrix",
- 50838: "IJMetadataByteCounts",
- 50839: "IJMetadata",
- 50844: "RPCCoefficientTag",
- 50879: "ColorimetricReference",
- 50885: "SRawType",
- 50898: "PanasonicTitle",
- 50899: "PanasonicTitle2",
- 50931: "CameraCalibrationSignature",
- 50932: "ProfileCalibrationSignature",
- 50933: "ProfileIFD",
- 50934: "AsShotProfileName",
- 50935: "NoiseReductionApplied",
- 50936: "ProfileName",
- 50937: "ProfileHueSatMapDims",
- 50938: "ProfileHueSatMapData1",
- 50939: "ProfileHueSatMapData2",
- 50940: "ProfileToneCurve",
- 50941: "ProfileEmbedPolicy",
- 50942: "ProfileCopyright",
- 50964: "ForwardMatrix1",
- 50965: "ForwardMatrix2",
- 50966: "PreviewApplicationName",
- 50967: "PreviewApplicationVersion",
- 50968: "PreviewSettingsName",
- 50969: "PreviewSettingsDigest",
- 50970: "PreviewColorSpace",
- 50971: "PreviewDateTime",
- 50972: "RawImageDigest",
- 50973: "OriginalRawFileDigest",
- 50974: "SubTileBlockSize",
- 50975: "RowInterleaveFactor",
- 50981: "ProfileLookTableDims",
- 50982: "ProfileLookTableData",
- 51008: "OpcodeList1",
- 51009: "OpcodeList2",
- 51022: "OpcodeList3",
- 51023: "FibicsXML", #
- 51041: "NoiseProfile",
- 51043: "TimeCodes",
- 51044: "FrameRate",
- 51058: "TStop",
- 51081: "ReelName",
- 51089: "OriginalDefaultFinalSize",
- 51090: "OriginalBestQualitySize",
- 51091: "OriginalDefaultCropSize",
- 51105: "CameraLabel",
- 51107: "ProfileHueSatMapEncoding",
- 51108: "ProfileLookTableEncoding",
- 51109: "BaselineExposureOffset",
- 51110: "DefaultBlackRender",
- 51111: "NewRawImageDigest",
- 51112: "RawToPreviewGain",
- 51125: "DefaultUserCrop",
- 51123: "MicroManagerMetadata",
- 59932: "Padding",
- 59933: "OffsetSchema",
- # Reusable Tags 65000-65535
- # 65000: Dimap_Document XML
- # 65000-65112: Photoshop Camera RAW EXIF tags
- # 65000: 'OwnerName',
- # 65001: 'SerialNumber',
- # 65002: 'Lens',
- # 65024: 'KDC_IFD',
- # 65100: 'RawFile',
- # 65101: 'Converter',
- # 65102: 'WhiteBalance',
- # 65105: 'Exposure',
- # 65106: 'Shadows',
- # 65107: 'Brightness',
- # 65108: 'Contrast',
- # 65109: 'Saturation',
- # 65110: 'Sharpness',
- # 65111: 'Smoothness',
- # 65112: 'MoireFilter',
- 65200: "FlexXML", #
- 65563: "PerSample",
- }
- def TAG_NAMES():
- return {v: c for c, v in TIFF.TAGS.items()}
- def TAG_READERS():
- # Map TIFF tag codes to import functions
- return {
- 320: read_colormap,
- # 700: read_bytes, # read_utf8,
- # 34377: read_bytes,
- 33723: read_bytes,
- # 34675: read_bytes,
- 33628: read_uic1tag, # Universal Imaging Corp STK
- 33629: read_uic2tag,
- 33630: read_uic3tag,
- 33631: read_uic4tag,
- 34118: read_cz_sem, # Carl Zeiss SEM
- 34361: read_mm_header, # Olympus FluoView
- 34362: read_mm_stamp,
- 34363: read_numpy, # MM_Unknown
- 34386: read_numpy, # MM_UserBlock
- 34412: read_cz_lsminfo, # Carl Zeiss LSM
- 34680: read_fei_metadata, # S-FEG
- 34682: read_fei_metadata, # Helios NanoLab
- 37706: read_tvips_header, # TVIPS EMMENU
- 37724: read_bytes, # ImageSourceData
- 33923: read_bytes, # read_leica_magic
- 43314: read_nih_image_header,
- # 40001: read_bytes,
- 40100: read_bytes,
- 50288: read_bytes,
- 50296: read_bytes,
- 50839: read_bytes,
- 51123: read_json,
- 34665: read_exif_ifd,
- 34853: read_gps_ifd,
- 40965: read_interoperability_ifd,
- }
- def TAG_TUPLE():
- # Tags whose values must be stored as tuples
- return frozenset((273, 279, 324, 325, 530, 531, 34736))
- def TAG_ATTRIBUTES():
- # Map tag codes to TiffPage attribute names
- return {
- "ImageWidth": "imagewidth",
- "ImageLength": "imagelength",
- "BitsPerSample": "bitspersample",
- "Compression": "compression",
- "PlanarConfiguration": "planarconfig",
- "FillOrder": "fillorder",
- "PhotometricInterpretation": "photometric",
- "ColorMap": "colormap",
- "ImageDescription": "description",
- "ImageDescription1": "description1",
- "SamplesPerPixel": "samplesperpixel",
- "RowsPerStrip": "rowsperstrip",
- "Software": "software",
- "Predictor": "predictor",
- "TileWidth": "tilewidth",
- "TileLength": "tilelength",
- "ExtraSamples": "extrasamples",
- "SampleFormat": "sampleformat",
- "ImageDepth": "imagedepth",
- "TileDepth": "tiledepth",
- }
- def TAG_ENUM():
- return {
- # 254: TIFF.FILETYPE,
- 255: TIFF.OFILETYPE,
- 259: TIFF.COMPRESSION,
- 262: TIFF.PHOTOMETRIC,
- 263: TIFF.THRESHHOLD,
- 266: TIFF.FILLORDER,
- 274: TIFF.ORIENTATION,
- 284: TIFF.PLANARCONFIG,
- 290: TIFF.GRAYRESPONSEUNIT,
- # 292: TIFF.GROUP3OPT,
- # 293: TIFF.GROUP4OPT,
- 296: TIFF.RESUNIT,
- 300: TIFF.COLORRESPONSEUNIT,
- 317: TIFF.PREDICTOR,
- 338: TIFF.EXTRASAMPLE,
- 339: TIFF.SAMPLEFORMAT,
- # 512: TIFF.JPEGPROC,
- # 531: TIFF.YCBCRPOSITION,
- }
- def FILETYPE():
- class FILETYPE(enum.IntFlag):
- # Python 3.6 only
- UNDEFINED = 0
- REDUCEDIMAGE = 1
- PAGE = 2
- MASK = 4
- return FILETYPE
- def OFILETYPE():
- class OFILETYPE(enum.IntEnum):
- UNDEFINED = 0
- IMAGE = 1
- REDUCEDIMAGE = 2
- PAGE = 3
- return OFILETYPE
- def COMPRESSION():
- class COMPRESSION(enum.IntEnum):
- NONE = 1 # Uncompressed
- CCITTRLE = 2 # CCITT 1D
- CCITT_T4 = 3 # 'T4/Group 3 Fax',
- CCITT_T6 = 4 # 'T6/Group 4 Fax',
- LZW = 5
- OJPEG = 6 # old-style JPEG
- JPEG = 7
- ADOBE_DEFLATE = 8
- JBIG_BW = 9
- JBIG_COLOR = 10
- JPEG_99 = 99
- KODAK_262 = 262
- NEXT = 32766
- SONY_ARW = 32767
- PACKED_RAW = 32769
- SAMSUNG_SRW = 32770
- CCIRLEW = 32771
- SAMSUNG_SRW2 = 32772
- PACKBITS = 32773
- THUNDERSCAN = 32809
- IT8CTPAD = 32895
- IT8LW = 32896
- IT8MP = 32897
- IT8BL = 32898
- PIXARFILM = 32908
- PIXARLOG = 32909
- DEFLATE = 32946
- DCS = 32947
- APERIO_JP2000_YCBC = 33003 # Leica Aperio
- APERIO_JP2000_RGB = 33005 # Leica Aperio
- JBIG = 34661
- SGILOG = 34676
- SGILOG24 = 34677
- JPEG2000 = 34712
- NIKON_NEF = 34713
- JBIG2 = 34715
- MDI_BINARY = 34718 # 'Microsoft Document Imaging
- MDI_PROGRESSIVE = 34719 # 'Microsoft Document Imaging
- MDI_VECTOR = 34720 # 'Microsoft Document Imaging
- JPEG_LOSSY = 34892
- LZMA = 34925
- ZSTD = 34926
- OPS_PNG = 34933 # Objective Pathology Services
- OPS_JPEGXR = 34934 # Objective Pathology Services
- PIXTIFF = 50013
- KODAK_DCR = 65000
- PENTAX_PEF = 65535
- # def __bool__(self): return self != 1 # Python 3.6 only
- return COMPRESSION
- def PHOTOMETRIC():
- class PHOTOMETRIC(enum.IntEnum):
- MINISWHITE = 0
- MINISBLACK = 1
- RGB = 2
- PALETTE = 3
- MASK = 4
- SEPARATED = 5 # CMYK
- YCBCR = 6
- CIELAB = 8
- ICCLAB = 9
- ITULAB = 10
- CFA = 32803 # Color Filter Array
- LOGL = 32844
- LOGLUV = 32845
- LINEAR_RAW = 34892
- return PHOTOMETRIC
- def THRESHHOLD():
- class THRESHHOLD(enum.IntEnum):
- BILEVEL = 1
- HALFTONE = 2
- ERRORDIFFUSE = 3
- return THRESHHOLD
- def FILLORDER():
- class FILLORDER(enum.IntEnum):
- MSB2LSB = 1
- LSB2MSB = 2
- return FILLORDER
- def ORIENTATION():
- class ORIENTATION(enum.IntEnum):
- TOPLEFT = 1
- TOPRIGHT = 2
- BOTRIGHT = 3
- BOTLEFT = 4
- LEFTTOP = 5
- RIGHTTOP = 6
- RIGHTBOT = 7
- LEFTBOT = 8
- return ORIENTATION
- def PLANARCONFIG():
- class PLANARCONFIG(enum.IntEnum):
- CONTIG = 1
- SEPARATE = 2
- return PLANARCONFIG
- def GRAYRESPONSEUNIT():
- class GRAYRESPONSEUNIT(enum.IntEnum):
- _10S = 1
- _100S = 2
- _1000S = 3
- _10000S = 4
- _100000S = 5
- return GRAYRESPONSEUNIT
- def GROUP4OPT():
- class GROUP4OPT(enum.IntEnum):
- UNCOMPRESSED = 2
- return GROUP4OPT
- def RESUNIT():
- class RESUNIT(enum.IntEnum):
- NONE = 1
- INCH = 2
- CENTIMETER = 3
- # def __bool__(self): return self != 1 # Python 3.6 only
- return RESUNIT
- def COLORRESPONSEUNIT():
- class COLORRESPONSEUNIT(enum.IntEnum):
- _10S = 1
- _100S = 2
- _1000S = 3
- _10000S = 4
- _100000S = 5
- return COLORRESPONSEUNIT
- def PREDICTOR():
- class PREDICTOR(enum.IntEnum):
- NONE = 1
- HORIZONTAL = 2
- FLOATINGPOINT = 3
- # def __bool__(self): return self != 1 # Python 3.6 only
- return PREDICTOR
- def EXTRASAMPLE():
- class EXTRASAMPLE(enum.IntEnum):
- UNSPECIFIED = 0
- ASSOCALPHA = 1
- UNASSALPHA = 2
- return EXTRASAMPLE
- def SAMPLEFORMAT():
- class SAMPLEFORMAT(enum.IntEnum):
- UINT = 1
- INT = 2
- IEEEFP = 3
- VOID = 4
- COMPLEXINT = 5
- COMPLEXIEEEFP = 6
- return SAMPLEFORMAT
- def DATATYPES():
- class DATATYPES(enum.IntEnum):
- NOTYPE = 0
- BYTE = 1
- ASCII = 2
- SHORT = 3
- LONG = 4
- RATIONAL = 5
- SBYTE = 6
- UNDEFINED = 7
- SSHORT = 8
- SLONG = 9
- SRATIONAL = 10
- FLOAT = 11
- DOUBLE = 12
- IFD = 13
- UNICODE = 14
- COMPLEX = 15
- LONG8 = 16
- SLONG8 = 17
- IFD8 = 18
- return DATATYPES
- def DATA_FORMATS():
- # Map TIFF DATATYPES to Python struct formats
- return {
- 1: "1B", # BYTE 8-bit unsigned integer.
- 2: "1s", # ASCII 8-bit byte that contains a 7-bit ASCII code;
- # the last byte must be NULL (binary zero).
- 3: "1H", # SHORT 16-bit (2-byte) unsigned integer
- 4: "1I", # LONG 32-bit (4-byte) unsigned integer.
- 5: "2I", # RATIONAL Two LONGs: the first represents the numerator
- # of a fraction; the second, the denominator.
- 6: "1b", # SBYTE An 8-bit signed (twos-complement) integer.
- 7: "1B", # UNDEFINED An 8-bit byte that may contain anything,
- # depending on the definition of the field.
- 8: "1h", # SSHORT A 16-bit (2-byte) signed (twos-complement)
- # integer.
- 9: "1i", # SLONG A 32-bit (4-byte) signed (twos-complement)
- # integer.
- 10: "2i", # SRATIONAL Two SLONGs: the first represents the
- # numerator of a fraction, the second the denominator.
- 11: "1f", # FLOAT Single precision (4-byte) IEEE format.
- 12: "1d", # DOUBLE Double precision (8-byte) IEEE format.
- 13: "1I", # IFD unsigned 4 byte IFD offset.
- # 14: '', # UNICODE
- # 15: '', # COMPLEX
- 16: "1Q", # LONG8 unsigned 8 byte integer (BigTiff)
- 17: "1q", # SLONG8 signed 8 byte integer (BigTiff)
- 18: "1Q", # IFD8 unsigned 8 byte IFD offset (BigTiff)
- }
- def DATA_DTYPES():
- # Map numpy dtypes to TIFF DATATYPES
- return {
- "B": 1,
- "s": 2,
- "H": 3,
- "I": 4,
- "2I": 5,
- "b": 6,
- "h": 8,
- "i": 9,
- "2i": 10,
- "f": 11,
- "d": 12,
- "Q": 16,
- "q": 17,
- }
- def SAMPLE_DTYPES():
- # Map TIFF SampleFormats and BitsPerSample to numpy dtype
- return {
- (1, 1): "?", # bitmap
- (1, 2): "B",
- (1, 3): "B",
- (1, 4): "B",
- (1, 5): "B",
- (1, 6): "B",
- (1, 7): "B",
- (1, 8): "B",
- (1, 9): "H",
- (1, 10): "H",
- (1, 11): "H",
- (1, 12): "H",
- (1, 13): "H",
- (1, 14): "H",
- (1, 15): "H",
- (1, 16): "H",
- (1, 17): "I",
- (1, 18): "I",
- (1, 19): "I",
- (1, 20): "I",
- (1, 21): "I",
- (1, 22): "I",
- (1, 23): "I",
- (1, 24): "I",
- (1, 25): "I",
- (1, 26): "I",
- (1, 27): "I",
- (1, 28): "I",
- (1, 29): "I",
- (1, 30): "I",
- (1, 31): "I",
- (1, 32): "I",
- (1, 64): "Q",
- (2, 8): "b",
- (2, 16): "h",
- (2, 32): "i",
- (2, 64): "q",
- (3, 16): "e",
- (3, 32): "f",
- (3, 64): "d",
- (6, 64): "F",
- (6, 128): "D",
- (1, (5, 6, 5)): "B",
- }
- def COMPESSORS():
- # Map COMPRESSION to compress functions and default compression levels
- class Compressors(object):
- """Delay import compressor functions."""
- def __init__(self):
- self._compressors = {8: (zlib.compress, 6), 32946: (zlib.compress, 6)}
- def __getitem__(self, key):
- if key in self._compressors:
- return self._compressors[key]
- if key == 34925:
- try:
- import lzma # delayed import
- except ImportError:
- try:
- import backports.lzma as lzma # delayed import
- except ImportError:
- raise KeyError
- def lzma_compress(x, level):
- return lzma.compress(x)
- self._compressors[key] = lzma_compress, 0
- return lzma_compress, 0
- if key == 34926:
- try:
- import zstd # delayed import
- except ImportError:
- raise KeyError
- self._compressors[key] = zstd.compress, 9
- return zstd.compress, 9
- raise KeyError
- def __contains__(self, key):
- try:
- self[key]
- return True
- except KeyError:
- return False
- return Compressors()
- def DECOMPESSORS():
- # Map COMPRESSION to decompress functions
- class Decompressors(object):
- """Delay import decompressor functions."""
- def __init__(self):
- self._decompressors = {
- None: identityfunc,
- 1: identityfunc,
- 5: decode_lzw,
- 8: zlib.decompress,
- 32773: decode_packbits,
- 32946: zlib.decompress,
- }
- def __getitem__(self, key):
- if key in self._decompressors:
- return self._decompressors[key]
- if key == 7:
- try:
- from imagecodecs import jpeg, jpeg_12
- except ImportError:
- raise KeyError
- def decode_jpeg(x, table, bps, colorspace=None):
- if bps == 8:
- return jpeg.decode_jpeg(x, table, colorspace)
- elif bps == 12:
- return jpeg_12.decode_jpeg_12(x, table, colorspace)
- else:
- raise ValueError("bitspersample not supported")
- self._decompressors[key] = decode_jpeg
- return decode_jpeg
- if key == 34925:
- try:
- import lzma # delayed import
- except ImportError:
- try:
- import backports.lzma as lzma # delayed import
- except ImportError:
- raise KeyError
- self._decompressors[key] = lzma.decompress
- return lzma.decompress
- if key == 34926:
- try:
- import zstd # delayed import
- except ImportError:
- raise KeyError
- self._decompressors[key] = zstd.decompress
- return zstd.decompress
- raise KeyError
- def __contains__(self, item):
- try:
- self[item]
- return True
- except KeyError:
- return False
- return Decompressors()
- def FRAME_ATTRS():
- # Attributes that a TiffFrame shares with its keyframe
- return set("shape ndim size dtype axes is_final".split())
- def FILE_FLAGS():
- # TiffFile and TiffPage 'is_\*' attributes
- exclude = set(
- "reduced final memmappable contiguous tiled " "chroma_subsampled".split()
- )
- return set(
- a[3:] for a in dir(TiffPage) if a[:3] == "is_" and a[3:] not in exclude
- )
- def FILE_EXTENSIONS():
- # TIFF file extensions
- return tuple(
- "tif tiff ome.tif lsm stk qptiff pcoraw "
- "gel seq svs bif tf8 tf2 btf".split()
- )
- def FILEOPEN_FILTER():
- # String for use in Windows File Open box
- return [
- ("%s files" % ext.upper(), "*.%s" % ext) for ext in TIFF.FILE_EXTENSIONS
- ] + [("allfiles", "*")]
- def AXES_LABELS():
- # TODO: is there a standard for character axes labels?
- axes = {
- "X": "width",
- "Y": "height",
- "Z": "depth",
- "S": "sample", # rgb(a)
- "I": "series", # general sequence, plane, page, IFD
- "T": "time",
- "C": "channel", # color, emission wavelength
- "A": "angle",
- "P": "phase", # formerly F # P is Position in LSM!
- "R": "tile", # region, point, mosaic
- "H": "lifetime", # histogram
- "E": "lambda", # excitation wavelength
- "L": "exposure", # lux
- "V": "event",
- "Q": "other",
- "M": "mosaic", # LSM 6
- }
- axes.update(dict((v, k) for k, v in axes.items()))
- return axes
- def ANDOR_TAGS():
- # Andor Technology tags #4864 - 5030
- return set(range(4864, 5030))
- def EXIF_TAGS():
- tags = {
- # 65000 - 65112 Photoshop Camera RAW EXIF tags
- 65000: "OwnerName",
- 65001: "SerialNumber",
- 65002: "Lens",
- 65100: "RawFile",
- 65101: "Converter",
- 65102: "WhiteBalance",
- 65105: "Exposure",
- 65106: "Shadows",
- 65107: "Brightness",
- 65108: "Contrast",
- 65109: "Saturation",
- 65110: "Sharpness",
- 65111: "Smoothness",
- 65112: "MoireFilter",
- }
- tags.update(TIFF.TAGS)
- return tags
- def GPS_TAGS():
- return {
- 0: "GPSVersionID",
- 1: "GPSLatitudeRef",
- 2: "GPSLatitude",
- 3: "GPSLongitudeRef",
- 4: "GPSLongitude",
- 5: "GPSAltitudeRef",
- 6: "GPSAltitude",
- 7: "GPSTimeStamp",
- 8: "GPSSatellites",
- 9: "GPSStatus",
- 10: "GPSMeasureMode",
- 11: "GPSDOP",
- 12: "GPSSpeedRef",
- 13: "GPSSpeed",
- 14: "GPSTrackRef",
- 15: "GPSTrack",
- 16: "GPSImgDirectionRef",
- 17: "GPSImgDirection",
- 18: "GPSMapDatum",
- 19: "GPSDestLatitudeRef",
- 20: "GPSDestLatitude",
- 21: "GPSDestLongitudeRef",
- 22: "GPSDestLongitude",
- 23: "GPSDestBearingRef",
- 24: "GPSDestBearing",
- 25: "GPSDestDistanceRef",
- 26: "GPSDestDistance",
- 27: "GPSProcessingMethod",
- 28: "GPSAreaInformation",
- 29: "GPSDateStamp",
- 30: "GPSDifferential",
- 31: "GPSHPositioningError",
- }
- def IOP_TAGS():
- return {
- 1: "InteroperabilityIndex",
- 2: "InteroperabilityVersion",
- 4096: "RelatedImageFileFormat",
- 4097: "RelatedImageWidth",
- 4098: "RelatedImageLength",
- }
- def GEO_KEYS():
- return {
- 1024: "GTModelTypeGeoKey",
- 1025: "GTRasterTypeGeoKey",
- 1026: "GTCitationGeoKey",
- 2048: "GeographicTypeGeoKey",
- 2049: "GeogCitationGeoKey",
- 2050: "GeogGeodeticDatumGeoKey",
- 2051: "GeogPrimeMeridianGeoKey",
- 2052: "GeogLinearUnitsGeoKey",
- 2053: "GeogLinearUnitSizeGeoKey",
- 2054: "GeogAngularUnitsGeoKey",
- 2055: "GeogAngularUnitsSizeGeoKey",
- 2056: "GeogEllipsoidGeoKey",
- 2057: "GeogSemiMajorAxisGeoKey",
- 2058: "GeogSemiMinorAxisGeoKey",
- 2059: "GeogInvFlatteningGeoKey",
- 2060: "GeogAzimuthUnitsGeoKey",
- 2061: "GeogPrimeMeridianLongGeoKey",
- 2062: "GeogTOWGS84GeoKey",
- 3059: "ProjLinearUnitsInterpCorrectGeoKey", # GDAL
- 3072: "ProjectedCSTypeGeoKey",
- 3073: "PCSCitationGeoKey",
- 3074: "ProjectionGeoKey",
- 3075: "ProjCoordTransGeoKey",
- 3076: "ProjLinearUnitsGeoKey",
- 3077: "ProjLinearUnitSizeGeoKey",
- 3078: "ProjStdParallel1GeoKey",
- 3079: "ProjStdParallel2GeoKey",
- 3080: "ProjNatOriginLongGeoKey",
- 3081: "ProjNatOriginLatGeoKey",
- 3082: "ProjFalseEastingGeoKey",
- 3083: "ProjFalseNorthingGeoKey",
- 3084: "ProjFalseOriginLongGeoKey",
- 3085: "ProjFalseOriginLatGeoKey",
- 3086: "ProjFalseOriginEastingGeoKey",
- 3087: "ProjFalseOriginNorthingGeoKey",
- 3088: "ProjCenterLongGeoKey",
- 3089: "ProjCenterLatGeoKey",
- 3090: "ProjCenterEastingGeoKey",
- 3091: "ProjFalseOriginNorthingGeoKey",
- 3092: "ProjScaleAtNatOriginGeoKey",
- 3093: "ProjScaleAtCenterGeoKey",
- 3094: "ProjAzimuthAngleGeoKey",
- 3095: "ProjStraightVertPoleLongGeoKey",
- 3096: "ProjRectifiedGridAngleGeoKey",
- 4096: "VerticalCSTypeGeoKey",
- 4097: "VerticalCitationGeoKey",
- 4098: "VerticalDatumGeoKey",
- 4099: "VerticalUnitsGeoKey",
- }
- def GEO_CODES():
- try:
- from .tifffile_geodb import GEO_CODES # delayed import
- except (ImportError, ValueError):
- try:
- from tifffile_geodb import GEO_CODES # delayed import
- except (ImportError, ValueError):
- GEO_CODES = {}
- return GEO_CODES
- def CZ_LSMINFO():
- return [
- ("MagicNumber", "u4"),
- ("StructureSize", "i4"),
- ("DimensionX", "i4"),
- ("DimensionY", "i4"),
- ("DimensionZ", "i4"),
- ("DimensionChannels", "i4"),
- ("DimensionTime", "i4"),
- ("DataType", "i4"), # DATATYPES
- ("ThumbnailX", "i4"),
- ("ThumbnailY", "i4"),
- ("VoxelSizeX", "f8"),
- ("VoxelSizeY", "f8"),
- ("VoxelSizeZ", "f8"),
- ("OriginX", "f8"),
- ("OriginY", "f8"),
- ("OriginZ", "f8"),
- ("ScanType", "u2"),
- ("SpectralScan", "u2"),
- ("TypeOfData", "u4"), # TYPEOFDATA
- ("OffsetVectorOverlay", "u4"),
- ("OffsetInputLut", "u4"),
- ("OffsetOutputLut", "u4"),
- ("OffsetChannelColors", "u4"),
- ("TimeIntervall", "f8"),
- ("OffsetChannelDataTypes", "u4"),
- ("OffsetScanInformation", "u4"), # SCANINFO
- ("OffsetKsData", "u4"),
- ("OffsetTimeStamps", "u4"),
- ("OffsetEventList", "u4"),
- ("OffsetRoi", "u4"),
- ("OffsetBleachRoi", "u4"),
- ("OffsetNextRecording", "u4"),
- # LSM 2.0 ends here
- ("DisplayAspectX", "f8"),
- ("DisplayAspectY", "f8"),
- ("DisplayAspectZ", "f8"),
- ("DisplayAspectTime", "f8"),
- ("OffsetMeanOfRoisOverlay", "u4"),
- ("OffsetTopoIsolineOverlay", "u4"),
- ("OffsetTopoProfileOverlay", "u4"),
- ("OffsetLinescanOverlay", "u4"),
- ("ToolbarFlags", "u4"),
- ("OffsetChannelWavelength", "u4"),
- ("OffsetChannelFactors", "u4"),
- ("ObjectiveSphereCorrection", "f8"),
- ("OffsetUnmixParameters", "u4"),
- # LSM 3.2, 4.0 end here
- ("OffsetAcquisitionParameters", "u4"),
- ("OffsetCharacteristics", "u4"),
- ("OffsetPalette", "u4"),
- ("TimeDifferenceX", "f8"),
- ("TimeDifferenceY", "f8"),
- ("TimeDifferenceZ", "f8"),
- ("InternalUse1", "u4"),
- ("DimensionP", "i4"),
- ("DimensionM", "i4"),
- ("DimensionsReserved", "16i4"),
- ("OffsetTilePositions", "u4"),
- ("", "9u4"), # Reserved
- ("OffsetPositions", "u4"),
- # ('', '21u4'), # must be 0
- ]
- def CZ_LSMINFO_READERS():
- # Import functions for CZ_LSMINFO sub-records
- # TODO: read more CZ_LSMINFO sub-records
- return {
- "ScanInformation": read_lsm_scaninfo,
- "TimeStamps": read_lsm_timestamps,
- "EventList": read_lsm_eventlist,
- "ChannelColors": read_lsm_channelcolors,
- "Positions": read_lsm_floatpairs,
- "TilePositions": read_lsm_floatpairs,
- "VectorOverlay": None,
- "InputLut": None,
- "OutputLut": None,
- "TimeIntervall": None,
- "ChannelDataTypes": None,
- "KsData": None,
- "Roi": None,
- "BleachRoi": None,
- "NextRecording": None,
- "MeanOfRoisOverlay": None,
- "TopoIsolineOverlay": None,
- "TopoProfileOverlay": None,
- "ChannelWavelength": None,
- "SphereCorrection": None,
- "ChannelFactors": None,
- "UnmixParameters": None,
- "AcquisitionParameters": None,
- "Characteristics": None,
- }
- def CZ_LSMINFO_SCANTYPE():
- # Map CZ_LSMINFO.ScanType to dimension order
- return {
- 0: "XYZCT", # 'Stack' normal x-y-z-scan
- 1: "XYZCT", # 'Z-Scan' x-z-plane Y=1
- 2: "XYZCT", # 'Line'
- 3: "XYTCZ", # 'Time Series Plane' time series x-y XYCTZ ? Z=1
- 4: "XYZTC", # 'Time Series z-Scan' time series x-z
- 5: "XYTCZ", # 'Time Series Mean-of-ROIs'
- 6: "XYZTC", # 'Time Series Stack' time series x-y-z
- 7: "XYCTZ", # Spline Scan
- 8: "XYCZT", # Spline Plane x-z
- 9: "XYTCZ", # Time Series Spline Plane x-z
- 10: "XYZCT", # 'Time Series Point' point mode
- }
- def CZ_LSMINFO_DIMENSIONS():
- # Map dimension codes to CZ_LSMINFO attribute
- return {
- "X": "DimensionX",
- "Y": "DimensionY",
- "Z": "DimensionZ",
- "C": "DimensionChannels",
- "T": "DimensionTime",
- "P": "DimensionP",
- "M": "DimensionM",
- }
- def CZ_LSMINFO_DATATYPES():
- # Description of CZ_LSMINFO.DataType
- return {
- 0: "varying data types",
- 1: "8 bit unsigned integer",
- 2: "12 bit unsigned integer",
- 5: "32 bit float",
- }
- def CZ_LSMINFO_TYPEOFDATA():
- # Description of CZ_LSMINFO.TypeOfData
- return {
- 0: "Original scan data",
- 1: "Calculated data",
- 2: "3D reconstruction",
- 3: "Topography height map",
- }
- def CZ_LSMINFO_SCANINFO_ARRAYS():
- return {
- 0x20000000: "Tracks",
- 0x30000000: "Lasers",
- 0x60000000: "DetectionChannels",
- 0x80000000: "IlluminationChannels",
- 0xA0000000: "BeamSplitters",
- 0xC0000000: "DataChannels",
- 0x11000000: "Timers",
- 0x13000000: "Markers",
- }
- def CZ_LSMINFO_SCANINFO_STRUCTS():
- return {
- # 0x10000000: 'Recording',
- 0x40000000: "Track",
- 0x50000000: "Laser",
- 0x70000000: "DetectionChannel",
- 0x90000000: "IlluminationChannel",
- 0xB0000000: "BeamSplitter",
- 0xD0000000: "DataChannel",
- 0x12000000: "Timer",
- 0x14000000: "Marker",
- }
- def CZ_LSMINFO_SCANINFO_ATTRIBUTES():
- return {
- # Recording
- 0x10000001: "Name",
- 0x10000002: "Description",
- 0x10000003: "Notes",
- 0x10000004: "Objective",
- 0x10000005: "ProcessingSummary",
- 0x10000006: "SpecialScanMode",
- 0x10000007: "ScanType",
- 0x10000008: "ScanMode",
- 0x10000009: "NumberOfStacks",
- 0x1000000A: "LinesPerPlane",
- 0x1000000B: "SamplesPerLine",
- 0x1000000C: "PlanesPerVolume",
- 0x1000000D: "ImagesWidth",
- 0x1000000E: "ImagesHeight",
- 0x1000000F: "ImagesNumberPlanes",
- 0x10000010: "ImagesNumberStacks",
- 0x10000011: "ImagesNumberChannels",
- 0x10000012: "LinscanXySize",
- 0x10000013: "ScanDirection",
- 0x10000014: "TimeSeries",
- 0x10000015: "OriginalScanData",
- 0x10000016: "ZoomX",
- 0x10000017: "ZoomY",
- 0x10000018: "ZoomZ",
- 0x10000019: "Sample0X",
- 0x1000001A: "Sample0Y",
- 0x1000001B: "Sample0Z",
- 0x1000001C: "SampleSpacing",
- 0x1000001D: "LineSpacing",
- 0x1000001E: "PlaneSpacing",
- 0x1000001F: "PlaneWidth",
- 0x10000020: "PlaneHeight",
- 0x10000021: "VolumeDepth",
- 0x10000023: "Nutation",
- 0x10000034: "Rotation",
- 0x10000035: "Precession",
- 0x10000036: "Sample0time",
- 0x10000037: "StartScanTriggerIn",
- 0x10000038: "StartScanTriggerOut",
- 0x10000039: "StartScanEvent",
- 0x10000040: "StartScanTime",
- 0x10000041: "StopScanTriggerIn",
- 0x10000042: "StopScanTriggerOut",
- 0x10000043: "StopScanEvent",
- 0x10000044: "StopScanTime",
- 0x10000045: "UseRois",
- 0x10000046: "UseReducedMemoryRois",
- 0x10000047: "User",
- 0x10000048: "UseBcCorrection",
- 0x10000049: "PositionBcCorrection1",
- 0x10000050: "PositionBcCorrection2",
- 0x10000051: "InterpolationY",
- 0x10000052: "CameraBinning",
- 0x10000053: "CameraSupersampling",
- 0x10000054: "CameraFrameWidth",
- 0x10000055: "CameraFrameHeight",
- 0x10000056: "CameraOffsetX",
- 0x10000057: "CameraOffsetY",
- 0x10000059: "RtBinning",
- 0x1000005A: "RtFrameWidth",
- 0x1000005B: "RtFrameHeight",
- 0x1000005C: "RtRegionWidth",
- 0x1000005D: "RtRegionHeight",
- 0x1000005E: "RtOffsetX",
- 0x1000005F: "RtOffsetY",
- 0x10000060: "RtZoom",
- 0x10000061: "RtLinePeriod",
- 0x10000062: "Prescan",
- 0x10000063: "ScanDirectionZ",
- # Track
- 0x40000001: "MultiplexType", # 0 After Line; 1 After Frame
- 0x40000002: "MultiplexOrder",
- 0x40000003: "SamplingMode", # 0 Sample; 1 Line Avg; 2 Frame Avg
- 0x40000004: "SamplingMethod", # 1 Mean; 2 Sum
- 0x40000005: "SamplingNumber",
- 0x40000006: "Acquire",
- 0x40000007: "SampleObservationTime",
- 0x4000000B: "TimeBetweenStacks",
- 0x4000000C: "Name",
- 0x4000000D: "Collimator1Name",
- 0x4000000E: "Collimator1Position",
- 0x4000000F: "Collimator2Name",
- 0x40000010: "Collimator2Position",
- 0x40000011: "IsBleachTrack",
- 0x40000012: "IsBleachAfterScanNumber",
- 0x40000013: "BleachScanNumber",
- 0x40000014: "TriggerIn",
- 0x40000015: "TriggerOut",
- 0x40000016: "IsRatioTrack",
- 0x40000017: "BleachCount",
- 0x40000018: "SpiCenterWavelength",
- 0x40000019: "PixelTime",
- 0x40000021: "CondensorFrontlens",
- 0x40000023: "FieldStopValue",
- 0x40000024: "IdCondensorAperture",
- 0x40000025: "CondensorAperture",
- 0x40000026: "IdCondensorRevolver",
- 0x40000027: "CondensorFilter",
- 0x40000028: "IdTransmissionFilter1",
- 0x40000029: "IdTransmission1",
- 0x40000030: "IdTransmissionFilter2",
- 0x40000031: "IdTransmission2",
- 0x40000032: "RepeatBleach",
- 0x40000033: "EnableSpotBleachPos",
- 0x40000034: "SpotBleachPosx",
- 0x40000035: "SpotBleachPosy",
- 0x40000036: "SpotBleachPosz",
- 0x40000037: "IdTubelens",
- 0x40000038: "IdTubelensPosition",
- 0x40000039: "TransmittedLight",
- 0x4000003A: "ReflectedLight",
- 0x4000003B: "SimultanGrabAndBleach",
- 0x4000003C: "BleachPixelTime",
- # Laser
- 0x50000001: "Name",
- 0x50000002: "Acquire",
- 0x50000003: "Power",
- # DetectionChannel
- 0x70000001: "IntegrationMode",
- 0x70000002: "SpecialMode",
- 0x70000003: "DetectorGainFirst",
- 0x70000004: "DetectorGainLast",
- 0x70000005: "AmplifierGainFirst",
- 0x70000006: "AmplifierGainLast",
- 0x70000007: "AmplifierOffsFirst",
- 0x70000008: "AmplifierOffsLast",
- 0x70000009: "PinholeDiameter",
- 0x7000000A: "CountingTrigger",
- 0x7000000B: "Acquire",
- 0x7000000C: "PointDetectorName",
- 0x7000000D: "AmplifierName",
- 0x7000000E: "PinholeName",
- 0x7000000F: "FilterSetName",
- 0x70000010: "FilterName",
- 0x70000013: "IntegratorName",
- 0x70000014: "ChannelName",
- 0x70000015: "DetectorGainBc1",
- 0x70000016: "DetectorGainBc2",
- 0x70000017: "AmplifierGainBc1",
- 0x70000018: "AmplifierGainBc2",
- 0x70000019: "AmplifierOffsetBc1",
- 0x70000020: "AmplifierOffsetBc2",
- 0x70000021: "SpectralScanChannels",
- 0x70000022: "SpiWavelengthStart",
- 0x70000023: "SpiWavelengthStop",
- 0x70000026: "DyeName",
- 0x70000027: "DyeFolder",
- # IlluminationChannel
- 0x90000001: "Name",
- 0x90000002: "Power",
- 0x90000003: "Wavelength",
- 0x90000004: "Aquire",
- 0x90000005: "DetchannelName",
- 0x90000006: "PowerBc1",
- 0x90000007: "PowerBc2",
- # BeamSplitter
- 0xB0000001: "FilterSet",
- 0xB0000002: "Filter",
- 0xB0000003: "Name",
- # DataChannel
- 0xD0000001: "Name",
- 0xD0000003: "Acquire",
- 0xD0000004: "Color",
- 0xD0000005: "SampleType",
- 0xD0000006: "BitsPerSample",
- 0xD0000007: "RatioType",
- 0xD0000008: "RatioTrack1",
- 0xD0000009: "RatioTrack2",
- 0xD000000A: "RatioChannel1",
- 0xD000000B: "RatioChannel2",
- 0xD000000C: "RatioConst1",
- 0xD000000D: "RatioConst2",
- 0xD000000E: "RatioConst3",
- 0xD000000F: "RatioConst4",
- 0xD0000010: "RatioConst5",
- 0xD0000011: "RatioConst6",
- 0xD0000012: "RatioFirstImages1",
- 0xD0000013: "RatioFirstImages2",
- 0xD0000014: "DyeName",
- 0xD0000015: "DyeFolder",
- 0xD0000016: "Spectrum",
- 0xD0000017: "Acquire",
- # Timer
- 0x12000001: "Name",
- 0x12000002: "Description",
- 0x12000003: "Interval",
- 0x12000004: "TriggerIn",
- 0x12000005: "TriggerOut",
- 0x12000006: "ActivationTime",
- 0x12000007: "ActivationNumber",
- # Marker
- 0x14000001: "Name",
- 0x14000002: "Description",
- 0x14000003: "TriggerIn",
- 0x14000004: "TriggerOut",
- }
- def NIH_IMAGE_HEADER():
- return [
- ("FileID", "a8"),
- ("nLines", "i2"),
- ("PixelsPerLine", "i2"),
- ("Version", "i2"),
- ("OldLutMode", "i2"),
- ("OldnColors", "i2"),
- ("Colors", "u1", (3, 32)),
- ("OldColorStart", "i2"),
- ("ColorWidth", "i2"),
- ("ExtraColors", "u2", (6, 3)),
- ("nExtraColors", "i2"),
- ("ForegroundIndex", "i2"),
- ("BackgroundIndex", "i2"),
- ("XScale", "f8"),
- ("Unused2", "i2"),
- ("Unused3", "i2"),
- ("UnitsID", "i2"), # NIH_UNITS_TYPE
- ("p1", [("x", "i2"), ("y", "i2")]),
- ("p2", [("x", "i2"), ("y", "i2")]),
- ("CurveFitType", "i2"), # NIH_CURVEFIT_TYPE
- ("nCoefficients", "i2"),
- ("Coeff", "f8", 6),
- ("UMsize", "u1"),
- ("UM", "a15"),
- ("UnusedBoolean", "u1"),
- ("BinaryPic", "b1"),
- ("SliceStart", "i2"),
- ("SliceEnd", "i2"),
- ("ScaleMagnification", "f4"),
- ("nSlices", "i2"),
- ("SliceSpacing", "f4"),
- ("CurrentSlice", "i2"),
- ("FrameInterval", "f4"),
- ("PixelAspectRatio", "f4"),
- ("ColorStart", "i2"),
- ("ColorEnd", "i2"),
- ("nColors", "i2"),
- ("Fill1", "3u2"),
- ("Fill2", "3u2"),
- ("Table", "u1"), # NIH_COLORTABLE_TYPE
- ("LutMode", "u1"), # NIH_LUTMODE_TYPE
- ("InvertedTable", "b1"),
- ("ZeroClip", "b1"),
- ("XUnitSize", "u1"),
- ("XUnit", "a11"),
- ("StackType", "i2"), # NIH_STACKTYPE_TYPE
- # ('UnusedBytes', 'u1', 200)
- ]
- def NIH_COLORTABLE_TYPE():
- return (
- "CustomTable",
- "AppleDefault",
- "Pseudo20",
- "Pseudo32",
- "Rainbow",
- "Fire1",
- "Fire2",
- "Ice",
- "Grays",
- "Spectrum",
- )
- def NIH_LUTMODE_TYPE():
- return (
- "PseudoColor",
- "OldAppleDefault",
- "OldSpectrum",
- "GrayScale",
- "ColorLut",
- "CustomGrayscale",
- )
- def NIH_CURVEFIT_TYPE():
- return (
- "StraightLine",
- "Poly2",
- "Poly3",
- "Poly4",
- "Poly5",
- "ExpoFit",
- "PowerFit",
- "LogFit",
- "RodbardFit",
- "SpareFit1",
- "Uncalibrated",
- "UncalibratedOD",
- )
- def NIH_UNITS_TYPE():
- return (
- "Nanometers",
- "Micrometers",
- "Millimeters",
- "Centimeters",
- "Meters",
- "Kilometers",
- "Inches",
- "Feet",
- "Miles",
- "Pixels",
- "OtherUnits",
- )
- def NIH_STACKTYPE_TYPE():
- return ("VolumeStack", "RGBStack", "MovieStack", "HSVStack")
- def TVIPS_HEADER_V1():
- # TVIPS TemData structure from EMMENU Help file
- return [
- ("Version", "i4"),
- ("CommentV1", "a80"),
- ("HighTension", "i4"),
- ("SphericalAberration", "i4"),
- ("IlluminationAperture", "i4"),
- ("Magnification", "i4"),
- ("PostMagnification", "i4"),
- ("FocalLength", "i4"),
- ("Defocus", "i4"),
- ("Astigmatism", "i4"),
- ("AstigmatismDirection", "i4"),
- ("BiprismVoltage", "i4"),
- ("SpecimenTiltAngle", "i4"),
- ("SpecimenTiltDirection", "i4"),
- ("IlluminationTiltDirection", "i4"),
- ("IlluminationTiltAngle", "i4"),
- ("ImageMode", "i4"),
- ("EnergySpread", "i4"),
- ("ChromaticAberration", "i4"),
- ("ShutterType", "i4"),
- ("DefocusSpread", "i4"),
- ("CcdNumber", "i4"),
- ("CcdSize", "i4"),
- ("OffsetXV1", "i4"),
- ("OffsetYV1", "i4"),
- ("PhysicalPixelSize", "i4"),
- ("Binning", "i4"),
- ("ReadoutSpeed", "i4"),
- ("GainV1", "i4"),
- ("SensitivityV1", "i4"),
- ("ExposureTimeV1", "i4"),
- ("FlatCorrected", "i4"),
- ("DeadPxCorrected", "i4"),
- ("ImageMean", "i4"),
- ("ImageStd", "i4"),
- ("DisplacementX", "i4"),
- ("DisplacementY", "i4"),
- ("DateV1", "i4"),
- ("TimeV1", "i4"),
- ("ImageMin", "i4"),
- ("ImageMax", "i4"),
- ("ImageStatisticsQuality", "i4"),
- ]
- def TVIPS_HEADER_V2():
- return [
- ("ImageName", "V160"), # utf16
- ("ImageFolder", "V160"),
- ("ImageSizeX", "i4"),
- ("ImageSizeY", "i4"),
- ("ImageSizeZ", "i4"),
- ("ImageSizeE", "i4"),
- ("ImageDataType", "i4"),
- ("Date", "i4"),
- ("Time", "i4"),
- ("Comment", "V1024"),
- ("ImageHistory", "V1024"),
- ("Scaling", "16f4"),
- ("ImageStatistics", "16c16"),
- ("ImageType", "i4"),
- ("ImageDisplaType", "i4"),
- ("PixelSizeX", "f4"), # distance between two px in x, [nm]
- ("PixelSizeY", "f4"), # distance between two px in y, [nm]
- ("ImageDistanceZ", "f4"),
- ("ImageDistanceE", "f4"),
- ("ImageMisc", "32f4"),
- ("TemType", "V160"),
- ("TemHighTension", "f4"),
- ("TemAberrations", "32f4"),
- ("TemEnergy", "32f4"),
- ("TemMode", "i4"),
- ("TemMagnification", "f4"),
- ("TemMagnificationCorrection", "f4"),
- ("PostMagnification", "f4"),
- ("TemStageType", "i4"),
- ("TemStagePosition", "5f4"), # x, y, z, a, b
- ("TemImageShift", "2f4"),
- ("TemBeamShift", "2f4"),
- ("TemBeamTilt", "2f4"),
- ("TilingParameters", "7f4"), # 0: tiling? 1:x 2:y 3: max x
- # 4: max y 5: overlap x 6: overlap y
- ("TemIllumination", "3f4"), # 0: spotsize 1: intensity
- ("TemShutter", "i4"),
- ("TemMisc", "32f4"),
- ("CameraType", "V160"),
- ("PhysicalPixelSizeX", "f4"),
- ("PhysicalPixelSizeY", "f4"),
- ("OffsetX", "i4"),
- ("OffsetY", "i4"),
- ("BinningX", "i4"),
- ("BinningY", "i4"),
- ("ExposureTime", "f4"),
- ("Gain", "f4"),
- ("ReadoutRate", "f4"),
- ("FlatfieldDescription", "V160"),
- ("Sensitivity", "f4"),
- ("Dose", "f4"),
- ("CamMisc", "32f4"),
- ("FeiMicroscopeInformation", "V1024"),
- ("FeiSpecimenInformation", "V1024"),
- ("Magic", "u4"),
- ]
- def MM_HEADER():
- # Olympus FluoView MM_Header
- MM_DIMENSION = [
- ("Name", "a16"),
- ("Size", "i4"),
- ("Origin", "f8"),
- ("Resolution", "f8"),
- ("Unit", "a64"),
- ]
- return [
- ("HeaderFlag", "i2"),
- ("ImageType", "u1"),
- ("ImageName", "a257"),
- ("OffsetData", "u4"),
- ("PaletteSize", "i4"),
- ("OffsetPalette0", "u4"),
- ("OffsetPalette1", "u4"),
- ("CommentSize", "i4"),
- ("OffsetComment", "u4"),
- ("Dimensions", MM_DIMENSION, 10),
- ("OffsetPosition", "u4"),
- ("MapType", "i2"),
- ("MapMin", "f8"),
- ("MapMax", "f8"),
- ("MinValue", "f8"),
- ("MaxValue", "f8"),
- ("OffsetMap", "u4"),
- ("Gamma", "f8"),
- ("Offset", "f8"),
- ("GrayChannel", MM_DIMENSION),
- ("OffsetThumbnail", "u4"),
- ("VoiceField", "i4"),
- ("OffsetVoiceField", "u4"),
- ]
- def MM_DIMENSIONS():
- # Map FluoView MM_Header.Dimensions to axes characters
- return {
- "X": "X",
- "Y": "Y",
- "Z": "Z",
- "T": "T",
- "CH": "C",
- "WAVELENGTH": "C",
- "TIME": "T",
- "XY": "R",
- "EVENT": "V",
- "EXPOSURE": "L",
- }
- def UIC_TAGS():
- # Map Universal Imaging Corporation MetaMorph internal tag ids to
- # name and type
- from fractions import Fraction # delayed import
- return [
- ("AutoScale", int),
- ("MinScale", int),
- ("MaxScale", int),
- ("SpatialCalibration", int),
- ("XCalibration", Fraction),
- ("YCalibration", Fraction),
- ("CalibrationUnits", str),
- ("Name", str),
- ("ThreshState", int),
- ("ThreshStateRed", int),
- ("tagid_10", None), # undefined
- ("ThreshStateGreen", int),
- ("ThreshStateBlue", int),
- ("ThreshStateLo", int),
- ("ThreshStateHi", int),
- ("Zoom", int),
- ("CreateTime", julian_datetime),
- ("LastSavedTime", julian_datetime),
- ("currentBuffer", int),
- ("grayFit", None),
- ("grayPointCount", None),
- ("grayX", Fraction),
- ("grayY", Fraction),
- ("grayMin", Fraction),
- ("grayMax", Fraction),
- ("grayUnitName", str),
- ("StandardLUT", int),
- ("wavelength", int),
- ("StagePosition", "(%i,2,2)u4"), # N xy positions as fract
- ("CameraChipOffset", "(%i,2,2)u4"), # N xy offsets as fract
- ("OverlayMask", None),
- ("OverlayCompress", None),
- ("Overlay", None),
- ("SpecialOverlayMask", None),
- ("SpecialOverlayCompress", None),
- ("SpecialOverlay", None),
- ("ImageProperty", read_uic_image_property),
- ("StageLabel", "%ip"), # N str
- ("AutoScaleLoInfo", Fraction),
- ("AutoScaleHiInfo", Fraction),
- ("AbsoluteZ", "(%i,2)u4"), # N fractions
- ("AbsoluteZValid", "(%i,)u4"), # N long
- ("Gamma", "I"), # 'I' uses offset
- ("GammaRed", "I"),
- ("GammaGreen", "I"),
- ("GammaBlue", "I"),
- ("CameraBin", "2I"),
- ("NewLUT", int),
- ("ImagePropertyEx", None),
- ("PlaneProperty", int),
- ("UserLutTable", "(256,3)u1"),
- ("RedAutoScaleInfo", int),
- ("RedAutoScaleLoInfo", Fraction),
- ("RedAutoScaleHiInfo", Fraction),
- ("RedMinScaleInfo", int),
- ("RedMaxScaleInfo", int),
- ("GreenAutoScaleInfo", int),
- ("GreenAutoScaleLoInfo", Fraction),
- ("GreenAutoScaleHiInfo", Fraction),
- ("GreenMinScaleInfo", int),
- ("GreenMaxScaleInfo", int),
- ("BlueAutoScaleInfo", int),
- ("BlueAutoScaleLoInfo", Fraction),
- ("BlueAutoScaleHiInfo", Fraction),
- ("BlueMinScaleInfo", int),
- ("BlueMaxScaleInfo", int),
- # ('OverlayPlaneColor', read_uic_overlay_plane_color),
- ]
- def PILATUS_HEADER():
- # PILATUS CBF Header Specification, Version 1.4
- # Map key to [value_indices], type
- return {
- "Detector": ([slice(1, None)], str),
- "Pixel_size": ([1, 4], float),
- "Silicon": ([3], float),
- "Exposure_time": ([1], float),
- "Exposure_period": ([1], float),
- "Tau": ([1], float),
- "Count_cutoff": ([1], int),
- "Threshold_setting": ([1], float),
- "Gain_setting": ([1, 2], str),
- "N_excluded_pixels": ([1], int),
- "Excluded_pixels": ([1], str),
- "Flat_field": ([1], str),
- "Trim_file": ([1], str),
- "Image_path": ([1], str),
- # optional
- "Wavelength": ([1], float),
- "Energy_range": ([1, 2], float),
- "Detector_distance": ([1], float),
- "Detector_Voffset": ([1], float),
- "Beam_xy": ([1, 2], float),
- "Flux": ([1], str),
- "Filter_transmission": ([1], float),
- "Start_angle": ([1], float),
- "Angle_increment": ([1], float),
- "Detector_2theta": ([1], float),
- "Polarization": ([1], float),
- "Alpha": ([1], float),
- "Kappa": ([1], float),
- "Phi": ([1], float),
- "Phi_increment": ([1], float),
- "Chi": ([1], float),
- "Chi_increment": ([1], float),
- "Oscillation_axis": ([slice(1, None)], str),
- "N_oscillations": ([1], int),
- "Start_position": ([1], float),
- "Position_increment": ([1], float),
- "Shutter_time": ([1], float),
- "Omega": ([1], float),
- "Omega_increment": ([1], float),
- }
- def REVERSE_BITORDER_BYTES():
- # Bytes with reversed bitorder
- return (
- b"\x00\x80@\xc0 \xa0`\xe0\x10\x90P\xd00\xb0p\xf0\x08\x88H\xc8("
- b"\xa8h\xe8\x18\x98X\xd88\xb8x\xf8\x04\x84D\xc4$\xa4d\xe4\x14"
- b"\x94T\xd44\xb4t\xf4\x0c\x8cL\xcc,\xacl\xec\x1c\x9c\\\xdc<\xbc|"
- b'\xfc\x02\x82B\xc2"\xa2b\xe2\x12\x92R\xd22\xb2r\xf2\n\x8aJ\xca*'
- b"\xaaj\xea\x1a\x9aZ\xda:\xbaz\xfa\x06\x86F\xc6&\xa6f\xe6\x16"
- b"\x96V\xd66\xb6v\xf6\x0e\x8eN\xce.\xaen\xee\x1e\x9e^\xde>\xbe~"
- b"\xfe\x01\x81A\xc1!\xa1a\xe1\x11\x91Q\xd11\xb1q\xf1\t\x89I\xc9)"
- b"\xa9i\xe9\x19\x99Y\xd99\xb9y\xf9\x05\x85E\xc5%\xa5e\xe5\x15"
- b"\x95U\xd55\xb5u\xf5\r\x8dM\xcd-\xadm\xed\x1d\x9d]\xdd=\xbd}"
- b"\xfd\x03\x83C\xc3#\xa3c\xe3\x13\x93S\xd33\xb3s\xf3\x0b\x8bK"
- b"\xcb+\xabk\xeb\x1b\x9b[\xdb;\xbb{\xfb\x07\x87G\xc7'\xa7g\xe7"
- b"\x17\x97W\xd77\xb7w\xf7\x0f\x8fO\xcf/\xafo\xef\x1f\x9f_"
- b"\xdf?\xbf\x7f\xff"
- )
- def REVERSE_BITORDER_ARRAY():
- # Numpy array of bytes with reversed bitorder
- return numpy.frombuffer(TIFF.REVERSE_BITORDER_BYTES, dtype="uint8")
- def ALLOCATIONGRANULARITY():
- # alignment for writing contiguous data to TIFF
- import mmap # delayed import
- return mmap.ALLOCATIONGRANULARITY
- def read_tags(fh, byteorder, offsetsize, tagnames, customtags=None, maxifds=None):
- """Read tags from chain of IFDs and return as list of dicts.
- The file handle position must be at a valid IFD header.
- """
- if offsetsize == 4:
- offsetformat = byteorder + "I"
- tagnosize = 2
- tagnoformat = byteorder + "H"
- tagsize = 12
- tagformat1 = byteorder + "HH"
- tagformat2 = byteorder + "I4s"
- elif offsetsize == 8:
- offsetformat = byteorder + "Q"
- tagnosize = 8
- tagnoformat = byteorder + "Q"
- tagsize = 20
- tagformat1 = byteorder + "HH"
- tagformat2 = byteorder + "Q8s"
- else:
- raise ValueError("invalid offset size")
- if customtags is None:
- customtags = {}
- if maxifds is None:
- maxifds = 2**32
- result = []
- unpack = struct.unpack
- offset = fh.tell()
- while len(result) < maxifds:
- # loop over IFDs
- try:
- tagno = unpack(tagnoformat, fh.read(tagnosize))[0]
- if tagno > 4096:
- raise ValueError("suspicious number of tags")
- except Exception:
- warnings.warn("corrupted tag list at offset %i" % offset)
- break
- tags = {}
- data = fh.read(tagsize * tagno)
- pos = fh.tell()
- index = 0
- for _ in range(tagno):
- code, type_ = unpack(tagformat1, data[index : index + 4])
- count, value = unpack(tagformat2, data[index + 4 : index + tagsize])
- index += tagsize
- name = tagnames.get(code, str(code))
- try:
- dtype = TIFF.DATA_FORMATS[type_]
- except KeyError:
- raise TiffTag.Error("unknown tag data type %i" % type_)
- fmt = "%s%i%s" % (byteorder, count * int(dtype[0]), dtype[1])
- size = struct.calcsize(fmt)
- if size > offsetsize or code in customtags:
- offset = unpack(offsetformat, value)[0]
- if offset < 8 or offset > fh.size - size:
- raise TiffTag.Error("invalid tag value offset %i" % offset)
- fh.seek(offset)
- if code in customtags:
- readfunc = customtags[code][1]
- value = readfunc(fh, byteorder, dtype, count, offsetsize)
- elif type_ == 7 or (count > 1 and dtype[-1] == "B"):
- value = read_bytes(fh, byteorder, dtype, count, offsetsize)
- elif code in tagnames or dtype[-1] == "s":
- value = unpack(fmt, fh.read(size))
- else:
- value = read_numpy(fh, byteorder, dtype, count, offsetsize)
- elif dtype[-1] == "B" or type_ == 7:
- value = value[:size]
- else:
- value = unpack(fmt, value[:size])
- if code not in customtags and code not in TIFF.TAG_TUPLE:
- if len(value) == 1:
- value = value[0]
- if type_ != 7 and dtype[-1] == "s" and isinstance(value, bytes):
- # TIFF ASCII fields can contain multiple strings,
- # each terminated with a NUL
- try:
- value = bytes2str(stripascii(value).strip())
- except UnicodeDecodeError:
- warnings.warn("tag %i: coercing invalid ASCII to bytes" % code)
- tags[name] = value
- result.append(tags)
- # read offset to next page
- fh.seek(pos)
- offset = unpack(offsetformat, fh.read(offsetsize))[0]
- if offset == 0:
- break
- if offset >= fh.size:
- warnings.warn("invalid page offset %i" % offset)
- break
- fh.seek(offset)
- if result and maxifds == 1:
- result = result[0]
- return result
- def read_exif_ifd(fh, byteorder, dtype, count, offsetsize):
- """Read EXIF tags from file and return as dict."""
- exif = read_tags(fh, byteorder, offsetsize, TIFF.EXIF_TAGS, maxifds=1)
- for name in ("ExifVersion", "FlashpixVersion"):
- try:
- exif[name] = bytes2str(exif[name])
- except Exception:
- pass
- if "UserComment" in exif:
- idcode = exif["UserComment"][:8]
- try:
- if idcode == b"ASCII\x00\x00\x00":
- exif["UserComment"] = bytes2str(exif["UserComment"][8:])
- elif idcode == b"UNICODE\x00":
- exif["UserComment"] = exif["UserComment"][8:].decode("utf-16")
- except Exception:
- pass
- return exif
- def read_gps_ifd(fh, byteorder, dtype, count, offsetsize):
- """Read GPS tags from file and return as dict."""
- return read_tags(fh, byteorder, offsetsize, TIFF.GPS_TAGS, maxifds=1)
- def read_interoperability_ifd(fh, byteorder, dtype, count, offsetsize):
- """Read Interoperability tags from file and return as dict."""
- tag_names = {1: "InteroperabilityIndex"}
- return read_tags(fh, byteorder, offsetsize, tag_names, maxifds=1)
- def read_bytes(fh, byteorder, dtype, count, offsetsize):
- """Read tag data from file and return as byte string."""
- dtype = "B" if dtype[-1] == "s" else byteorder + dtype[-1]
- count *= numpy.dtype(dtype).itemsize
- data = fh.read(count)
- if len(data) != count:
- warnings.warn("failed to read all bytes: %i, %i" % (len(data), count))
- return data
- def read_utf8(fh, byteorder, dtype, count, offsetsize):
- """Read tag data from file and return as unicode string."""
- return fh.read(count).decode("utf-8")
- def read_numpy(fh, byteorder, dtype, count, offsetsize):
- """Read tag data from file and return as numpy array."""
- dtype = "b" if dtype[-1] == "s" else byteorder + dtype[-1]
- return fh.read_array(dtype, count)
- def read_colormap(fh, byteorder, dtype, count, offsetsize):
- """Read ColorMap data from file and return as numpy array."""
- cmap = fh.read_array(byteorder + dtype[-1], count)
- cmap.shape = (3, -1)
- return cmap
- def read_json(fh, byteorder, dtype, count, offsetsize):
- """Read JSON tag data from file and return as object."""
- data = fh.read(count)
- try:
- return json.loads(unicode(stripnull(data), "utf-8"))
- except ValueError:
- warnings.warn("invalid JSON '%s'" % data)
- def read_mm_header(fh, byteorder, dtype, count, offsetsize):
- """Read FluoView mm_header tag from file and return as dict."""
- mmh = fh.read_record(TIFF.MM_HEADER, byteorder=byteorder)
- mmh = recarray2dict(mmh)
- mmh["Dimensions"] = [
- (bytes2str(d[0]).strip(), d[1], d[2], d[3], bytes2str(d[4]).strip())
- for d in mmh["Dimensions"]
- ]
- d = mmh["GrayChannel"]
- mmh["GrayChannel"] = (
- bytes2str(d[0]).strip(),
- d[1],
- d[2],
- d[3],
- bytes2str(d[4]).strip(),
- )
- return mmh
- def read_mm_stamp(fh, byteorder, dtype, count, offsetsize):
- """Read FluoView mm_stamp tag from file and return as numpy.ndarray."""
- return fh.read_array(byteorder + "f8", 8)
- def read_uic1tag(fh, byteorder, dtype, count, offsetsize, planecount=None):
- """Read MetaMorph STK UIC1Tag from file and return as dict.
- Return empty dictionary if planecount is unknown.
- """
- assert dtype in ("2I", "1I") and byteorder == "<"
- result = {}
- if dtype == "2I":
- # pre MetaMorph 2.5 (not tested)
- values = fh.read_array("<u4", 2 * count).reshape(count, 2)
- result = {"ZDistance": values[:, 0] / values[:, 1]}
- elif planecount:
- for _ in range(count):
- tagid = struct.unpack("<I", fh.read(4))[0]
- if tagid in (28, 29, 37, 40, 41):
- # silently skip unexpected tags
- fh.read(4)
- continue
- name, value = read_uic_tag(fh, tagid, planecount, offset=True)
- result[name] = value
- return result
- def read_uic2tag(fh, byteorder, dtype, planecount, offsetsize):
- """Read MetaMorph STK UIC2Tag from file and return as dict."""
- assert dtype == "2I" and byteorder == "<"
- values = fh.read_array("<u4", 6 * planecount).reshape(planecount, 6)
- return {
- "ZDistance": values[:, 0] / values[:, 1],
- "DateCreated": values[:, 2], # julian days
- "TimeCreated": values[:, 3], # milliseconds
- "DateModified": values[:, 4], # julian days
- "TimeModified": values[:, 5],
- } # milliseconds
- def read_uic3tag(fh, byteorder, dtype, planecount, offsetsize):
- """Read MetaMorph STK UIC3Tag from file and return as dict."""
- assert dtype == "2I" and byteorder == "<"
- values = fh.read_array("<u4", 2 * planecount).reshape(planecount, 2)
- return {"Wavelengths": values[:, 0] / values[:, 1]}
- def read_uic4tag(fh, byteorder, dtype, planecount, offsetsize):
- """Read MetaMorph STK UIC4Tag from file and return as dict."""
- assert dtype == "1I" and byteorder == "<"
- result = {}
- while True:
- tagid = struct.unpack("<H", fh.read(2))[0]
- if tagid == 0:
- break
- name, value = read_uic_tag(fh, tagid, planecount, offset=False)
- result[name] = value
- return result
- def read_uic_tag(fh, tagid, planecount, offset):
- """Read a single UIC tag value from file and return tag name and value.
- UIC1Tags use an offset.
- """
- def read_int(count=1):
- value = struct.unpack("<%iI" % count, fh.read(4 * count))
- return value[0] if count == 1 else value
- try:
- name, dtype = TIFF.UIC_TAGS[tagid]
- except IndexError:
- # unknown tag
- return "_TagId%i" % tagid, read_int()
- Fraction = TIFF.UIC_TAGS[4][1]
- if offset:
- pos = fh.tell()
- if dtype not in (int, None):
- off = read_int()
- if off < 8:
- if dtype is str:
- return name, ""
- warnings.warn("invalid offset for uic tag '%s': %i" % (name, off))
- return name, off
- fh.seek(off)
- if dtype is None:
- # skip
- name = "_" + name
- value = read_int()
- elif dtype is int:
- # int
- value = read_int()
- elif dtype is Fraction:
- # fraction
- value = read_int(2)
- value = value[0] / value[1]
- elif dtype is julian_datetime:
- # datetime
- value = julian_datetime(*read_int(2))
- elif dtype is read_uic_image_property:
- # ImagePropertyEx
- value = read_uic_image_property(fh)
- elif dtype is str:
- # pascal string
- size = read_int()
- if 0 <= size < 2**10:
- value = struct.unpack("%is" % size, fh.read(size))[0][:-1]
- value = bytes2str(stripnull(value))
- elif offset:
- value = ""
- warnings.warn("corrupt string in uic tag '%s'" % name)
- else:
- raise ValueError("invalid string size: %i" % size)
- elif dtype == "%ip":
- # sequence of pascal strings
- value = []
- for _ in range(planecount):
- size = read_int()
- if 0 <= size < 2**10:
- string = struct.unpack("%is" % size, fh.read(size))[0][:-1]
- string = bytes2str(stripnull(string))
- value.append(string)
- elif offset:
- warnings.warn("corrupt string in uic tag '%s'" % name)
- else:
- raise ValueError("invalid string size: %i" % size)
- else:
- # struct or numpy type
- dtype = "<" + dtype
- if "%i" in dtype:
- dtype = dtype % planecount
- if "(" in dtype:
- # numpy type
- value = fh.read_array(dtype, 1)[0]
- if value.shape[-1] == 2:
- # assume fractions
- value = value[..., 0] / value[..., 1]
- else:
- # struct format
- value = struct.unpack(dtype, fh.read(struct.calcsize(dtype)))
- if len(value) == 1:
- value = value[0]
- if offset:
- fh.seek(pos + 4)
- return name, value
- def read_uic_image_property(fh):
- """Read UIC ImagePropertyEx tag from file and return as dict."""
- # TODO: test this
- size = struct.unpack("B", fh.read(1))[0]
- name = struct.unpack("%is" % size, fh.read(size))[0][:-1]
- flags, prop = struct.unpack("<IB", fh.read(5))
- if prop == 1:
- value = struct.unpack("II", fh.read(8))
- value = value[0] / value[1]
- else:
- size = struct.unpack("B", fh.read(1))[0]
- value = struct.unpack("%is" % size, fh.read(size))[0]
- return dict(name=name, flags=flags, value=value)
- def read_cz_lsminfo(fh, byteorder, dtype, count, offsetsize):
- """Read CZ_LSMINFO tag from file and return as dict."""
- assert byteorder == "<"
- magic_number, structure_size = struct.unpack("<II", fh.read(8))
- if magic_number not in (50350412, 67127628):
- raise ValueError("invalid CZ_LSMINFO structure")
- fh.seek(-8, 1)
- if structure_size < numpy.dtype(TIFF.CZ_LSMINFO).itemsize:
- # adjust structure according to structure_size
- lsminfo = []
- size = 0
- for name, dtype in TIFF.CZ_LSMINFO:
- size += numpy.dtype(dtype).itemsize
- if size > structure_size:
- break
- lsminfo.append((name, dtype))
- else:
- lsminfo = TIFF.CZ_LSMINFO
- lsminfo = fh.read_record(lsminfo, byteorder=byteorder)
- lsminfo = recarray2dict(lsminfo)
- # read LSM info subrecords at offsets
- for name, reader in TIFF.CZ_LSMINFO_READERS.items():
- if reader is None:
- continue
- offset = lsminfo.get("Offset" + name, 0)
- if offset < 8:
- continue
- fh.seek(offset)
- try:
- lsminfo[name] = reader(fh)
- except ValueError:
- pass
- return lsminfo
- def read_lsm_floatpairs(fh):
- """Read LSM sequence of float pairs from file and return as list."""
- size = struct.unpack("<i", fh.read(4))[0]
- return fh.read_array("<2f8", count=size)
- def read_lsm_positions(fh):
- """Read LSM positions from file and return as list."""
- size = struct.unpack("<I", fh.read(4))[0]
- return fh.read_array("<2f8", count=size)
- def read_lsm_timestamps(fh):
- """Read LSM time stamps from file and return as list."""
- size, count = struct.unpack("<ii", fh.read(8))
- if size != (8 + 8 * count):
- warnings.warn("invalid LSM TimeStamps block")
- return []
- # return struct.unpack('<%dd' % count, fh.read(8*count))
- return fh.read_array("<f8", count=count)
- def read_lsm_eventlist(fh):
- """Read LSM events from file and return as list of (time, type, text)."""
- count = struct.unpack("<II", fh.read(8))[1]
- events = []
- while count > 0:
- esize, etime, etype = struct.unpack("<IdI", fh.read(16))
- etext = bytes2str(stripnull(fh.read(esize - 16)))
- events.append((etime, etype, etext))
- count -= 1
- return events
- def read_lsm_channelcolors(fh):
- """Read LSM ChannelColors structure from file and return as dict."""
- result = {"Mono": False, "Colors": [], "ColorNames": []}
- pos = fh.tell()
- (size, ncolors, nnames, coffset, noffset, mono) = struct.unpack(
- "<IIIIII", fh.read(24)
- )
- if ncolors != nnames:
- warnings.warn("invalid LSM ChannelColors structure")
- return result
- result["Mono"] = bool(mono)
- # Colors
- fh.seek(pos + coffset)
- colors = fh.read_array("uint8", count=ncolors * 4).reshape((ncolors, 4))
- result["Colors"] = colors.tolist()
- # ColorNames
- fh.seek(pos + noffset)
- buffer = fh.read(size - noffset)
- names = []
- while len(buffer) > 4:
- size = struct.unpack("<I", buffer[:4])[0]
- names.append(bytes2str(buffer[4 : 3 + size]))
- buffer = buffer[4 + size :]
- result["ColorNames"] = names
- return result
- def read_lsm_scaninfo(fh):
- """Read LSM ScanInfo structure from file and return as dict."""
- block = {}
- blocks = [block]
- unpack = struct.unpack
- if struct.unpack("<I", fh.read(4))[0] != 0x10000000:
- # not a Recording sub block
- warnings.warn("invalid LSM ScanInfo structure")
- return block
- fh.read(8)
- while True:
- entry, dtype, size = unpack("<III", fh.read(12))
- if dtype == 2:
- # ascii
- value = bytes2str(stripnull(fh.read(size)))
- elif dtype == 4:
- # long
- value = unpack("<i", fh.read(4))[0]
- elif dtype == 5:
- # rational
- value = unpack("<d", fh.read(8))[0]
- else:
- value = 0
- if entry in TIFF.CZ_LSMINFO_SCANINFO_ARRAYS:
- blocks.append(block)
- name = TIFF.CZ_LSMINFO_SCANINFO_ARRAYS[entry]
- newobj = []
- block[name] = newobj
- block = newobj
- elif entry in TIFF.CZ_LSMINFO_SCANINFO_STRUCTS:
- blocks.append(block)
- newobj = {}
- block.append(newobj)
- block = newobj
- elif entry in TIFF.CZ_LSMINFO_SCANINFO_ATTRIBUTES:
- name = TIFF.CZ_LSMINFO_SCANINFO_ATTRIBUTES[entry]
- block[name] = value
- elif entry == 0xFFFFFFFF:
- # end sub block
- block = blocks.pop()
- else:
- # unknown entry
- block["Entry0x%x" % entry] = value
- if not blocks:
- break
- return block
- def read_tvips_header(fh, byteorder, dtype, count, offsetsize):
- """Read TVIPS EM-MENU headers and return as dict."""
- result = {}
- header = fh.read_record(TIFF.TVIPS_HEADER_V1, byteorder=byteorder)
- for name, typestr in TIFF.TVIPS_HEADER_V1:
- result[name] = header[name].tolist()
- if header["Version"] == 2:
- header = fh.read_record(TIFF.TVIPS_HEADER_V2, byteorder=byteorder)
- if header["Magic"] != int(0xAAAAAAAA):
- warnings.warn("invalid TVIPS v2 magic number")
- return {}
- # decode utf16 strings
- for name, typestr in TIFF.TVIPS_HEADER_V2:
- if typestr.startswith("V"):
- s = header[name].tostring().decode("utf16", errors="ignore")
- result[name] = stripnull(s, null="\0")
- else:
- result[name] = header[name].tolist()
- # convert nm to m
- for axis in "XY":
- header["PhysicalPixelSize" + axis] /= 1e9
- header["PixelSize" + axis] /= 1e9
- elif header.version != 1:
- warnings.warn("unknown TVIPS header version")
- return {}
- return result
- def read_fei_metadata(fh, byteorder, dtype, count, offsetsize):
- """Read FEI SFEG/HELIOS headers and return as dict."""
- result = {}
- section = {}
- data = bytes2str(fh.read(count))
- for line in data.splitlines():
- line = line.strip()
- if line.startswith("["):
- section = {}
- result[line[1:-1]] = section
- continue
- try:
- key, value = line.split("=")
- except ValueError:
- continue
- section[key] = astype(value)
- return result
- def read_cz_sem(fh, byteorder, dtype, count, offsetsize):
- """Read Zeiss SEM tag and return as dict."""
- result = {"": ()}
- key = None
- data = bytes2str(fh.read(count))
- for line in data.splitlines():
- if line.isupper():
- key = line.lower()
- elif key:
- try:
- name, value = line.split("=")
- except ValueError:
- continue
- value = value.strip()
- unit = ""
- try:
- v, u = value.split()
- number = astype(v, (int, float))
- if number != v:
- value = number
- unit = u
- except Exception:
- number = astype(value, (int, float))
- if number != value:
- value = number
- if value in ("No", "Off"):
- value = False
- elif value in ("Yes", "On"):
- value = True
- result[key] = (name.strip(), value)
- if unit:
- result[key] += (unit,)
- key = None
- else:
- result[""] += (astype(line, (int, float)),)
- return result
- def read_nih_image_header(fh, byteorder, dtype, count, offsetsize):
- """Read NIH_IMAGE_HEADER tag from file and return as dict."""
- a = fh.read_record(TIFF.NIH_IMAGE_HEADER, byteorder=byteorder)
- a = a.view(a.dtype.newbyteorder(byteorder))
- a = recarray2dict(a)
- a["XUnit"] = a["XUnit"][: a["XUnitSize"]]
- a["UM"] = a["UM"][: a["UMsize"]]
- return a
- def read_scanimage_metadata(fh):
- """Read ScanImage BigTIFF v3 static and ROI metadata from open file.
- Return non-varying frame data as dict and ROI group data as JSON.
- The settings can be used to read image data and metadata without parsing
- the TIFF file.
- Raise ValueError if file does not contain valid ScanImage v3 metadata.
- """
- fh.seek(0)
- try:
- byteorder, version = struct.unpack("<2sH", fh.read(4))
- if byteorder != b"II" or version != 43:
- raise Exception
- fh.seek(16)
- magic, version, size0, size1 = struct.unpack("<IIII", fh.read(16))
- if magic != 117637889 or version != 3:
- raise Exception
- except Exception:
- raise ValueError("not a ScanImage BigTIFF v3 file")
- frame_data = matlabstr2py(bytes2str(fh.read(size0)[:-1]))
- roi_data = read_json(fh, "<", None, size1, None) if size1 > 1 else {}
- return frame_data, roi_data
- def read_micromanager_metadata(fh):
- """Read MicroManager non-TIFF settings from open file and return as dict.
- The settings can be used to read image data without parsing the TIFF file.
- Raise ValueError if the file does not contain valid MicroManager metadata.
- """
- fh.seek(0)
- try:
- byteorder = {b"II": "<", b"MM": ">"}[fh.read(2)]
- except IndexError:
- raise ValueError("not a MicroManager TIFF file")
- result = {}
- fh.seek(8)
- (
- index_header,
- index_offset,
- display_header,
- display_offset,
- comments_header,
- comments_offset,
- summary_header,
- summary_length,
- ) = struct.unpack(byteorder + "IIIIIIII", fh.read(32))
- if summary_header != 2355492:
- raise ValueError("invalid MicroManager summary header")
- result["Summary"] = read_json(fh, byteorder, None, summary_length, None)
- if index_header != 54773648:
- raise ValueError("invalid MicroManager index header")
- fh.seek(index_offset)
- header, count = struct.unpack(byteorder + "II", fh.read(8))
- if header != 3453623:
- raise ValueError("invalid MicroManager index header")
- data = struct.unpack(byteorder + "IIIII" * count, fh.read(20 * count))
- result["IndexMap"] = {
- "Channel": data[::5],
- "Slice": data[1::5],
- "Frame": data[2::5],
- "Position": data[3::5],
- "Offset": data[4::5],
- }
- if display_header != 483765892:
- raise ValueError("invalid MicroManager display header")
- fh.seek(display_offset)
- header, count = struct.unpack(byteorder + "II", fh.read(8))
- if header != 347834724:
- raise ValueError("invalid MicroManager display header")
- result["DisplaySettings"] = read_json(fh, byteorder, None, count, None)
- if comments_header != 99384722:
- raise ValueError("invalid MicroManager comments header")
- fh.seek(comments_offset)
- header, count = struct.unpack(byteorder + "II", fh.read(8))
- if header != 84720485:
- raise ValueError("invalid MicroManager comments header")
- result["Comments"] = read_json(fh, byteorder, None, count, None)
- return result
- def read_metaseries_catalog(fh):
- """Read MetaSeries non-TIFF hint catalog from file.
- Raise ValueError if the file does not contain a valid hint catalog.
- """
- # TODO: implement read_metaseries_catalog
- raise NotImplementedError()
- def imagej_metadata_tags(metadata, byteorder):
- """Return IJMetadata and IJMetadataByteCounts tags from metadata dict.
- The tags can be passed to the TiffWriter.save function as extratags.
- The metadata dict may contain the following keys and values:
- Info : str
- Human-readable information as string.
- Labels : sequence of str
- Human-readable labels for each channel.
- Ranges : sequence of doubles
- Lower and upper values for each channel.
- LUTs : sequence of (3, 256) uint8 ndarrays
- Color palettes for each channel.
- Plot : bytes
- Undocumented ImageJ internal format.
- ROI: bytes
- Undocumented ImageJ internal region of interest format.
- Overlays : bytes
- Undocumented ImageJ internal format.
- """
- header = [{">": b"IJIJ", "<": b"JIJI"}[byteorder]]
- bytecounts = [0]
- body = []
- def _string(data, byteorder):
- return data.encode("utf-16" + {">": "be", "<": "le"}[byteorder])
- def _doubles(data, byteorder):
- return struct.pack(byteorder + ("d" * len(data)), *data)
- def _ndarray(data, byteorder):
- return data.tobytes()
- def _bytes(data, byteorder):
- return data
- metadata_types = (
- ("Info", b"info", 1, _string),
- ("Labels", b"labl", None, _string),
- ("Ranges", b"rang", 1, _doubles),
- ("LUTs", b"luts", None, _ndarray),
- ("Plot", b"plot", 1, _bytes),
- ("ROI", b"roi ", 1, _bytes),
- ("Overlays", b"over", None, _bytes),
- )
- for key, mtype, count, func in metadata_types:
- if key.lower() in metadata:
- key = key.lower()
- elif key not in metadata:
- continue
- if byteorder == "<":
- mtype = mtype[::-1]
- values = metadata[key]
- if count is None:
- count = len(values)
- else:
- values = [values]
- header.append(mtype + struct.pack(byteorder + "I", count))
- for value in values:
- data = func(value, byteorder)
- body.append(data)
- bytecounts.append(len(data))
- if not body:
- return ()
- body = b"".join(body)
- header = b"".join(header)
- data = header + body
- bytecounts[0] = len(header)
- bytecounts = struct.pack(byteorder + ("I" * len(bytecounts)), *bytecounts)
- return (
- (50839, "B", len(data), data, True),
- (50838, "I", len(bytecounts) // 4, bytecounts, True),
- )
- def imagej_metadata(data, bytecounts, byteorder):
- """Return IJMetadata tag value as dict.
- The 'Info' string can have multiple formats, e.g. OIF or ScanImage,
- that might be parsed into dicts using the matlabstr2py or
- oiffile.SettingsFile functions.
- """
- def _string(data, byteorder):
- return data.decode("utf-16" + {">": "be", "<": "le"}[byteorder])
- def _doubles(data, byteorder):
- return struct.unpack(byteorder + ("d" * (len(data) // 8)), data)
- def _lut(data, byteorder):
- return numpy.frombuffer(data, "uint8").reshape(-1, 256)
- def _bytes(data, byteorder):
- return data
- metadata_types = { # big-endian
- b"info": ("Info", _string),
- b"labl": ("Labels", _string),
- b"rang": ("Ranges", _doubles),
- b"luts": ("LUTs", _lut),
- b"plot": ("Plots", _bytes),
- b"roi ": ("ROI", _bytes),
- b"over": ("Overlays", _bytes),
- }
- metadata_types.update( # little-endian
- dict((k[::-1], v) for k, v in metadata_types.items())
- )
- if not bytecounts:
- raise ValueError("no ImageJ metadata")
- if data[:4] not in (b"IJIJ", b"JIJI"):
- raise ValueError("invalid ImageJ metadata")
- header_size = bytecounts[0]
- if header_size < 12 or header_size > 804:
- raise ValueError("invalid ImageJ metadata header size")
- ntypes = (header_size - 4) // 8
- header = struct.unpack(byteorder + "4sI" * ntypes, data[4 : 4 + ntypes * 8])
- pos = 4 + ntypes * 8
- counter = 0
- result = {}
- for mtype, count in zip(header[::2], header[1::2]):
- values = []
- name, func = metadata_types.get(mtype, (bytes2str(mtype), read_bytes))
- for _ in range(count):
- counter += 1
- pos1 = pos + bytecounts[counter]
- values.append(func(data[pos:pos1], byteorder))
- pos = pos1
- result[name.strip()] = values[0] if count == 1 else values
- return result
- def imagej_description_metadata(description):
- """Return metatata from ImageJ image description as dict.
- Raise ValueError if not a valid ImageJ description.
- >>> description = 'ImageJ=1.11a\\nimages=510\\nhyperstack=true\\n'
- >>> imagej_description_metadata(description) # doctest: +SKIP
- {'ImageJ': '1.11a', 'images': 510, 'hyperstack': True}
- """
- def _bool(val):
- return {"true": True, "false": False}[val.lower()]
- result = {}
- for line in description.splitlines():
- try:
- key, val = line.split("=")
- except Exception:
- continue
- key = key.strip()
- val = val.strip()
- for dtype in (int, float, _bool):
- try:
- val = dtype(val)
- break
- except Exception:
- pass
- result[key] = val
- if "ImageJ" not in result:
- raise ValueError("not a ImageJ image description")
- return result
- def imagej_description(
- shape,
- rgb=None,
- colormaped=False,
- version="1.11a",
- hyperstack=None,
- mode=None,
- loop=None,
- **kwargs,
- ):
- """Return ImageJ image description from data shape.
- ImageJ can handle up to 6 dimensions in order TZCYXS.
- >>> imagej_description((51, 5, 2, 196, 171)) # doctest: +SKIP
- ImageJ=1.11a
- images=510
- channels=2
- slices=5
- frames=51
- hyperstack=true
- mode=grayscale
- loop=false
- """
- if colormaped:
- raise NotImplementedError("ImageJ colormapping not supported")
- shape = imagej_shape(shape, rgb=rgb)
- rgb = shape[-1] in (3, 4)
- result = ["ImageJ=%s" % version]
- append = []
- result.append("images=%i" % product(shape[:-3]))
- if hyperstack is None:
- hyperstack = True
- append.append("hyperstack=true")
- else:
- append.append("hyperstack=%s" % bool(hyperstack))
- if shape[2] > 1:
- result.append("channels=%i" % shape[2])
- if mode is None and not rgb:
- mode = "grayscale"
- if hyperstack and mode:
- append.append("mode=%s" % mode)
- if shape[1] > 1:
- result.append("slices=%i" % shape[1])
- if shape[0] > 1:
- result.append("frames=%i" % shape[0])
- if loop is None:
- append.append("loop=false")
- if loop is not None:
- append.append("loop=%s" % bool(loop))
- for key, value in kwargs.items():
- append.append("%s=%s" % (key.lower(), value))
- return "\n".join(result + append + [""])
- def imagej_shape(shape, rgb=None):
- """Return shape normalized to 6D ImageJ hyperstack TZCYXS.
- Raise ValueError if not a valid ImageJ hyperstack shape.
- >>> imagej_shape((2, 3, 4, 5, 3), False)
- (2, 3, 4, 5, 3, 1)
- """
- shape = tuple(int(i) for i in shape)
- ndim = len(shape)
- if 1 > ndim > 6:
- raise ValueError("invalid ImageJ hyperstack: not 2 to 6 dimensional")
- if rgb is None:
- rgb = shape[-1] in (3, 4) and ndim > 2
- if rgb and shape[-1] not in (3, 4):
- raise ValueError("invalid ImageJ hyperstack: not a RGB image")
- if not rgb and ndim == 6 and shape[-1] != 1:
- raise ValueError("invalid ImageJ hyperstack: not a non-RGB image")
- if rgb or shape[-1] == 1:
- return (1,) * (6 - ndim) + shape
- return (1,) * (5 - ndim) + shape + (1,)
- def json_description(shape, **metadata):
- """Return JSON image description from data shape and other meta data.
- Return UTF-8 encoded JSON.
- >>> json_description((256, 256, 3), axes='YXS') # doctest: +SKIP
- b'{"shape": [256, 256, 3], "axes": "YXS"}'
- """
- metadata.update(shape=shape)
- return json.dumps(metadata) # .encode('utf-8')
- def json_description_metadata(description):
- """Return metatata from JSON formatted image description as dict.
- Raise ValuError if description is of unknown format.
- >>> description = '{"shape": [256, 256, 3], "axes": "YXS"}'
- >>> json_description_metadata(description) # doctest: +SKIP
- {'shape': [256, 256, 3], 'axes': 'YXS'}
- >>> json_description_metadata('shape=(256, 256, 3)')
- {'shape': (256, 256, 3)}
- """
- if description[:6] == "shape=":
- # old style 'shaped' description; not JSON
- shape = tuple(int(i) for i in description[7:-1].split(","))
- return dict(shape=shape)
- if description[:1] == "{" and description[-1:] == "}":
- # JSON description
- return json.loads(description)
- raise ValueError("invalid JSON image description", description)
- def fluoview_description_metadata(description, ignoresections=None):
- """Return metatata from FluoView image description as dict.
- The FluoView image description format is unspecified. Expect failures.
- >>> descr = ('[Intensity Mapping]\\nMap Ch0: Range=00000 to 02047\\n'
- ... '[Intensity Mapping End]')
- >>> fluoview_description_metadata(descr)
- {'Intensity Mapping': {'Map Ch0: Range': '00000 to 02047'}}
- """
- if not description.startswith("["):
- raise ValueError("invalid FluoView image description")
- if ignoresections is None:
- ignoresections = {"Region Info (Fields)", "Protocol Description"}
- result = {}
- sections = [result]
- comment = False
- for line in description.splitlines():
- if not comment:
- line = line.strip()
- if not line:
- continue
- if line[0] == "[":
- if line[-5:] == " End]":
- # close section
- del sections[-1]
- section = sections[-1]
- name = line[1:-5]
- if comment:
- section[name] = "\n".join(section[name])
- if name[:4] == "LUT ":
- a = numpy.array(section[name], dtype="uint8")
- a.shape = -1, 3
- section[name] = a
- continue
- # new section
- comment = False
- name = line[1:-1]
- if name[:4] == "LUT ":
- section = []
- elif name in ignoresections:
- section = []
- comment = True
- else:
- section = {}
- sections.append(section)
- result[name] = section
- continue
- # add entry
- if comment:
- section.append(line)
- continue
- line = line.split("=", 1)
- if len(line) == 1:
- section[line[0].strip()] = None
- continue
- key, value = line
- if key[:4] == "RGB ":
- section.extend(int(rgb) for rgb in value.split())
- else:
- section[key.strip()] = astype(value.strip())
- return result
- def pilatus_description_metadata(description):
- """Return metatata from Pilatus image description as dict.
- Return metadata from Pilatus pixel array detectors by Dectris, created
- by camserver or TVX software.
- >>> pilatus_description_metadata('# Pixel_size 172e-6 m x 172e-6 m')
- {'Pixel_size': (0.000172, 0.000172)}
- """
- result = {}
- if not description.startswith("# "):
- return result
- for c in "#:=,()":
- description = description.replace(c, " ")
- for line in description.split("\n"):
- if line[:2] != " ":
- continue
- line = line.split()
- name = line[0]
- if line[0] not in TIFF.PILATUS_HEADER:
- try:
- result["DateTime"] = datetime.datetime.strptime(
- " ".join(line), "%Y-%m-%dT%H %M %S.%f"
- )
- except Exception:
- result[name] = " ".join(line[1:])
- continue
- indices, dtype = TIFF.PILATUS_HEADER[line[0]]
- if isinstance(indices[0], slice):
- # assumes one slice
- values = line[indices[0]]
- else:
- values = [line[i] for i in indices]
- if dtype is float and values[0] == "not":
- values = ["NaN"]
- values = tuple(dtype(v) for v in values)
- if dtype == str:
- values = " ".join(values)
- elif len(values) == 1:
- values = values[0]
- result[name] = values
- return result
- def svs_description_metadata(description):
- """Return metatata from Aperio image description as dict.
- The Aperio image description format is unspecified. Expect failures.
- >>> svs_description_metadata('Aperio Image Library v1.0')
- {'Aperio Image Library': 'v1.0'}
- """
- if not description.startswith("Aperio Image Library "):
- raise ValueError("invalid Aperio image description")
- result = {}
- lines = description.split("\n")
- key, value = lines[0].strip().rsplit(None, 1) # 'Aperio Image Library'
- result[key.strip()] = value.strip()
- if len(lines) == 1:
- return result
- items = lines[1].split("|")
- result[""] = items[0].strip() # TODO: parse this?
- for item in items[1:]:
- key, value = item.split(" = ")
- result[key.strip()] = astype(value.strip())
- return result
- def stk_description_metadata(description):
- """Return metadata from MetaMorph image description as list of dict.
- The MetaMorph image description format is unspecified. Expect failures.
- """
- description = description.strip()
- if not description:
- return []
- try:
- description = bytes2str(description)
- except UnicodeDecodeError:
- warnings.warn("failed to parse MetaMorph image description")
- return []
- result = []
- for plane in description.split("\x00"):
- d = {}
- for line in plane.split("\r\n"):
- line = line.split(":", 1)
- if len(line) > 1:
- name, value = line
- d[name.strip()] = astype(value.strip())
- else:
- value = line[0].strip()
- if value:
- if "" in d:
- d[""].append(value)
- else:
- d[""] = [value]
- result.append(d)
- return result
- def metaseries_description_metadata(description):
- """Return metatata from MetaSeries image description as dict."""
- if not description.startswith("<MetaData>"):
- raise ValueError("invalid MetaSeries image description")
- from xml.etree import cElementTree as etree # delayed import
- root = etree.fromstring(description)
- types = {"float": float, "int": int, "bool": lambda x: asbool(x, "on", "off")}
- def parse(root, result):
- # recursive
- for child in root:
- attrib = child.attrib
- if not attrib:
- result[child.tag] = parse(child, {})
- continue
- if "id" in attrib:
- i = attrib["id"]
- t = attrib["type"]
- v = attrib["value"]
- if t in types:
- result[i] = types[t](v)
- else:
- result[i] = v
- return result
- adict = parse(root, {})
- if "Description" in adict:
- adict["Description"] = adict["Description"].replace(" ", "\n")
- return adict
- def scanimage_description_metadata(description):
- """Return metatata from ScanImage image description as dict."""
- return matlabstr2py(description)
- def scanimage_artist_metadata(artist):
- """Return metatata from ScanImage artist tag as dict."""
- try:
- return json.loads(artist)
- except ValueError:
- warnings.warn("invalid JSON '%s'" % artist)
- def _replace_by(module_function, package=__package__, warn=None, prefix="_"):
- """Try replace decorated function by module.function."""
- return lambda f: f # imageio: just use what's in here
- def _warn(e, warn):
- if warn is None:
- warn = "\n Functionality might be degraded or be slow.\n"
- elif warn is True:
- warn = ""
- elif not warn:
- return
- warnings.warn("%s%s" % (e, warn))
- try:
- from importlib import import_module
- except ImportError as e:
- _warn(e, warn)
- return identityfunc
- def decorate(func, module_function=module_function, warn=warn):
- module, function = module_function.split(".")
- try:
- if package:
- module = import_module("." + module, package=package)
- else:
- module = import_module(module)
- except Exception as e:
- _warn(e, warn)
- return func
- try:
- func, oldfunc = getattr(module, function), func
- except Exception as e:
- _warn(e, warn)
- return func
- globals()[prefix + func.__name__] = oldfunc
- return func
- return decorate
- def decode_floats(data):
- """Decode floating point horizontal differencing.
- The TIFF predictor type 3 reorders the bytes of the image values and
- applies horizontal byte differencing to improve compression of floating
- point images. The ordering of interleaved color channels is preserved.
- Parameters
- ----------
- data : numpy.ndarray
- The image to be decoded. The dtype must be a floating point.
- The shape must include the number of contiguous samples per pixel
- even if 1.
- """
- shape = data.shape
- dtype = data.dtype
- if len(shape) < 3:
- raise ValueError("invalid data shape")
- if dtype.char not in "dfe":
- raise ValueError("not a floating point image")
- littleendian = data.dtype.byteorder == "<" or (
- sys.byteorder == "little" and data.dtype.byteorder == "="
- )
- # undo horizontal byte differencing
- data = data.view("uint8")
- data.shape = shape[:-2] + (-1,) + shape[-1:]
- numpy.cumsum(data, axis=-2, dtype="uint8", out=data)
- # reorder bytes
- if littleendian:
- data.shape = shape[:-2] + (-1,) + shape[-2:]
- data = numpy.swapaxes(data, -3, -2)
- data = numpy.swapaxes(data, -2, -1)
- data = data[..., ::-1]
- # back to float
- data = numpy.ascontiguousarray(data)
- data = data.view(dtype)
- data.shape = shape
- return data
- @_replace_by("_tifffile.decode_packbits")
- def decode_packbits(encoded):
- """Decompress PackBits encoded byte string.
- PackBits is a simple byte-oriented run-length compression scheme.
- """
- func = ord if sys.version[0] == "2" else identityfunc
- result = []
- result_extend = result.extend
- i = 0
- try:
- while True:
- n = func(encoded[i]) + 1
- i += 1
- if n < 129:
- result_extend(encoded[i : i + n])
- i += n
- elif n > 129:
- result_extend(encoded[i : i + 1] * (258 - n))
- i += 1
- except IndexError:
- pass
- return b"".join(result) if sys.version[0] == "2" else bytes(result)
- @_replace_by("_tifffile.decode_lzw")
- def decode_lzw(encoded):
- """Decompress LZW (Lempel-Ziv-Welch) encoded TIFF strip (byte string).
- The strip must begin with a CLEAR code and end with an EOI code.
- This implementation of the LZW decoding algorithm is described in (1) and
- is not compatible with old style LZW compressed files like quad-lzw.tif.
- """
- len_encoded = len(encoded)
- bitcount_max = len_encoded * 8
- unpack = struct.unpack
- if sys.version[0] == "2":
- newtable = [chr(i) for i in range(256)]
- else:
- newtable = [bytes([i]) for i in range(256)]
- newtable.extend((0, 0))
- def next_code():
- """Return integer of 'bitw' bits at 'bitcount' position in encoded."""
- start = bitcount // 8
- s = encoded[start : start + 4]
- try:
- code = unpack(">I", s)[0]
- except Exception:
- code = unpack(">I", s + b"\x00" * (4 - len(s)))[0]
- code <<= bitcount % 8
- code &= mask
- return code >> shr
- switchbitch = { # code: bit-width, shr-bits, bit-mask
- 255: (9, 23, int(9 * "1" + "0" * 23, 2)),
- 511: (10, 22, int(10 * "1" + "0" * 22, 2)),
- 1023: (11, 21, int(11 * "1" + "0" * 21, 2)),
- 2047: (12, 20, int(12 * "1" + "0" * 20, 2)),
- }
- bitw, shr, mask = switchbitch[255]
- bitcount = 0
- if len_encoded < 4:
- raise ValueError("strip must be at least 4 characters long")
- if next_code() != 256:
- raise ValueError("strip must begin with CLEAR code")
- code = 0
- oldcode = 0
- result = []
- result_append = result.append
- while True:
- code = next_code() # ~5% faster when inlining this function
- bitcount += bitw
- if code == 257 or bitcount >= bitcount_max: # EOI
- break
- if code == 256: # CLEAR
- table = newtable[:]
- table_append = table.append
- lentable = 258
- bitw, shr, mask = switchbitch[255]
- code = next_code()
- bitcount += bitw
- if code == 257: # EOI
- break
- result_append(table[code])
- else:
- if code < lentable:
- decoded = table[code]
- newcode = table[oldcode] + decoded[:1]
- else:
- newcode = table[oldcode]
- newcode += newcode[:1]
- decoded = newcode
- result_append(decoded)
- table_append(newcode)
- lentable += 1
- oldcode = code
- if lentable in switchbitch:
- bitw, shr, mask = switchbitch[lentable]
- if code != 257:
- warnings.warn("unexpected end of LZW stream (code %i)" % code)
- return b"".join(result)
- @_replace_by("_tifffile.unpack_ints")
- def unpack_ints(data, dtype, itemsize, runlen=0):
- """Decompress byte string to array of integers of any bit size <= 32.
- This Python implementation is slow and only handles itemsizes 1, 2, 4, 8,
- 16, 32, and 64.
- Parameters
- ----------
- data : byte str
- Data to decompress.
- dtype : numpy.dtype or str
- A numpy boolean or integer type.
- itemsize : int
- Number of bits per integer.
- runlen : int
- Number of consecutive integers, after which to start at next byte.
- Examples
- --------
- >>> unpack_ints(b'a', 'B', 1)
- array([0, 1, 1, 0, 0, 0, 0, 1], dtype=uint8)
- >>> unpack_ints(b'ab', 'B', 2)
- array([1, 2, 0, 1, 1, 2, 0, 2], dtype=uint8)
- """
- if itemsize == 1: # bitarray
- data = numpy.frombuffer(data, "|B")
- data = numpy.unpackbits(data)
- if runlen % 8:
- data = data.reshape(-1, runlen + (8 - runlen % 8))
- data = data[:, :runlen].reshape(-1)
- return data.astype(dtype)
- dtype = numpy.dtype(dtype)
- if itemsize in (8, 16, 32, 64):
- return numpy.frombuffer(data, dtype)
- if itemsize not in (1, 2, 4, 8, 16, 32):
- raise ValueError("itemsize not supported: %i" % itemsize)
- if dtype.kind not in "biu":
- raise ValueError("invalid dtype")
- itembytes = next(i for i in (1, 2, 4, 8) if 8 * i >= itemsize)
- if itembytes != dtype.itemsize:
- raise ValueError("dtype.itemsize too small")
- if runlen == 0:
- runlen = (8 * len(data)) // itemsize
- skipbits = runlen * itemsize % 8
- if skipbits:
- skipbits = 8 - skipbits
- shrbits = itembytes * 8 - itemsize
- bitmask = int(itemsize * "1" + "0" * shrbits, 2)
- dtypestr = ">" + dtype.char # dtype always big-endian?
- unpack = struct.unpack
- size = runlen * (len(data) * 8 // (runlen * itemsize + skipbits))
- result = numpy.empty((size,), dtype)
- bitcount = 0
- for i in range(size):
- start = bitcount // 8
- s = data[start : start + itembytes]
- try:
- code = unpack(dtypestr, s)[0]
- except Exception:
- code = unpack(dtypestr, s + b"\x00" * (itembytes - len(s)))[0]
- code <<= bitcount % 8
- code &= bitmask
- result[i] = code >> shrbits
- bitcount += itemsize
- if (i + 1) % runlen == 0:
- bitcount += skipbits
- return result
- def unpack_rgb(data, dtype="<B", bitspersample=(5, 6, 5), rescale=True):
- """Return array from byte string containing packed samples.
- Use to unpack RGB565 or RGB555 to RGB888 format.
- Parameters
- ----------
- data : byte str
- The data to be decoded. Samples in each pixel are stored consecutively.
- Pixels are aligned to 8, 16, or 32 bit boundaries.
- dtype : numpy.dtype
- The sample data type. The byteorder applies also to the data stream.
- bitspersample : tuple
- Number of bits for each sample in a pixel.
- rescale : bool
- Upscale samples to the number of bits in dtype.
- Returns
- -------
- result : ndarray
- Flattened array of unpacked samples of native dtype.
- Examples
- --------
- >>> data = struct.pack('BBBB', 0x21, 0x08, 0xff, 0xff)
- >>> print(unpack_rgb(data, '<B', (5, 6, 5), False))
- [ 1 1 1 31 63 31]
- >>> print(unpack_rgb(data, '<B', (5, 6, 5)))
- [ 8 4 8 255 255 255]
- >>> print(unpack_rgb(data, '<B', (5, 5, 5)))
- [ 16 8 8 255 255 255]
- """
- dtype = numpy.dtype(dtype)
- bits = int(numpy.sum(bitspersample))
- if not (bits <= 32 and all(i <= dtype.itemsize * 8 for i in bitspersample)):
- raise ValueError("sample size not supported: %s" % str(bitspersample))
- dt = next(i for i in "BHI" if numpy.dtype(i).itemsize * 8 >= bits)
- data = numpy.frombuffer(data, dtype.byteorder + dt)
- result = numpy.empty((data.size, len(bitspersample)), dtype.char)
- for i, bps in enumerate(bitspersample):
- t = data >> int(numpy.sum(bitspersample[i + 1 :]))
- t &= int("0b" + "1" * bps, 2)
- if rescale:
- o = ((dtype.itemsize * 8) // bps + 1) * bps
- if o > data.dtype.itemsize * 8:
- t = t.astype("I")
- t *= (2**o - 1) // (2**bps - 1)
- t //= 2 ** (o - (dtype.itemsize * 8))
- result[:, i] = t
- return result.reshape(-1)
- @_replace_by("_tifffile.reverse_bitorder")
- def reverse_bitorder(data):
- """Reverse bits in each byte of byte string or numpy array.
- Decode data where pixels with lower column values are stored in the
- lower-order bits of the bytes (FillOrder is LSB2MSB).
- Parameters
- ----------
- data : byte string or ndarray
- The data to be bit reversed. If byte string, a new bit-reversed byte
- string is returned. Numpy arrays are bit-reversed in-place.
- Examples
- --------
- >>> reverse_bitorder(b'\\x01\\x64')
- b'\\x80&'
- >>> data = numpy.array([1, 666], dtype='uint16')
- >>> reverse_bitorder(data)
- >>> data
- array([ 128, 16473], dtype=uint16)
- """
- try:
- view = data.view("uint8")
- numpy.take(TIFF.REVERSE_BITORDER_ARRAY, view, out=view)
- except AttributeError:
- return data.translate(TIFF.REVERSE_BITORDER_BYTES)
- except ValueError:
- raise NotImplementedError("slices of arrays not supported")
- def apply_colormap(image, colormap, contig=True):
- """Return palette-colored image.
- The image values are used to index the colormap on axis 1. The returned
- image is of shape image.shape+colormap.shape[0] and dtype colormap.dtype.
- Parameters
- ----------
- image : numpy.ndarray
- Indexes into the colormap.
- colormap : numpy.ndarray
- RGB lookup table aka palette of shape (3, 2**bits_per_sample).
- contig : bool
- If True, return a contiguous array.
- Examples
- --------
- >>> image = numpy.arange(256, dtype='uint8')
- >>> colormap = numpy.vstack([image, image, image]).astype('uint16') * 256
- >>> apply_colormap(image, colormap)[-1]
- array([65280, 65280, 65280], dtype=uint16)
- """
- image = numpy.take(colormap, image, axis=1)
- image = numpy.rollaxis(image, 0, image.ndim)
- if contig:
- image = numpy.ascontiguousarray(image)
- return image
- def reorient(image, orientation):
- """Return reoriented view of image array.
- Parameters
- ----------
- image : numpy.ndarray
- Non-squeezed output of asarray() functions.
- Axes -3 and -2 must be image length and width respectively.
- orientation : int or str
- One of TIFF.ORIENTATION names or values.
- """
- ORIENTATION = TIFF.ORIENTATION
- orientation = enumarg(ORIENTATION, orientation)
- if orientation == ORIENTATION.TOPLEFT:
- return image
- elif orientation == ORIENTATION.TOPRIGHT:
- return image[..., ::-1, :]
- elif orientation == ORIENTATION.BOTLEFT:
- return image[..., ::-1, :, :]
- elif orientation == ORIENTATION.BOTRIGHT:
- return image[..., ::-1, ::-1, :]
- elif orientation == ORIENTATION.LEFTTOP:
- return numpy.swapaxes(image, -3, -2)
- elif orientation == ORIENTATION.RIGHTTOP:
- return numpy.swapaxes(image, -3, -2)[..., ::-1, :]
- elif orientation == ORIENTATION.RIGHTBOT:
- return numpy.swapaxes(image, -3, -2)[..., ::-1, :, :]
- elif orientation == ORIENTATION.LEFTBOT:
- return numpy.swapaxes(image, -3, -2)[..., ::-1, ::-1, :]
- def repeat_nd(a, repeats):
- """Return read-only view into input array with elements repeated.
- Zoom nD image by integer factors using nearest neighbor interpolation
- (box filter).
- Parameters
- ----------
- a : array_like
- Input array.
- repeats : sequence of int
- The number of repetitions to apply along each dimension of input array.
- Example
- -------
- >>> repeat_nd([[1, 2], [3, 4]], (2, 2))
- array([[1, 1, 2, 2],
- [1, 1, 2, 2],
- [3, 3, 4, 4],
- [3, 3, 4, 4]])
- """
- a = numpy.asarray(a)
- reshape = []
- shape = []
- strides = []
- for i, j, k in zip(a.strides, a.shape, repeats):
- shape.extend((j, k))
- strides.extend((i, 0))
- reshape.append(j * k)
- return numpy.lib.stride_tricks.as_strided(
- a, shape, strides, writeable=False
- ).reshape(reshape)
- def reshape_nd(data_or_shape, ndim):
- """Return image array or shape with at least ndim dimensions.
- Prepend 1s to image shape as necessary.
- >>> reshape_nd(numpy.empty(0), 1).shape
- (0,)
- >>> reshape_nd(numpy.empty(1), 2).shape
- (1, 1)
- >>> reshape_nd(numpy.empty((2, 3)), 3).shape
- (1, 2, 3)
- >>> reshape_nd(numpy.empty((3, 4, 5)), 3).shape
- (3, 4, 5)
- >>> reshape_nd((2, 3), 3)
- (1, 2, 3)
- """
- is_shape = isinstance(data_or_shape, tuple)
- shape = data_or_shape if is_shape else data_or_shape.shape
- if len(shape) >= ndim:
- return data_or_shape
- shape = (1,) * (ndim - len(shape)) + shape
- return shape if is_shape else data_or_shape.reshape(shape)
- def squeeze_axes(shape, axes, skip="XY"):
- """Return shape and axes with single-dimensional entries removed.
- Remove unused dimensions unless their axes are listed in 'skip'.
- >>> squeeze_axes((5, 1, 2, 1, 1), 'TZYXC')
- ((5, 2, 1), 'TYX')
- """
- if len(shape) != len(axes):
- raise ValueError("dimensions of axes and shape do not match")
- shape, axes = zip(*(i for i in zip(shape, axes) if i[0] > 1 or i[1] in skip))
- return tuple(shape), "".join(axes)
- def transpose_axes(image, axes, asaxes="CTZYX"):
- """Return image with its axes permuted to match specified axes.
- A view is returned if possible.
- >>> transpose_axes(numpy.zeros((2, 3, 4, 5)), 'TYXC', asaxes='CTZYX').shape
- (5, 2, 1, 3, 4)
- """
- for ax in axes:
- if ax not in asaxes:
- raise ValueError("unknown axis %s" % ax)
- # add missing axes to image
- shape = image.shape
- for ax in reversed(asaxes):
- if ax not in axes:
- axes = ax + axes
- shape = (1,) + shape
- image = image.reshape(shape)
- # transpose axes
- image = image.transpose([axes.index(ax) for ax in asaxes])
- return image
- def reshape_axes(axes, shape, newshape, unknown="Q"):
- """Return axes matching new shape.
- Unknown dimensions are labelled 'Q'.
- >>> reshape_axes('YXS', (219, 301, 1), (219, 301))
- 'YX'
- >>> reshape_axes('IYX', (12, 219, 301), (3, 4, 219, 1, 301, 1))
- 'QQYQXQ'
- """
- shape = tuple(shape)
- newshape = tuple(newshape)
- if len(axes) != len(shape):
- raise ValueError("axes do not match shape")
- size = product(shape)
- newsize = product(newshape)
- if size != newsize:
- raise ValueError("cannot reshape %s to %s" % (shape, newshape))
- if not axes or not newshape:
- return ""
- lendiff = max(0, len(shape) - len(newshape))
- if lendiff:
- newshape = newshape + (1,) * lendiff
- i = len(shape) - 1
- prodns = 1
- prods = 1
- result = []
- for ns in newshape[::-1]:
- prodns *= ns
- while i > 0 and shape[i] == 1 and ns != 1:
- i -= 1
- if ns == shape[i] and prodns == prods * shape[i]:
- prods *= shape[i]
- result.append(axes[i])
- i -= 1
- else:
- result.append(unknown)
- return "".join(reversed(result[lendiff:]))
- def stack_pages(pages, out=None, maxworkers=1, *args, **kwargs):
- """Read data from sequence of TiffPage and stack them vertically.
- Additional parameters are passed to the TiffPage.asarray function.
- """
- npages = len(pages)
- if npages == 0:
- raise ValueError("no pages")
- if npages == 1:
- return pages[0].asarray(out=out, *args, **kwargs)
- page0 = next(p for p in pages if p is not None)
- page0.asarray(validate=None) # ThreadPoolExecutor swallows exceptions
- shape = (npages,) + page0.keyframe.shape
- dtype = page0.keyframe.dtype
- out = create_output(out, shape, dtype)
- if maxworkers is None:
- maxworkers = multiprocessing.cpu_count() // 2
- page0.parent.filehandle.lock = maxworkers > 1
- filecache = OpenFileCache(
- size=max(4, maxworkers), lock=page0.parent.filehandle.lock
- )
- def func(page, index, out=out, filecache=filecache, args=args, kwargs=kwargs):
- """Read, decode, and copy page data."""
- if page is not None:
- filecache.open(page.parent.filehandle)
- out[index] = page.asarray(
- lock=filecache.lock, reopen=False, validate=False, *args, **kwargs
- )
- filecache.close(page.parent.filehandle)
- if maxworkers < 2:
- for i, page in enumerate(pages):
- func(page, i)
- else:
- with concurrent.futures.ThreadPoolExecutor(maxworkers) as executor:
- executor.map(func, pages, range(npages))
- filecache.clear()
- page0.parent.filehandle.lock = None
- return out
- def clean_offsets_counts(offsets, counts):
- """Return cleaned offsets and byte counts.
- Remove zero offsets and counts. Use to sanitize _offsets and _bytecounts
- tag values for strips or tiles.
- """
- offsets = list(offsets)
- counts = list(counts)
- assert len(offsets) == len(counts)
- j = 0
- for i, (o, b) in enumerate(zip(offsets, counts)):
- if o > 0 and b > 0:
- if i > j:
- offsets[j] = o
- counts[j] = b
- j += 1
- elif b > 0 and o <= 0:
- raise ValueError("invalid offset")
- else:
- warnings.warn("empty byte count")
- if j == 0:
- j = 1
- return offsets[:j], counts[:j]
- def buffered_read(fh, lock, offsets, bytecounts, buffersize=2**26):
- """Return iterator over blocks read from file."""
- length = len(offsets)
- i = 0
- while i < length:
- data = []
- with lock:
- size = 0
- while size < buffersize and i < length:
- fh.seek(offsets[i])
- bytecount = bytecounts[i]
- data.append(fh.read(bytecount))
- size += bytecount
- i += 1
- for block in data:
- yield block
- def create_output(out, shape, dtype, mode="w+", suffix=".memmap"):
- """Return numpy array where image data of shape and dtype can be copied.
- The 'out' parameter may have the following values or types:
- None
- An empty array of shape and dtype is created and returned.
- numpy.ndarray
- An existing writable array of compatible dtype and shape. A view of
- the same array is returned after verification.
- 'memmap' or 'memmap:tempdir'
- A memory-map to an array stored in a temporary binary file on disk
- is created and returned.
- str or open file
- The file name or file object used to create a memory-map to an array
- stored in a binary file on disk. The created memory-mapped array is
- returned.
- """
- if out is None:
- return numpy.zeros(shape, dtype)
- if isinstance(out, str) and out[:6] == "memmap":
- tempdir = out[7:] if len(out) > 7 else None
- with tempfile.NamedTemporaryFile(dir=tempdir, suffix=suffix) as fh:
- return numpy.memmap(fh, shape=shape, dtype=dtype, mode=mode)
- if isinstance(out, numpy.ndarray):
- if product(shape) != product(out.shape):
- raise ValueError("incompatible output shape")
- if not numpy.can_cast(dtype, out.dtype):
- raise ValueError("incompatible output dtype")
- return out.reshape(shape)
- if isinstance(out, pathlib.Path):
- out = str(out)
- return numpy.memmap(out, shape=shape, dtype=dtype, mode=mode)
- def matlabstr2py(string):
- """Return Python object from Matlab string representation.
- Return str, bool, int, float, list (Matlab arrays or cells), or
- dict (Matlab structures) types.
- Use to access ScanImage metadata.
- >>> matlabstr2py('1')
- 1
- >>> matlabstr2py("['x y z' true false; 1 2.0 -3e4; NaN Inf @class]")
- [['x y z', True, False], [1, 2.0, -30000.0], [nan, inf, '@class']]
- >>> d = matlabstr2py("SI.hChannels.channelType = {'stripe' 'stripe'}\\n"
- ... "SI.hChannels.channelsActive = 2")
- >>> d['SI.hChannels.channelType']
- ['stripe', 'stripe']
- """
- # TODO: handle invalid input
- # TODO: review unboxing of multidimensional arrays
- def lex(s):
- # return sequence of tokens from matlab string representation
- tokens = ["["]
- while True:
- t, i = next_token(s)
- if t is None:
- break
- if t == ";":
- tokens.extend(("]", "["))
- elif t == "[":
- tokens.extend(("[", "["))
- elif t == "]":
- tokens.extend(("]", "]"))
- else:
- tokens.append(t)
- s = s[i:]
- tokens.append("]")
- return tokens
- def next_token(s):
- # return next token in matlab string
- length = len(s)
- if length == 0:
- return None, 0
- i = 0
- while i < length and s[i] == " ":
- i += 1
- if i == length:
- return None, i
- if s[i] in "{[;]}":
- return s[i], i + 1
- if s[i] == "'":
- j = i + 1
- while j < length and s[j] != "'":
- j += 1
- return s[i : j + 1], j + 1
- if s[i] == "<":
- j = i + 1
- while j < length and s[j] != ">":
- j += 1
- return s[i : j + 1], j + 1
- j = i
- while j < length and s[j] not in " {[;]}":
- j += 1
- return s[i:j], j
- def value(s, fail=False):
- # return Python value of token
- s = s.strip()
- if not s:
- return s
- if len(s) == 1:
- try:
- return int(s)
- except Exception:
- if fail:
- raise ValueError()
- return s
- if s[0] == "'":
- if fail and s[-1] != "'" or "'" in s[1:-1]:
- raise ValueError()
- return s[1:-1]
- if s[0] == "<":
- if fail and s[-1] != ">" or "<" in s[1:-1]:
- raise ValueError()
- return s
- if fail and any(i in s for i in " ';[]{}"):
- raise ValueError()
- if s[0] == "@":
- return s
- if s in ("true", "True"):
- return True
- if s in ("false", "False"):
- return False
- if s[:6] == "zeros(":
- return numpy.zeros([int(i) for i in s[6:-1].split(",")]).tolist()
- if s[:5] == "ones(":
- return numpy.ones([int(i) for i in s[5:-1].split(",")]).tolist()
- if "." in s or "e" in s:
- try:
- return float(s)
- except Exception:
- pass
- try:
- return int(s)
- except Exception:
- pass
- try:
- return float(s) # nan, inf
- except Exception:
- if fail:
- raise ValueError()
- return s
- def parse(s):
- # return Python value from string representation of Matlab value
- s = s.strip()
- try:
- return value(s, fail=True)
- except ValueError:
- pass
- result = add2 = []
- levels = [add2]
- for t in lex(s):
- if t in "[{":
- add2 = []
- levels.append(add2)
- elif t in "]}":
- x = levels.pop()
- if len(x) == 1 and isinstance(x[0], (list, str)):
- x = x[0]
- add2 = levels[-1]
- add2.append(x)
- else:
- add2.append(value(t))
- if len(result) == 1 and isinstance(result[0], (list, str)):
- result = result[0]
- return result
- if "\r" in string or "\n" in string:
- # structure
- d = {}
- for line in string.splitlines():
- line = line.strip()
- if not line or line[0] == "%":
- continue
- k, v = line.split("=", 1)
- k = k.strip()
- if any(c in k for c in " ';[]{}<>"):
- continue
- d[k] = parse(v)
- return d
- return parse(string)
- def stripnull(string, null=b"\x00"):
- """Return string truncated at first null character.
- Clean NULL terminated C strings. For unicode strings use null='\\0'.
- >>> stripnull(b'string\\x00')
- b'string'
- >>> stripnull('string\\x00', null='\\0')
- 'string'
- """
- i = string.find(null)
- return string if (i < 0) else string[:i]
- def stripascii(string):
- """Return string truncated at last byte that is 7-bit ASCII.
- Clean NULL separated and terminated TIFF strings.
- >>> stripascii(b'string\\x00string\\n\\x01\\x00')
- b'string\\x00string\\n'
- >>> stripascii(b'\\x00')
- b''
- """
- # TODO: pythonize this
- i = len(string)
- while i:
- i -= 1
- if 8 < byte2int(string[i]) < 127:
- break
- else:
- i = -1
- return string[: i + 1]
- def asbool(value, true=(b"true", "true"), false=(b"false", "false")):
- """Return string as bool if possible, else raise TypeError.
- >>> asbool(b' False ')
- False
- """
- value = value.strip().lower()
- if value in true: # might raise UnicodeWarning/BytesWarning
- return True
- if value in false:
- return False
- raise TypeError()
- def astype(value, types=None):
- """Return argument as one of types if possible.
- >>> astype('42')
- 42
- >>> astype('3.14')
- 3.14
- >>> astype('True')
- True
- >>> astype(b'Neee-Wom')
- 'Neee-Wom'
- """
- if types is None:
- types = int, float, asbool, bytes2str
- for typ in types:
- try:
- return typ(value)
- except (ValueError, AttributeError, TypeError, UnicodeEncodeError):
- pass
- return value
- def format_size(size, threshold=1536):
- """Return file size as string from byte size.
- >>> format_size(1234)
- '1234 B'
- >>> format_size(12345678901)
- '11.50 GiB'
- """
- if size < threshold:
- return "%i B" % size
- for unit in ("KiB", "MiB", "GiB", "TiB", "PiB"):
- size /= 1024.0
- if size < threshold:
- return "%.2f %s" % (size, unit)
- def identityfunc(arg):
- """Single argument identity function.
- >>> identityfunc('arg')
- 'arg'
- """
- return arg
- def nullfunc(*args, **kwargs):
- """Null function.
- >>> nullfunc('arg', kwarg='kwarg')
- """
- return
- def sequence(value):
- """Return tuple containing value if value is not a sequence.
- >>> sequence(1)
- (1,)
- >>> sequence([1])
- [1]
- """
- try:
- len(value)
- return value
- except TypeError:
- return (value,)
- def product(iterable):
- """Return product of sequence of numbers.
- Equivalent of functools.reduce(operator.mul, iterable, 1).
- Multiplying numpy integers might overflow.
- >>> product([2**8, 2**30])
- 274877906944
- >>> product([])
- 1
- """
- prod = 1
- for i in iterable:
- prod *= i
- return prod
- def natural_sorted(iterable):
- """Return human sorted list of strings.
- E.g. for sorting file names.
- >>> natural_sorted(['f1', 'f2', 'f10'])
- ['f1', 'f2', 'f10']
- """
- def sortkey(x):
- return [(int(c) if c.isdigit() else c) for c in re.split(numbers, x)]
- numbers = re.compile(r"(\d+)")
- return sorted(iterable, key=sortkey)
- def excel_datetime(timestamp, epoch=datetime.datetime.fromordinal(693594)):
- """Return datetime object from timestamp in Excel serial format.
- Convert LSM time stamps.
- >>> excel_datetime(40237.029999999795)
- datetime.datetime(2010, 2, 28, 0, 43, 11, 999982)
- """
- return epoch + datetime.timedelta(timestamp)
- def julian_datetime(julianday, milisecond=0):
- """Return datetime from days since 1/1/4713 BC and ms since midnight.
- Convert Julian dates according to MetaMorph.
- >>> julian_datetime(2451576, 54362783)
- datetime.datetime(2000, 2, 2, 15, 6, 2, 783)
- """
- if julianday <= 1721423:
- # no datetime before year 1
- return None
- a = julianday + 1
- if a > 2299160:
- alpha = math.trunc((a - 1867216.25) / 36524.25)
- a += 1 + alpha - alpha // 4
- b = a + (1524 if a > 1721423 else 1158)
- c = math.trunc((b - 122.1) / 365.25)
- d = math.trunc(365.25 * c)
- e = math.trunc((b - d) / 30.6001)
- day = b - d - math.trunc(30.6001 * e)
- month = e - (1 if e < 13.5 else 13)
- year = c - (4716 if month > 2.5 else 4715)
- hour, milisecond = divmod(milisecond, 1000 * 60 * 60)
- minute, milisecond = divmod(milisecond, 1000 * 60)
- second, milisecond = divmod(milisecond, 1000)
- return datetime.datetime(year, month, day, hour, minute, second, milisecond)
- def byteorder_isnative(byteorder):
- """Return if byteorder matches the system's byteorder.
- >>> byteorder_isnative('=')
- True
- """
- if byteorder == "=" or byteorder == sys.byteorder:
- return True
- keys = {"big": ">", "little": "<"}
- return keys.get(byteorder, byteorder) == keys[sys.byteorder]
- def recarray2dict(recarray):
- """Return numpy.recarray as dict."""
- # TODO: subarrays
- result = {}
- for descr, value in zip(recarray.dtype.descr, recarray):
- name, dtype = descr[:2]
- if dtype[1] == "S":
- value = bytes2str(stripnull(value))
- elif value.ndim < 2:
- value = value.tolist()
- result[name] = value
- return result
- def xml2dict(xml, sanitize=True, prefix=None):
- """Return XML as dict.
- >>> xml2dict('<?xml version="1.0" ?><root attr="name"><key>1</key></root>')
- {'root': {'key': 1, 'attr': 'name'}}
- """
- from xml.etree import cElementTree as etree # delayed import
- at = tx = ""
- if prefix:
- at, tx = prefix
- def astype(value):
- # return value as int, float, bool, or str
- for t in (int, float, asbool):
- try:
- return t(value)
- except Exception:
- pass
- return value
- def etree2dict(t):
- # adapted from https://stackoverflow.com/a/10077069/453463
- key = t.tag
- if sanitize:
- key = key.rsplit("}", 1)[-1]
- d = {key: {} if t.attrib else None}
- children = list(t)
- if children:
- dd = collections.defaultdict(list)
- for dc in map(etree2dict, children):
- for k, v in dc.items():
- dd[k].append(astype(v))
- d = {
- key: {
- k: astype(v[0]) if len(v) == 1 else astype(v) for k, v in dd.items()
- }
- }
- if t.attrib:
- d[key].update((at + k, astype(v)) for k, v in t.attrib.items())
- if t.text:
- text = t.text.strip()
- if children or t.attrib:
- if text:
- d[key][tx + "value"] = astype(text)
- else:
- d[key] = astype(text)
- return d
- return etree2dict(etree.fromstring(xml))
- def hexdump(bytestr, width=75, height=24, snipat=-2, modulo=2, ellipsis="..."):
- """Return hexdump representation of byte string.
- >>> hexdump(binascii.unhexlify('49492a00080000000e00fe0004000100'))
- '49 49 2a 00 08 00 00 00 0e 00 fe 00 04 00 01 00 II*.............'
- """
- size = len(bytestr)
- if size < 1 or width < 2 or height < 1:
- return ""
- if height == 1:
- addr = b""
- bytesperline = min(modulo * (((width - len(addr)) // 4) // modulo), size)
- if bytesperline < 1:
- return ""
- nlines = 1
- else:
- addr = b"%%0%ix: " % len(b"%x" % size)
- bytesperline = min(modulo * (((width - len(addr % 1)) // 4) // modulo), size)
- if bytesperline < 1:
- return ""
- width = 3 * bytesperline + len(addr % 1)
- nlines = (size - 1) // bytesperline + 1
- if snipat is None or snipat == 1:
- snipat = height
- elif 0 < abs(snipat) < 1:
- snipat = int(math.floor(height * snipat))
- if snipat < 0:
- snipat += height
- if height == 1 or nlines == 1:
- blocks = [(0, bytestr[:bytesperline])]
- addr = b""
- height = 1
- width = 3 * bytesperline
- elif height is None or nlines <= height:
- blocks = [(0, bytestr)]
- elif snipat <= 0:
- start = bytesperline * (nlines - height)
- blocks = [(start, bytestr[start:])] # (start, None)
- elif snipat >= height or height < 3:
- end = bytesperline * height
- blocks = [(0, bytestr[:end])] # (end, None)
- else:
- end1 = bytesperline * snipat
- end2 = bytesperline * (height - snipat - 1)
- blocks = [
- (0, bytestr[:end1]),
- (size - end1 - end2, None),
- (size - end2, bytestr[size - end2 :]),
- ]
- ellipsis = str2bytes(ellipsis)
- result = []
- for start, bytestr in blocks:
- if bytestr is None:
- result.append(ellipsis) # 'skip %i bytes' % start)
- continue
- hexstr = binascii.hexlify(bytestr)
- strstr = re.sub(rb"[^\x20-\x7f]", b".", bytestr)
- for i in range(0, len(bytestr), bytesperline):
- h = hexstr[2 * i : 2 * i + bytesperline * 2]
- r = (addr % (i + start)) if height > 1 else addr
- r += b" ".join(h[i : i + 2] for i in range(0, 2 * bytesperline, 2))
- r += b" " * (width - len(r))
- r += strstr[i : i + bytesperline]
- result.append(r)
- result = b"\n".join(result)
- if sys.version_info[0] == 3:
- result = result.decode("ascii")
- return result
- def isprintable(string):
- """Return if all characters in string are printable.
- >>> isprintable('abc')
- True
- >>> isprintable(b'\01')
- False
- """
- string = string.strip()
- if len(string) < 1:
- return True
- if sys.version_info[0] == 3:
- try:
- return string.isprintable()
- except Exception:
- pass
- try:
- return string.decode("utf-8").isprintable()
- except Exception:
- pass
- else:
- if string.isalnum():
- return True
- printable = (
- "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRST"
- "UVWXYZ!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c"
- )
- return all(c in printable for c in string)
- def clean_whitespace(string, compact=False):
- """Return string with compressed whitespace."""
- for a, b in (
- ("\r\n", "\n"),
- ("\r", "\n"),
- ("\n\n", "\n"),
- ("\t", " "),
- (" ", " "),
- ):
- string = string.replace(a, b)
- if compact:
- for a, b in (("\n", " "), ("[ ", "["), (" ", " "), (" ", " "), (" ", " ")):
- string = string.replace(a, b)
- return string.strip()
- def pformat_xml(xml):
- """Return pretty formatted XML."""
- try:
- import lxml.etree as etree # delayed import
- if not isinstance(xml, bytes):
- xml = xml.encode("utf-8")
- xml = etree.parse(io.BytesIO(xml))
- xml = etree.tostring(
- xml, pretty_print=True, xml_declaration=True, encoding=xml.docinfo.encoding
- )
- xml = bytes2str(xml)
- except Exception:
- if isinstance(xml, bytes):
- xml = bytes2str(xml)
- xml = xml.replace("><", ">\n<")
- return xml.replace(" ", " ").replace("\t", " ")
- def pformat(arg, width=79, height=24, compact=True):
- """Return pretty formatted representation of object as string.
- Whitespace might be altered.
- """
- if height is None or height < 1:
- height = 1024
- if width is None or width < 1:
- width = 256
- npopt = numpy.get_printoptions()
- numpy.set_printoptions(threshold=100, linewidth=width)
- if isinstance(arg, basestring):
- if arg[:5].lower() in ("<?xml", b"<?xml"):
- if height == 1:
- arg = arg[: 4 * width]
- else:
- arg = pformat_xml(arg)
- elif isinstance(arg, bytes):
- if isprintable(arg):
- arg = bytes2str(arg)
- arg = clean_whitespace(arg)
- else:
- numpy.set_printoptions(**npopt)
- return hexdump(arg, width=width, height=height, modulo=1)
- arg = arg.rstrip()
- elif isinstance(arg, numpy.record):
- arg = arg.pprint()
- else:
- import pprint # delayed import
- compact = {} if sys.version_info[0] == 2 else dict(compact=compact)
- arg = pprint.pformat(arg, width=width, **compact)
- numpy.set_printoptions(**npopt)
- if height == 1:
- arg = clean_whitespace(arg, compact=True)
- return arg[:width]
- argl = list(arg.splitlines())
- if len(argl) > height:
- arg = "\n".join(argl[: height // 2] + ["..."] + argl[-height // 2 :])
- return arg
- def snipstr(string, width=79, snipat=0.5, ellipsis="..."):
- """Return string cut to specified length.
- >>> snipstr('abcdefghijklmnop', 8)
- 'abc...op'
- """
- if ellipsis is None:
- if isinstance(string, bytes):
- ellipsis = b"..."
- else:
- ellipsis = "\u2026" # does not print on win-py3.5
- esize = len(ellipsis)
- splitlines = string.splitlines()
- # TODO: finish and test multiline snip
- result = []
- for line in splitlines:
- if line is None:
- result.append(ellipsis)
- continue
- linelen = len(line)
- if linelen <= width:
- result.append(string)
- continue
- split = snipat
- if split is None or split == 1:
- split = linelen
- elif 0 < abs(split) < 1:
- split = int(math.floor(linelen * split))
- if split < 0:
- split += linelen
- if split < 0:
- split = 0
- if esize == 0 or width < esize + 1:
- if split <= 0:
- result.append(string[-width:])
- else:
- result.append(string[:width])
- elif split <= 0:
- result.append(ellipsis + string[esize - width :])
- elif split >= linelen or width < esize + 4:
- result.append(string[: width - esize] + ellipsis)
- else:
- splitlen = linelen - width + esize
- end1 = split - splitlen // 2
- end2 = end1 + splitlen
- result.append(string[:end1] + ellipsis + string[end2:])
- if isinstance(string, bytes):
- return b"\n".join(result)
- else:
- return "\n".join(result)
- def enumarg(enum, arg):
- """Return enum member from its name or value.
- >>> enumarg(TIFF.PHOTOMETRIC, 2)
- <PHOTOMETRIC.RGB: 2>
- >>> enumarg(TIFF.PHOTOMETRIC, 'RGB')
- <PHOTOMETRIC.RGB: 2>
- """
- try:
- return enum(arg)
- except Exception:
- try:
- return enum[arg.upper()]
- except Exception:
- raise ValueError("invalid argument %s" % arg)
- def parse_kwargs(kwargs, *keys, **keyvalues):
- """Return dict with keys from keys|keyvals and values from kwargs|keyvals.
- Existing keys are deleted from kwargs.
- >>> kwargs = {'one': 1, 'two': 2, 'four': 4}
- >>> kwargs2 = parse_kwargs(kwargs, 'two', 'three', four=None, five=5)
- >>> kwargs == {'one': 1}
- True
- >>> kwargs2 == {'two': 2, 'four': 4, 'five': 5}
- True
- """
- result = {}
- for key in keys:
- if key in kwargs:
- result[key] = kwargs[key]
- del kwargs[key]
- for key, value in keyvalues.items():
- if key in kwargs:
- result[key] = kwargs[key]
- del kwargs[key]
- else:
- result[key] = value
- return result
- def update_kwargs(kwargs, **keyvalues):
- """Update dict with keys and values if keys do not already exist.
- >>> kwargs = {'one': 1, }
- >>> update_kwargs(kwargs, one=None, two=2)
- >>> kwargs == {'one': 1, 'two': 2}
- True
- """
- for key, value in keyvalues.items():
- if key not in kwargs:
- kwargs[key] = value
- def validate_jhove(filename, jhove="jhove", ignore=("More than 50 IFDs",)):
- """Validate TIFF file using jhove -m TIFF-hul.
- Raise ValueError if jhove outputs an error message unless the message
- contains one of the strings in 'ignore'.
- JHOVE does not support bigtiff or more than 50 IFDs.
- See `JHOVE TIFF-hul Module <http://jhove.sourceforge.net/tiff-hul.html>`_
- """
- import subprocess # noqa: delayed import
- out = subprocess.check_output([jhove, filename, "-m", "TIFF-hul"])
- if b"ErrorMessage: " in out:
- for line in out.splitlines():
- line = line.strip()
- if line.startswith(b"ErrorMessage: "):
- error = line[14:].decode("utf8")
- for i in ignore:
- if i in error:
- break
- else:
- raise ValueError(error)
- break
- def lsm2bin(lsmfile, binfile=None, tile=(256, 256), verbose=True):
- """Convert [MP]TZCYX LSM file to series of BIN files.
- One BIN file containing 'ZCYX' data are created for each position, time,
- and tile. The position, time, and tile indices are encoded at the end
- of the filenames.
- """
- verbose = print_ if verbose else nullfunc
- if binfile is None:
- binfile = lsmfile
- elif binfile.lower() == "none":
- binfile = None
- if binfile:
- binfile += "_(z%ic%iy%ix%i)_m%%ip%%it%%03iy%%ix%%i.bin"
- verbose("\nOpening LSM file... ", end="", flush=True)
- start_time = time.time()
- with TiffFile(lsmfile) as lsm:
- if not lsm.is_lsm:
- verbose("\n", lsm, flush=True)
- raise ValueError("not a LSM file")
- series = lsm.series[0] # first series contains the image data
- shape = series.shape
- axes = series.axes
- dtype = series.dtype
- size = product(shape) * dtype.itemsize
- verbose("%.3f s" % (time.time() - start_time))
- # verbose(lsm, flush=True)
- verbose(
- "Image\n axes: %s\n shape: %s\n dtype: %s\n size: %s"
- % (axes, shape, dtype, format_size(size)),
- flush=True,
- )
- if not series.axes.endswith("TZCYX"):
- raise ValueError("not a *TZCYX LSM file")
- verbose("Copying image from LSM to BIN files", end="", flush=True)
- start_time = time.time()
- tiles = shape[-2] // tile[-2], shape[-1] // tile[-1]
- if binfile:
- binfile = binfile % (shape[-4], shape[-3], tile[0], tile[1])
- shape = (1,) * (7 - len(shape)) + shape
- # cache for ZCYX stacks and output files
- data = numpy.empty(shape[3:], dtype=dtype)
- out = numpy.empty((shape[-4], shape[-3], tile[0], tile[1]), dtype=dtype)
- # iterate over Tiff pages containing data
- pages = iter(series.pages)
- for m in range(shape[0]): # mosaic axis
- for p in range(shape[1]): # position axis
- for t in range(shape[2]): # time axis
- for z in range(shape[3]): # z slices
- data[z] = next(pages).asarray()
- for y in range(tiles[0]): # tile y
- for x in range(tiles[1]): # tile x
- out[:] = data[
- ...,
- y * tile[0] : (y + 1) * tile[0],
- x * tile[1] : (x + 1) * tile[1],
- ]
- if binfile:
- out.tofile(binfile % (m, p, t, y, x))
- verbose(".", end="", flush=True)
- verbose(" %.3f s" % (time.time() - start_time))
- def imshow(
- data,
- title=None,
- vmin=0,
- vmax=None,
- cmap=None,
- bitspersample=None,
- photometric="RGB",
- interpolation=None,
- dpi=96,
- figure=None,
- subplot=111,
- maxdim=32768,
- **kwargs,
- ):
- """Plot n-dimensional images using matplotlib.pyplot.
- Return figure, subplot and plot axis.
- Requires pyplot already imported C{from matplotlib import pyplot}.
- Parameters
- ----------
- bitspersample : int or None
- Number of bits per channel in integer RGB images.
- photometric : {'MINISWHITE', 'MINISBLACK', 'RGB', or 'PALETTE'}
- The color space of the image data.
- title : str
- Window and subplot title.
- figure : matplotlib.figure.Figure (optional).
- Matplotlib to use for plotting.
- subplot : int
- A matplotlib.pyplot.subplot axis.
- maxdim : int
- maximum image width and length.
- kwargs : optional
- Arguments for matplotlib.pyplot.imshow.
- """
- isrgb = photometric in ("RGB",) # 'PALETTE', 'YCBCR'
- if data.dtype.kind == "b":
- isrgb = False
- if isrgb and not (
- data.shape[-1] in (3, 4) or (data.ndim > 2 and data.shape[-3] in (3, 4))
- ):
- isrgb = False
- photometric = "MINISBLACK"
- data = data.squeeze()
- if photometric in ("MINISWHITE", "MINISBLACK", None):
- data = reshape_nd(data, 2)
- else:
- data = reshape_nd(data, 3)
- dims = data.ndim
- if dims < 2:
- raise ValueError("not an image")
- elif dims == 2:
- dims = 0
- isrgb = False
- else:
- if isrgb and data.shape[-3] in (3, 4):
- data = numpy.swapaxes(data, -3, -2)
- data = numpy.swapaxes(data, -2, -1)
- elif not isrgb and (
- data.shape[-1] < data.shape[-2] // 8
- and data.shape[-1] < data.shape[-3] // 8
- and data.shape[-1] < 5
- ):
- data = numpy.swapaxes(data, -3, -1)
- data = numpy.swapaxes(data, -2, -1)
- isrgb = isrgb and data.shape[-1] in (3, 4)
- dims -= 3 if isrgb else 2
- if isrgb:
- data = data[..., :maxdim, :maxdim, :maxdim]
- else:
- data = data[..., :maxdim, :maxdim]
- if photometric == "PALETTE" and isrgb:
- datamax = data.max()
- if datamax > 255:
- data = data >> 8 # possible precision loss
- data = data.astype("B")
- elif data.dtype.kind in "ui":
- if not (isrgb and data.dtype.itemsize <= 1) or bitspersample is None:
- try:
- bitspersample = int(math.ceil(math.log(data.max(), 2)))
- except Exception:
- bitspersample = data.dtype.itemsize * 8
- elif not isinstance(bitspersample, inttypes):
- # bitspersample can be tuple, e.g. (5, 6, 5)
- bitspersample = data.dtype.itemsize * 8
- datamax = 2**bitspersample
- if isrgb:
- if bitspersample < 8:
- data = data << (8 - bitspersample)
- elif bitspersample > 8:
- data = data >> (bitspersample - 8) # precision loss
- data = data.astype("B")
- elif data.dtype.kind == "f":
- datamax = data.max()
- if isrgb and datamax > 1.0:
- if data.dtype.char == "d":
- data = data.astype("f")
- data /= datamax
- else:
- data = data / datamax
- elif data.dtype.kind == "b":
- datamax = 1
- elif data.dtype.kind == "c":
- data = numpy.absolute(data)
- datamax = data.max()
- if not isrgb:
- if vmax is None:
- vmax = datamax
- if vmin is None:
- if data.dtype.kind == "i":
- dtmin = numpy.iinfo(data.dtype).min
- vmin = numpy.min(data)
- if vmin == dtmin:
- vmin = numpy.min(data > dtmin)
- if data.dtype.kind == "f":
- dtmin = numpy.finfo(data.dtype).min
- vmin = numpy.min(data)
- if vmin == dtmin:
- vmin = numpy.min(data > dtmin)
- else:
- vmin = 0
- pyplot = sys.modules["matplotlib.pyplot"]
- if figure is None:
- pyplot.rc("font", family="sans-serif", weight="normal", size=8)
- figure = pyplot.figure(
- dpi=dpi, figsize=(10.3, 6.3), frameon=True, facecolor="1.0", edgecolor="w"
- )
- try:
- figure.canvas.manager.window.title(title)
- except Exception:
- pass
- size = len(title.splitlines()) if title else 1
- pyplot.subplots_adjust(
- bottom=0.03 * (dims + 2),
- top=0.98 - size * 0.03,
- left=0.1,
- right=0.95,
- hspace=0.05,
- wspace=0.0,
- )
- subplot = pyplot.subplot(subplot)
- if title:
- try:
- title = unicode(title, "Windows-1252")
- except TypeError:
- pass
- pyplot.title(title, size=11)
- if cmap is None:
- if data.dtype.char == "?":
- cmap = "gray"
- elif data.dtype.kind in "buf" or vmin == 0:
- cmap = "viridis"
- else:
- cmap = "coolwarm"
- if photometric == "MINISWHITE":
- cmap += "_r"
- image = pyplot.imshow(
- numpy.atleast_2d(data[(0,) * dims].squeeze()),
- vmin=vmin,
- vmax=vmax,
- cmap=cmap,
- interpolation=interpolation,
- **kwargs,
- )
- if not isrgb:
- pyplot.colorbar() # panchor=(0.55, 0.5), fraction=0.05
- def format_coord(x, y):
- # callback function to format coordinate display in toolbar
- x = int(x + 0.5)
- y = int(y + 0.5)
- try:
- if dims:
- return "%s @ %s [%4i, %4i]" % (curaxdat[1][y, x], current, y, x)
- return "%s @ [%4i, %4i]" % (data[y, x], y, x)
- except IndexError:
- return ""
- def none(event):
- return ""
- subplot.format_coord = format_coord
- image.get_cursor_data = none
- image.format_cursor_data = none
- if dims:
- current = list((0,) * dims)
- curaxdat = [0, data[tuple(current)].squeeze()]
- sliders = [
- pyplot.Slider(
- pyplot.axes([0.125, 0.03 * (axis + 1), 0.725, 0.025]),
- "Dimension %i" % axis,
- 0,
- data.shape[axis] - 1,
- 0,
- facecolor="0.5",
- valfmt="%%.0f [%i]" % data.shape[axis],
- )
- for axis in range(dims)
- ]
- for slider in sliders:
- slider.drawon = False
- def set_image(current, sliders=sliders, data=data):
- # change image and redraw canvas
- curaxdat[1] = data[tuple(current)].squeeze()
- image.set_data(curaxdat[1])
- for ctrl, index in zip(sliders, current):
- ctrl.eventson = False
- ctrl.set_val(index)
- ctrl.eventson = True
- figure.canvas.draw()
- def on_changed(index, axis, data=data, current=current):
- # callback function for slider change event
- index = int(round(index))
- curaxdat[0] = axis
- if index == current[axis]:
- return
- if index >= data.shape[axis]:
- index = 0
- elif index < 0:
- index = data.shape[axis] - 1
- current[axis] = index
- set_image(current)
- def on_keypressed(event, data=data, current=current):
- # callback function for key press event
- key = event.key
- axis = curaxdat[0]
- if str(key) in "0123456789":
- on_changed(key, axis)
- elif key == "right":
- on_changed(current[axis] + 1, axis)
- elif key == "left":
- on_changed(current[axis] - 1, axis)
- elif key == "up":
- curaxdat[0] = 0 if axis == len(data.shape) - 1 else axis + 1
- elif key == "down":
- curaxdat[0] = len(data.shape) - 1 if axis == 0 else axis - 1
- elif key == "end":
- on_changed(data.shape[axis] - 1, axis)
- elif key == "home":
- on_changed(0, axis)
- figure.canvas.mpl_connect("key_press_event", on_keypressed)
- for axis, ctrl in enumerate(sliders):
- ctrl.on_changed(lambda k, a=axis: on_changed(k, a))
- return figure, subplot, image
- def _app_show():
- """Block the GUI. For use as skimage plugin."""
- pyplot = sys.modules["matplotlib.pyplot"]
- pyplot.show()
- def askopenfilename(**kwargs):
- """Return file name(s) from Tkinter's file open dialog."""
- try:
- from Tkinter import Tk
- import tkFileDialog as filedialog
- except ImportError:
- from tkinter import Tk, filedialog
- root = Tk()
- root.withdraw()
- root.update()
- filenames = filedialog.askopenfilename(**kwargs)
- root.destroy()
- return filenames
- def main(argv=None):
- """Command line usage main function."""
- if float(sys.version[0:3]) < 2.7:
- print("This script requires Python version 2.7 or better.")
- print("This is Python version %s" % sys.version)
- return 0
- if argv is None:
- argv = sys.argv
- import optparse # TODO: use argparse
- parser = optparse.OptionParser(
- usage="usage: %prog [options] path",
- description="Display image data in TIFF files.",
- version="%%prog %s" % __version__,
- )
- opt = parser.add_option
- opt("-p", "--page", dest="page", type="int", default=-1, help="display single page")
- opt(
- "-s",
- "--series",
- dest="series",
- type="int",
- default=-1,
- help="display series of pages of same shape",
- )
- opt(
- "--nomultifile",
- dest="nomultifile",
- action="store_true",
- default=False,
- help="do not read OME series from multiple files",
- )
- opt(
- "--noplots",
- dest="noplots",
- type="int",
- default=8,
- help="maximum number of plots",
- )
- opt(
- "--interpol",
- dest="interpol",
- metavar="INTERPOL",
- default="bilinear",
- help="image interpolation method",
- )
- opt("--dpi", dest="dpi", type="int", default=96, help="plot resolution")
- opt(
- "--vmin",
- dest="vmin",
- type="int",
- default=None,
- help="minimum value for colormapping",
- )
- opt(
- "--vmax",
- dest="vmax",
- type="int",
- default=None,
- help="maximum value for colormapping",
- )
- opt(
- "--debug",
- dest="debug",
- action="store_true",
- default=False,
- help="raise exception on failures",
- )
- opt(
- "--doctest",
- dest="doctest",
- action="store_true",
- default=False,
- help="runs the docstring examples",
- )
- opt("-v", "--detail", dest="detail", type="int", default=2)
- opt("-q", "--quiet", dest="quiet", action="store_true")
- settings, path = parser.parse_args()
- path = " ".join(path)
- if settings.doctest:
- import doctest
- doctest.testmod(optionflags=doctest.ELLIPSIS)
- return 0
- if not path:
- path = askopenfilename(
- title="Select a TIFF file", filetypes=TIFF.FILEOPEN_FILTER
- )
- if not path:
- parser.error("No file specified")
- if any(i in path for i in "?*"):
- path = glob.glob(path)
- if not path:
- print("no files match the pattern")
- return 0
- # TODO: handle image sequences
- path = path[0]
- if not settings.quiet:
- print("\nReading file structure...", end=" ")
- start = time.time()
- try:
- tif = TiffFile(path, multifile=not settings.nomultifile)
- except Exception as e:
- if settings.debug:
- raise
- else:
- print("\n", e)
- sys.exit(0)
- if not settings.quiet:
- print("%.3f ms" % ((time.time() - start) * 1e3))
- if tif.is_ome:
- settings.norgb = True
- images = []
- if settings.noplots > 0:
- if not settings.quiet:
- print("Reading image data... ", end=" ")
- def notnone(x):
- return next(i for i in x if i is not None)
- start = time.time()
- try:
- if settings.page >= 0:
- images = [(tif.asarray(key=settings.page), tif[settings.page], None)]
- elif settings.series >= 0:
- images = [
- (
- tif.asarray(series=settings.series),
- notnone(tif.series[settings.series]._pages),
- tif.series[settings.series],
- )
- ]
- else:
- images = []
- for i, s in enumerate(tif.series[: settings.noplots]):
- try:
- images.append(
- (tif.asarray(series=i), notnone(s._pages), tif.series[i])
- )
- except ValueError as e:
- images.append((None, notnone(s.pages), None))
- if settings.debug:
- raise
- else:
- print("\nSeries %i failed: %s... " % (i, e), end="")
- if not settings.quiet:
- print("%.3f ms" % ((time.time() - start) * 1e3))
- except Exception as e:
- if settings.debug:
- raise
- else:
- print(e)
- if not settings.quiet:
- print()
- print(TiffFile.__str__(tif, detail=int(settings.detail)))
- print()
- tif.close()
- if images and settings.noplots > 0:
- try:
- import matplotlib
- matplotlib.use("TkAgg")
- from matplotlib import pyplot
- except ImportError as e:
- warnings.warn("failed to import matplotlib.\n%s" % e)
- else:
- for img, page, series in images:
- if img is None:
- continue
- vmin, vmax = settings.vmin, settings.vmax
- if "GDAL_NODATA" in page.tags:
- try:
- vmin = numpy.min(
- img[img > float(page.tags["GDAL_NODATA"].value)]
- )
- except ValueError:
- pass
- if tif.is_stk:
- try:
- vmin = tif.stk_metadata["MinScale"]
- vmax = tif.stk_metadata["MaxScale"]
- except KeyError:
- pass
- else:
- if vmax <= vmin:
- vmin, vmax = settings.vmin, settings.vmax
- if series:
- title = "%s\n%s\n%s" % (str(tif), str(page), str(series))
- else:
- title = "%s\n %s" % (str(tif), str(page))
- photometric = "MINISBLACK"
- if page.photometric not in (3,):
- photometric = TIFF.PHOTOMETRIC(page.photometric).name
- imshow(
- img,
- title=title,
- vmin=vmin,
- vmax=vmax,
- bitspersample=page.bitspersample,
- photometric=photometric,
- interpolation=settings.interpol,
- dpi=settings.dpi,
- )
- pyplot.show()
- if sys.version_info[0] == 2:
- inttypes = int, long # noqa
- def print_(*args, **kwargs):
- """Print function with flush support."""
- flush = kwargs.pop("flush", False)
- print(*args, **kwargs)
- if flush:
- sys.stdout.flush()
- def bytes2str(b, encoding=None, errors=None):
- """Return string from bytes."""
- return b
- def str2bytes(s, encoding=None):
- """Return bytes from string."""
- return s
- def byte2int(b):
- """Return value of byte as int."""
- return ord(b)
- class FileNotFoundError(IOError):
- pass
- TiffFrame = TiffPage # noqa
- else:
- inttypes = int
- basestring = str, bytes
- unicode = str
- print_ = print
- def bytes2str(b, encoding=None, errors="strict"):
- """Return unicode string from encoded bytes."""
- if encoding is not None:
- return b.decode(encoding, errors)
- try:
- return b.decode("utf-8", errors)
- except UnicodeDecodeError:
- return b.decode("cp1252", errors)
- def str2bytes(s, encoding="cp1252"):
- """Return bytes from unicode string."""
- return s.encode(encoding)
- def byte2int(b):
- """Return value of byte as int."""
- return b
- if __name__ == "__main__":
- sys.exit(main())
|