Naloga

Program Picasa v vsakem direktoriju s slikami naredi (skrito) datoteko z imenom .picasa.ini, v katero shranjuje podatke o slikah (filtre in druge transformacije, ki jih uporabimo na sliki, imena oseb na sliki in podobno). Njena vsebina je lahko videti, recimo, takole:

[img_8587.jpg] faces=rect64(3d70291374bc8540),218f2860be7deadf backuphash=63076 filters=radsat=1,0.386667,0.748134,0.000000,0.000000; [img_8589.jpg] faces=rect64(4f302e14787d78d5),d5a2d2f6f0d7ccbc backuphash=31766 [img_8548.jpg] backuphash=63586 filters=finetune2=1,0.000000,0.000000,0.036491,00000000,0.415205; star=yes Vrstica, ki se začne in konča z oglatim oklepajem, vsebuje ime slike, sledijo ji podatki.

V prvih, preprostejših nalogah bomo predpostavili, da s slikami nismo počeli nič drugega, kot da smo Picasi povedali, katere osebe so na njej. Tedaj je datoteka takšne oblike.

[img_8538.jpg] faces=rect64(4ac022d1820c8624),d5a2d2f6f0d7ccbc backuphash=46512 [img_8551.jpg] faces=rect64(acb64583d1eb84cb),2623af3d8cb8e040;rect64(58bf441388df9592),d85d127e5c45cdc2 backuphash=8108 [img_8555.jpg] faces=rect64(4c755a1c7983ab84),8ff985a43603dbf8 backuphash=52059 Vrstici z imenom slike sledi vrstica, ki pove, kateri obrazi so na njej, tej pa še ena vrstica, ki nas ne zanima. Vrstica z obrazi se vedno začne s faces=. Temu sledi seznam oseb, ločen s podpičji. Na dveh slikah je le ena oseba, na drugi pa sta dve; prva je opisana z rect64(acb64583d1eb84cb),2623af3d8cb8e040 in druga z rect64(58bf441388df9592),d85d127e5c45cdc2. Opis osebe je sestavljen iz dveh podatkov, ki sta ločena z vejico: prvi nas bo zanimal le pri rešitvi za oceno 10 in ga tu ne bomo opisovali, drugi pa predstavlja identifikacijsko številko osebe.

Katera identifikacijska številka ustreza kateri osebi, vidimo v datoteki contacts.xml, ki jo Picasa shrani v C:\Users\janez\AppData\Local\Google\Picasa2\contacts\ (pri čemer namesto "janez" lahko piše ana, branka, cene, dani, eva ali francka; imena ostalih direktorijev so nekoliko drugačna v starejših različicah Windowsov). Vsebina contacts.xml je takšna.

<contacts> <contact id="8ff985a43603dbf8" name="Andrej Brodnik" display="Andrej" modified_time="2010-11-11T15:05:02+01:00" sync_enabled="1"/> <contact id="d85d127e5c45cdc2" name="Igor Kononenko" display="Igor" modified_time="2010-11-11T15:04:38+01:00" sync_enabled="1"/> <contact id="2623af3d8cb8e040" name="Rok Rupnik" display="Rok" modified_time="2010-11-11T15:04:48+01:00" sync_enabled="1"/> <contact id="d5a2d2f6f0d7ccbc" name="Franc Solina" display="Franc" modified_time="2010-11-11T15:03:59+01:00" sync_enabled="1"/> </contacts>

Tisto, kar vidimo pod id, je identifikacijska številka, pod name pa ime osebe. (Če vam brskalnik prelomi vrstice: v resnici je vsak contact ves v eni vrstici.)

Naloge

Naloga je sestavljena iz več delov, ki vam prinesejo različne ocene. Sistem za oddajo nalog si zapomni le zadnjo oddano datoteko; če boste oddali več nalog, jih zazipajte ali pa dajte rešitve v isto datoteko .py.

  • Za oceno 6: Napiši program, ki bere datoteko .picasa.ini in kopira slike v nove datoteke, katerih imena so enaka identifikacijskim številkam oseb na teh slikah (npr. 8ff985a43603dbf8.jpg). Če je na isti sliki več oseb, naredi več kopij. Če se ista oseba pojavi na več slikah, naj kasnejše kopije slike povozile prejšnje; četudi se oseba 8ff985a43603dbf8 morda pojavi na več slikah, bo obstajala bo le ena datoteka s tem imenom. Pri tem predpostavi, da imamo preprosto datoteko .picasa.ini (glej priloženo datoteko). V gornji, preprosti .picasa.ini bi skopiral img_8538.jpg v d5a2d2f6f0d7ccbc.jpg, img_8551.jpg v 2623af3d8cb8e040.jpg in d85d127e5c45cdc2.jpg (ker sta na njej dva obraza) ter img_8555.jpg v 8ff985a43603dbf8.jpg.
  • Za oceno 7: Kot za 6, vendar naj bodo imena datotek enaka imenom oseb, pri čemer presledke zamenjaš s podčrtaji. Namesto 8ff985a43603dbf8.jpg bo torej pisalo Andrej_Brodnik.jpg, saj nam contacts.xml pove, da številka 8ff985a43603dbf8 odgovarja Andreju Brodniku. (Priznam, da ga nisem vprašal, ali mu ta številka res odgovarja, vendar predpostavljam, da mu.) Tako kot prej kasnejše slike povozijo prejšnje; na koncu imamo le eno datoteko Andrej_Brodnik.jpg, čeprav se Andrej Brodnik morda pojavi na več slikah. V gornji .picasa.ini bi skopiral img_8538.jpg v Franc_Solina.jpg, img_8551.jpg v Rok_Rupnik.jpg in Igor_Kononenko.jpg (ker sta na njej dva obraza) ter img_8555.jpg v Andrej_Brodnik.jpg.
  • Za oceno 8: Kot za 7, vendar dodaj kasnejšim kopijam zaporedne številke, tako da ne povoziš slik osebe z istim imenom. Če bi bil Andrej Brodnik na štirih slikah, bi te imele imena Andrej_Brodnik.jpg, Andrej_Brodnik_0001.jpg, Andrej_Brodnik_0002.jpg in Andrej_Brodnik_0003.jpg. Predpostaviš lahko, da je bila posamezna oseba slikana največ 10000-krat.
  • Za oceno 9: Spremeni program tako, da bo znal prebrati tudi .picasa.ini, ki ima poleg teh še druge informacije (glej priloženo .picasa-tezji.ini. (Motivacija: rešitev naloge za 9 je v bistvu lažja od te za 8.)
  • Za oceno 10: Kot prej, vendar naj ne kopira slik takšnih kot so, temveč le obraze, ki se nahajajo na njih. Rezultat so torej datoteke z imeni Andrej_Brodnik.jpg, Andrej_Brodnik_0001.jpg, Andrej_Brodnik_0002.jpg... na katerih je samo obraz Andreja Brodnika. (Motivacija: sploh ni tako težko, rešitev obsega približno 30 vrstic, brez kakih posebnih trikov in tlačenja). Datoteke, ki jih naredi program, morajo biti takšne, kot v tem arhivu.

    Edit: Tudi program za oceno 10 mora znati brati "polno" datoteko .picasa.ini, sa pravi takšno, kot jo imate v arhivu pod imenom .picasa-tezji.ini
Pri reševanju uporabite priložene datoteke s slikami, datoteke .picasa.ini in contacts.xml. Predpostavljati smete, da imajo vse datoteke s slikami končnico .jpg.

Vaša rešitev mora biti pravilna. Če rešujete nalogo za oceno 10 in niste prepričani, da je rešitev pravilna, raje rešitev še nalogo za oceno 9 in/ali nižjo ter oddajte vse skupaj v enem samem .zip-u.

Roki

Nalogo je potrebno oddati do 26. novembra. Za reševanje imate torej dva tedna časa.

Pomoč

  • Za oceno 6:
    • Datoteko .picasa.ini berite v zanki while True: in za branje uporabljajte f.readline() (če je f datoteka). Zanko prekinete, ko preberete prazno vrstico. Znotraj vsake iteracije zanke boste trikrat napisali readline, da boste prebrali vse tri vrstice, ki se nanašajo na posamezno sliko. Namesto tega lahko uporabite tudi readlines(), ki prebere vse vrstice, in telovadite z njimi.
    • Vrstice, ki se začnejo s faces, najprej razdelite (split) glede na podpičja, nato glede na vejice.
    • Datoteko lahko skopirate tako, da jo odprete kot binarno datoteko, preberete v niz, odprete binarno datoteko z novim imenom in vanjo shranite prebrani niz. (Gre pa tudi hitreje, a ni potrebno.)
  • Za oceno 7:
    • Ko berete contacts.xml, vas zanimajo le vrstice, ki se začnejo (startswith) z " <contacts>" (ne spreglejte presledka na začetku!). Računate lahko na to, da je identifikacijska številka vedno od 14. do 30. znaka v vrstici, ime pa od 38. do dva znaka predtem, ko se v vrstici pojavi (find) podniz "display". (Če bi šlo zares, ne bi smeli delati tako mazaško, temveč bi uporabili Pythonov modul za branje datotek v obliki .xml. Če znate, ga smete uporabiti tudi pri reševanju naloge.)
    • Pare identifikacijska številka - ime osebe je najbolj praktično shranjevati v slovar. O njem se bomo učili naslednji teden. Gre pa tudi brez.
  • Za oceno 8:
    • Funkcija os.path.exists(ime_datoteke) pove, ali datoteka z danim imenom že obstaja. Če obstaja, poskusite ali obstaja tudi datoteka z dodano številko 0001. Če obstaja, poskusite z 0002 in tako naprej, dokler je treba.
  • Za oceno 9:
    • Datoteko beri kar v zanki for. Ko naletiš na vrstico, ki se začne in konča z oglatim oklepajem, ti pove ime datoteke. Ko naletiš na vrstico, ki se začne s faces=, ti pove, kateri obrazi so na datoteki, katere ime si nazadnje videl.
  • Za oceno 10:
    • Za to oceno pa je potrebno pokazati zmožnost samostojnega iskanja dokumentacije po spletu. Namestite si modul PIL in poglejte dokumentacijo zanj. Ne bo težko: potrebujete le funkcijo Image.open ter metodi crop in save ter lastnost size.
    • Kako prebrati koordinate, je nekdo lepo opisal (npr. http://www.facebook.com/topic.php?uid=2483740875&topic=12994 ): 1) If there are less than 16 characters, add 0's to the beginning. 2) If the picasa code is 0123456789ABCDEF, then the upper left corner of the face tag is at (0123,4567) and the lower right corner is at (89AB,CDEF) 3) The upper left corner of the photo is always (0000,0000) while the lower right is always (FFFF,FFFF). 4)The resolution of the photo doesn't matter. To the face tagging software, every photo is FFFF wide and FFFF tall, which equates to 65535 x 65535 (about 4,295 MP.. wow!) So, if your tag is in the center of the photo, the center of your tag would be at (32767,32767) which is (7FFF,7FFF). For example, the center of my [upper right] tag from my earlier post (cfff 0000 ffff 4000) is ((ffff-cfff),(4000-0000)) which comes to (3000,4000). Niz, v katerem je zapisano šestnajstiško število, pretvorite v int tako, da funkciji int dodate argument 16, npr. int("f00d", 16). Naprej se znajdite sami - desetko bo potrebno zaslužiti. :)

Rešitev za 7

Rešitev za 6 zahteva predvsem, da znamo prebrati in razkosati datoteko .picasa.ini. Od reševalca zahteva, da zna delati z datotekami (odpiranje, branje, kopiranje), predvsem pa gre za veliko telovadbo z metodo split in seznami.

Naloga za 7 zahteva branje datoteke contacts.xml, kar z nekaj iznajdljivosti ni težko. Od reševalca se pričakuje tudi, da bo znal uporabiti slovar, saj je to najhitrejši, najpreprostejši, najnaravnejši, da si zabeležimo, kateri številki pripada katero ime osebe.

Začnimo s tem, drugim, branjem contacts.xml in shranjevanjem v slovar.

d = {} for l in open("contacts.xml"): if not l.startswith(" <contact id="): continue id = l[14:30] ime = l[38:l.find("display")-2] d[id] = ime

Nič hudega nam ni bilo: najprej sestavimo prazen slovar. Nato po vrsticah beremo contacts.xml. Če se vrstica ne začne s <contact id=, nas ne zanima in rečemo le continue (lahko pa bi obrnili pogoj... kar vam je ljubše). Sicer iz nje poberemo id, ki je vedno od 14. do 30. znaka in ime, ki se začne pri 38. znaku in konča dva znaka pred besedo display. To shranimo v slovar.

Vse skupaj lahko stlačimo v eno vrstico - dovolj naravna je, da bi v resnici najbrž sprogramiral tako ... d = dict((l[14:30], l[38:l.find("display")-2]) for l in open("contacts.xml") if l.startswith(" <contact id=")) ... če ne bi bilo tole malo nevarno. Rešitev namreč temelji na predpostavki, da bo datoteka xml vedno do črke takšna, kot je. Pravi način za branje takšnih datotek je malo drugačen. Ne bomo se učili o njem, za tiste, ki jih zanima, pa le pokažimo. import xml.dom.minidom d = {} xml = xml.dom.minidom.parse("contacts.xml") for el in xml.getElementsByTagName("contact"): d[el.getAttribute("id")] = el.getAttribute("name") Ta rešitev prepušča branje datoteke .xml posebnemu modulu za branje xml-jev. Nato se sprehodi po vseh elementih contact in iz vsakega pobere atributa id in name ter ju doda v slovar. Pri takšnem branju predpostavljamo le, da bodo kontakti vedno zapisani v elementu z imenom contact in da bosta identifikacijska številka v atributih id in name. Vse ostalo pa lahko Google spreminja, kakor hoče.

Videl sem nekaj takšnih rešitev. Ob predpostavki, da njihovi avtorji v resnici sami vedo za parserje za XML: pohvalno.

Glavni problem drugega dela je, kako brati vsako tretjo vrstico datoteke in eno za njo, ono, ki še ostane, pa izpustiti. Ena možna rešitev je ta:

f = open(".picasa.ini") while True: slika = f.readline().strip()[1:-1] if not slika: break obrazi = f.readline().strip() f.readline() for osebe in obrazi.split(";"): id = osebe.split(",")[1] ime = d[id] v = open(slika, "rb").read() open(ime.replace(" ", "_")+".jpg", "wb").write(v)

Datoteko beremo z readline, ki jih ponavljamo v neskončni zanki - po tri v vsakem krogu zanke. Ko preberemo povsem prazno vrstico, zanko prekinemo z break. Sicer pa je prva prebrana vrstica ime slike - brez prvega in zadnjega znaka, ki sta zaklepaja, druga vrstica pa so obrazi. Vrstico z obrazi razkosamo po podpičjih, da dobimo posamezne osebe; for osebe in obraz.split(";"). Opis vsake osebe, naprej, razkosamo po vejici. Dobimo seznam z dvema elementoma. Prvi pove, kje na sliki se nahaja obraz osebe; tega ignoriramo. Drugi je id osebe, s katerim iz slovarja dobimo ime. Ostane le še kopiranje datoteke s primernimi imeni.

Drug, prav tako simpatičen način branja datoteke, je ta, da preberemo kar vse vrstice, potem pa z zanko jemljemo vsako tretjo...

vrstice = open(".picasa.ini").readlines() for stv in range(0, len(vrstice), 3): slika = vrstice[stv].strip()[1:-1] obrazi = vrstice[stv+1].strip() for osebe in obrazi.split(";"): # ... in tako naprej

Še tretji način bomo spoznali v rešitvi za oceno 9.

Tudi kopiramo lahko hitreje: uvozimo modul shutil in uporabimo njegovo funkcijo copy. Zadnji vrstici zamenjamo z

shutil.copy(slika, ime.replace(" ", "_")+".jpg")

Rešitev za 8

Za 8 je potrebno le še popaziti na ime datoteke. Ime sestavimo, nato v zanki preverjamo, ali že obstaja in v tem primeru dodeljujemo novega. Recimo tako:

ime_dat = "%s.jpg" % oseba.replace(" ", "_") for i in range(1, 10000): if not os.path.exists(ime_dat): break ime_dat = "%s_%04i.jpg" % (oseba, i)

Rešitev za 9

Za oceno 9 je bilo potrebno znati brati datoteke .picasa.ini, ki vsebujejo tudi več informacij, torej ni več nujno, da je v vsaki tretji vrstici nova slika in da ji tako sledijo obrazi. Branje napišemo s kar preprosto logiko: datoteko beremo po vrsticah, z običajno zanko for (s čimer se mimogrede izognemo tudi potrebi po tem, da bi preverjali, ali je vrstica prazna in v tem primeru prekinjali branje z break. Če se vrstica začne z oglatim oklepajem, vemo, da gre za ime slike - zapomnimo si ga. Če naletimo na vrstico, ki se začne s faces pa vemo, da gre za seznam oseb, ki ga razkosamo tako kot prej.

Skratka, "bralni" del se spremeni v

for l in open(".picasa.ini"): l = l.strip() if l[0] == "[": slika = l[1:-1] if l.startswith("faces"): obrazi = l.strip() for osebe in obrazi.split(";"): # ... in tako naprej

Rešitev za 10

Celotna rešitev za oceno 10 je takšna:

from PIL import Image d = {} for l in open("contacts.xml"): if not l.startswith(" <contact id="): continue id = l[14:30] ime = l[38:l.find("display")-2] d[id] = ime.replace(" ", "_") for l in open(".picasa.ini"): l = l.strip() if l[0] == "[": slika = l[1:-1] if not l.startswith("faces"): continue im = Image.open(slika) width, height = im.size for osebe in l[6:].split(";"): koord, kljuc = osebe.split(",") koord = koord[7:-1] koord = ("0000000000000000"+koord)[-16:] oseba = d[kljuc] ime_dat = "%s.jpg" % oseba for i in range(1, 10000): if os.path.exists(ime_dat): ime_dat = "%s_%04i.jpg" % (oseba, i) slika = im.crop((width*int(koord[:4], 16)/65536., height*int(koord[4:8], 16)/65536., width*int(koord[8:12], 16)/65536., height*int(koord[12:], 16)/65536.)) slika.save(ime_dat)
Zadnja sprememba: nedelja, 6. november 2011, 16.02