From f341af130e1888305a268f9ddc674ad7cbde4d1c Mon Sep 17 00:00:00 2001 From: Gaetan GOUZI Date: Mon, 5 Sep 2022 20:06:33 +0200 Subject: [PATCH] Add readme --- ImageMerger.py | 33 +++--- README.md | 109 +++++++++++++++++- images/results/output_10_2rows.png | Bin 0 -> 6011 bytes images/results/output_10_grid_shuffled.png | Bin 0 -> 3109 bytes images/results/output_4_grid.png | Bin 0 -> 2107 bytes .../output_5_grid_keep_aspect_ratio.png | Bin 0 -> 7520 bytes {samples => images/samples}/1.png | Bin {samples => images/samples}/10.png | Bin {samples => images/samples}/2.png | Bin {samples => images/samples}/3.png | Bin {samples => images/samples}/4.png | Bin {samples => images/samples}/5.png | Bin {samples => images/samples}/6.png | Bin {samples => images/samples}/7.png | Bin {samples => images/samples}/8.png | Bin {samples => images/samples}/9.png | Bin requirements.txt | 1 + 17 files changed, 126 insertions(+), 17 deletions(-) create mode 100644 images/results/output_10_2rows.png create mode 100644 images/results/output_10_grid_shuffled.png create mode 100644 images/results/output_4_grid.png create mode 100644 images/results/output_5_grid_keep_aspect_ratio.png rename {samples => images/samples}/1.png (100%) rename {samples => images/samples}/10.png (100%) rename {samples => images/samples}/2.png (100%) rename {samples => images/samples}/3.png (100%) rename {samples => images/samples}/4.png (100%) rename {samples => images/samples}/5.png (100%) rename {samples => images/samples}/6.png (100%) rename {samples => images/samples}/7.png (100%) rename {samples => images/samples}/8.png (100%) rename {samples => images/samples}/9.png (100%) create mode 100644 requirements.txt diff --git a/ImageMerger.py b/ImageMerger.py index e933766..c0362d4 100644 --- a/ImageMerger.py +++ b/ImageMerger.py @@ -24,6 +24,7 @@ def __get_image_from_url(self): img = Image.open(u) return img except Exception as e: + # TODO raise exception print(str(e)) print(traceback.format_exc()) return None @@ -34,14 +35,6 @@ def __post_init__(self): else: self.content = Image.open(self.path) - def resize(self, basewidth=800): - if self.content is None: - return None - wpercent = (basewidth / float(self.content.size[0])) - hsize = int((float(self.content.size[1]) * float(wpercent))) - img = self.content.resize((basewidth, hsize), Image.Resampling.LANCZOS) - self.content = img - @dataclass class Merger: @@ -55,11 +48,12 @@ class Merger: preserve_aspect_ratio: bool = False def __nearest_square(self, limit): + # TODO: rework sq = int((limit ** 0.5)) return sq def __post_init__(self): - # self.list_images = [im.content for im in list_images] + self.list_images = [im.content for im in self.list_images] warning_str = (f"Warning: limit_horizontal({self.limit_horizontal})*limit_vertical({self.limit_vertical}) is smaller than image set size({len(self.list_images)}). Output will not contain all images") if self.shuffle: @@ -69,9 +63,9 @@ def __post_init__(self): self.limit_horizontal = len(self.list_images) / self.limit_vertical elif self.limit_vertical and self.limit_horizontal: - print(warning_str) m = self.limit_vertical * self.limit_horizontal if m < len(self.list_images): + print(warning_str) self.list_images = self.list_images[0:m] self.limit_horizontal = self.limit_horizontal @@ -89,12 +83,12 @@ def generate_merge_list(self): elif self.merge_strategy == MERGE_GRID: merge, h = [], [] - limit_h = self.limit_horizontal if self.limit_horizontal is None: - limit_h = self.__nearest_square(len(self.list_images)) + self.limit_horizontal = self.__nearest_square(len(self.list_images)) + limit_h = self.limit_horizontal for idx, im in enumerate(self.list_images): - if idx < limit_h or idx > len(self.list_images) - self.limit_horizontal / 2: + if idx < limit_h or (idx > round(len(self.list_images) - self.limit_horizontal / 2) and len(merge) > 1): h.append(im) else: merge.append(h) @@ -106,7 +100,6 @@ def generate_merge_list(self): def __generate_merged_image(self): t = self.generate_merge_list() - print(t) if self.merge_strategy in (MERGE_HORIZONTALLY, MERGE_VERTICALLY): _im = merge_images(list_images_tmp=t, direction=self.merge_strategy, preserve_aspect_ratio=self.preserve_aspect_ratio) else: @@ -134,6 +127,7 @@ def generate_filename(suffix, extension): def concat_two_images(im1, im2, direction): + # TODO: Factorize if im1 is None: return im2 if direction == MERGE_HORIZONTALLY: @@ -147,10 +141,19 @@ def concat_two_images(im1, im2, direction): return dst +def resize(im, basewidth=800): + if im is None: + return None + wpercent = (basewidth / float(im.size[0])) + hsize = int((float(im.size[1]) * float(wpercent))) + img = im.resize((basewidth, hsize), Image.Resampling.LANCZOS) + return img + + def merge_images(list_images_tmp: List[ImageToMerge], direction: int, preserve_aspect_ratio: bool): _im = None for im in list_images_tmp: if not preserve_aspect_ratio: - im = im.resize() + im = resize(im) _im = concat_two_images(im1=_im, im2=im, direction=direction) return _im diff --git a/README.md b/README.md index 1b8b207..5662f23 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,109 @@ -# WIP # image-merger -A small Python library to merge images +A small Python library to merge images easily + +# Installation +``` +pip install image-merger +``` + +# Usage + +## Simple use case +``` +from ImageMerger import Merger, ImageToMerge, MERGE_GRID + +# Initialize list of images +list_images = [ + ImageToMerge(path='images/samples/1.png'), + ImageToMerge(path='images/samples/2.png'), + ImageToMerge(path='images/samples/3.png'), + ImageToMerge(path='images/samples/4.png') +] + +# Load merger with different settings +m = Merger(list_images=list_images) + +# Save merged image +m.save_image(filename="images/results/output_4_grid.png") +``` +![output_4_grid.png](https://raw.githubusercontent.com/ggouzi/image-merger/main/images/results/output_4_grid.png) + +## Parameters + + - `limit_horizontal: int`: Optional int to define the maximum number of images to append horizontally +- `limit_vertical: int`: Optional int to define the maximum number of images to append vertically +- `suffle: bool`: Optional boolean to decide if `list_images` must be shuffled prior to process. Default `False` +- `merge_strategy: int`: Optional int to set the merging strategy. Either `MERGE_HORIZONTALLY`, - `MERGE_VERTICALLY` or `MERGE_GRID`(default) +- `preserve_aspect_ratio: bool`: Optional boolean that defines if proportion of each image should be kept or if image can be squeezed/extended to fit. Default `False` + - Setting it to `True` can lead to images being distored to fit layout but there will be no gaps. + - Setting it to `False` will kept each image aspect ratio but can lead to black gaps in output image. + +## Configuration examples +- `ImageToMerge.path` can either be a path(local or relative) or HTTP(S) URL +- `limit_horizontal` and `limit_vertical` are optional and can also be used at the same time +- If `limit_horizontal` and `limit_vertical` are not set, and `merge_strategy` is set to `MERGE_GRID` The process will try to fit as best as possible all images in a square grid. +- List of images can be shuffled randomly using `shuffle=True` parameter + + +## More examples +``` +from ImageMerger import Merger, ImageToMerge + +# Initialize list of images +list_images = [ + ImageToMerge(path='images/samples/1.png'), + ImageToMerge(path='images/samples/2.png'), + ImageToMerge(path='https://raw.githubusercontent.com/ggouzi/image-merger/main/images/samples/3.png'), + ImageToMerge(path='https://raw.githubusercontent.com/ggouzi/image-merger/main/images/samples/4.png'), + ImageToMerge(path='https://raw.githubusercontent.com/ggouzi/image-merger/main/images/samples/5.png') +] + +# Load merger with different settings +m = Merger( + list_images=list_images, + preserve_aspect_ratio=True, + limit_vertical=2 + ) + +# Save merged image +m.save_image(filename="images/results/output_5_grid_keep_aspect_ratio.png") +``` +![output_5_grid_keep_aspect_ratio.png](https://raw.githubusercontent.com/ggouzi/image-merger/main/images/results/output_5_grid_keep_aspect_ratio.png) + +``` +from ImageMerger import Merger, ImageToMerge + +# Initialize list of images +list_images = [ImageToMerge(path=f"images/samples/{i}.png") for i in range(1, 11)] + +# Load merger with different settings +m = Merger( + list_images=list_images, + limit_horizontal=2 +) + +# Save merged image +m.save_image(filename="images/results/output_10_2rows.png") +``` +![output_10_2rows.png](https://raw.githubusercontent.com/ggouzi/image-merger/main/images/results/output_10_2rows.png) + +``` +from ImageMerger import Merger, ImageToMerge + +# Initialize list of images +list_images = [ImageToMerge(path=f"images/samples/{i}.png") for i in range(1, 11)] + +# Load merger with different settings +m = Merger( + list_images=list_images, + shuffle=True +) + +# Save merged image +m.save_image(filename="images/results/output_10_grid_shuffled.png") +``` +![output_10_grid_shuffled.png](https://raw.githubusercontent.com/ggouzi/image-merger/main/images/results/output_10_grid_shuffled.png) + +# References +- [Pillow](https://github.com/python-pillow/Pillow/) diff --git a/images/results/output_10_2rows.png b/images/results/output_10_2rows.png new file mode 100644 index 0000000000000000000000000000000000000000..c5116cd19bffe80d401f9cf0f37f1ce9b4cc7381 GIT binary patch literal 6011 zcmcJTXH=6*xP?&>L_|OZq=^MJbVYhSh=@U@sFX-iq=Y6QC3H}#)F@J=DM$^y7->*izUa;jfMQ*u*6pz8vj25qHPYuDNK?PBZIC6MJOk?}RQR_7`pi zfxnMF;=L4Ds{0gD&2|yO7A$D@^)K;Y`q;%~90j?K!IlfYTB0Uo?e5VUrWLlGzqiUbkSZnd9KR8Fd$-Gcv^6dojPN zs_N_4uNVv_KVL{tQ1H^FOINQpTS~Gglte^C_^i~Uv$L&!ygrgBZrW=jRb5>z)o-C6 zu)m&>C}xb9=}3P2_C!#_Zye4RJ(8=Ff$`lYH#Oa9tchuUAtWSJW+WQKz_RmGV87J5 zlZR*ip}P9dIF;}@Rd3(E?d!BC z{ZTLLE>obcp<(M3gYO6ClC3R-8e6|BzhL1?U~zHr!xR~o^EaMPx4oa2U#s}Ejrd^Y zg-sB1s^1*^C~Y5ln!lZ!nb4u(+?}p$fZFWQ6cB?zbO#<=HSTa4x{v>O)7aR!yIkuu z*PCr!0n>kW!Q9UnrQo^xTurSZPUvxh#Ip;WoH<`h-j!nF_4!qNzkT{N9;3KyRPB;o z;K-nl_B8a*p5JaRAfbDd2sN*Cl7{jlN^5Fsk$m!*O&i3- zX9|2VQ?1Bn-<;Lhf@f!E8-f`7KD72^X_7iqxOjPMeYagIaD_?@JA{mELGI|gjWsjv z@gfvLK!Cb>5b2Aw>&ye9Th*(nmf2aGF5~pRftF}qdpkP}buuzFl`o@P{AuNEes21t zp~6togB%Lpr=-=iC%?Tl>86GM?g#-dC!%*QjD@!)dsV@^-oGR1rm}xTfp5-Di6(2%{k?8aQ8f92=K@rihglGjNqoKh*=*~Ero8hnE zMYhcC<34jHbHFZ5J9o?x869m}_CiBjJH`NY^%MRs`sK*iz`&zwY$~)>b^Q?c^&z87 zNN8xN?JEH z*=s53jzIM4BDorX)(-mfA2V}`>XII*JsPQtE3kyAAQp{$e0;pU6}UG&*?qTg@W{x> zE0-^Oj;?al)YKH9P-Bj0EK$LqMvdSQ@!v!}d-zc*ocHGii9lCUS7%$B?vnG9bAv=| zBD^aVc*}9M-*#0cb>#NO5a#H+SFVnEvMaevw?#@|^7`hU?79~~3dhIC#f-nUl`pzf zw_~x-{I`i&n#f|KvfEYg-4u6rQDI^4%>hG37M?FNC66`Cxle@x5ku??*QQ!?zkH$1 zYO3-mxY_9Gu`5=4J);N9CdbDUakqQaaorEEa%sAY;g!{UY@|H!Uhunj??R#BKwwNv zpM}vQe&p$2XuP-m+GMl+Q1PZOZTi=T_7A-uKYr}*?+-tFz2LLhrsJ3hwD*%*kanuv zgquQsyT$D9&x#()8?Y2%^W@H%>lwZ-lTG2NWjnpSz2xqI{afPV;to1Z^_Zr2@k^tgW?J6vV2lc8ezu0QYn6-~0hp0~17c&5#b^CyU^ylKN2;R{NE zrGFV+6n*_4&EpI2h171C1UEh}(;Oqv8_QXc9O5%=a(`z*&nicv z$y`bCsueHpj=wr~f;ss3p;`Q!nefuiOMV$|7MM7sPjPj$iBTC?KxM1SKM3{iMsOW~H2n2EuHSj1F32Kq? zg69t#spUUqSEQx&nHK((Stz8%+&5;wn^$kuL)KwGRa?*_kZ?lv+Y| zZZ4Ah)-`2iS`6R}OxQ=q~CsaRKdflr7YL!?L+kB*L}DSFhHh<6!bxEhW# zb4Kwi$_NVHIoF(!kifz#qeLAK(nMbH+a@MHymnSu$P$Keo(L4hiWn4vwrcd<-_ORz z1{4s005X{@O!GdwXEZoc#PnzBlCMDhyB|)&|-Uiw8ELfsTwMp^s-?}pC8wshYg`;Y_z9#at{#zpP`yHc>6ksg9 zfBq!BfB$)CVeXe}ixmKIAt(bsoho78|M#4iP# ziVuNaRO2@HD61zaB?ZfkH8wFB%|fcwAeM%~XQ(w1M~^JIDNvTjfc$pkU1vr;!%xlD zHiARo@3OPA zm(*`P1y%%u;V^!?jsV~WP=;XPJd#3QMCl#92T&;ZDnmc#OHJo;bFm~wCG#31Wd1pD0F$xV(zbr2o+LOKj>4N}3janIR2+~p6wlp{Y>>ZlT zaIjDLF5tV`u(h=Xn&ZZXTlC$&(NWqws)Un+Lqk)uJNcI8)kF<_lanAffZ$N4I_aiB z@S}_YB49dut+a(~08G@>wBB*72K)>$<_7kox=)>Q`o6cNxKj5aF%d4sQ!P@rTt{45 z?^Y$0nyZ_cnccp9^rlxvo1%gOkkY+-tN4poL`6jz*7RG`9fRXz! zrKP-HGZSKMX<3Oze_w0i?dY=A#Umdtv@A)|I)3&$QN!PDm zrz{L1L%l5|lg&IQLZBb6E#WHQU=@&&wY0Q$HcJAO-RJw0lFo1XrU{?4r>;%_Z*NaK zePpx2MoK$f$*cDZR6F-^f)z9tP{(j+PG;%7`Gqd6k|7jDB}m}RzbxVZ(aCFerdM z1C;cq(@iQE9KL0``kbVQD*UR@$X@B1CE@=iM)hyHjHYu<5{do0Njij|dcRKTpF6By zclS;3l%;td-oD)kTxgfFqVoXICeg*+%`H_m08?F!vyqD7lP_di;no>4-x?{Dh4b9BPqt&f5VunO#-SE5fMQZ6o77Ng&qmm>C2Fjm1U%IV;#}0EiK~DFuvE!ZTL+0~ zXlPK~#dRZA@bz{B1yNC_DsX(x@WSQgWu(duL|Ax!mvDf&eo^mSUvKZ_D_77CxJhTZ zqdQw#JA^Uy9janataL#1MYjMeQ*XD;u8wUf9G7 z3@vOj1Wo*Sa|{&53nl^@u)xgBOl76iN_Gt-B0oQ0J9mAw+Vx-$HPOj>zhsD2K&1|a zLegyl!U0PZiSn=evAD-4TIC$+uV2{lPECbLMZ*+@@VKMy+ke6rv1{4Wc`80?> zhQ^0u1O(!YfcoX1$&)jk zDY-)nK#ko&R1APy9X9&hzB$+MMKI>)$*?+x_9+J94-ha!fgZs96AHC3R6@wp%L0hJ z|HlH!!Y8+yVC;A)4U;ST{0E&H+sBj?kc}y7p4()rL@Wn4H}@xVp}z2_s7fQzbdT-~ z)n`widJma;goK7Z(jka)M)ue!y_#$SX>H0C4LK1=wiIEL2vM;=Y6UBWcc%k=gQ&n8 z;CDpe)hloh6g~3wrLny|it^GOu+xJF9jg;RkGVw;;BYWdGy?sS{}%`h^%Rjrd6+k>+Aku)p5{U$>3fUG|v>;R*y-88insV3NRF zK>&ay@bd6{bsT$YW+r|j(5&QOJc#v$g+=_Wy-M4@bnp56R)K)Jx;mw+SF^#T<+pF1 z;9de;8Pp3MG^lZygXmObR=)!@ClEk;1&-?_dTeXCcE#GtYL61|Gy67RN~99GEgE@B ziYJ<)2bWAv2S*N`OMF5Cz|WgPLUUC^X0?Y`e1j+TIn3wWYHa&*tl)4(zSHQDqP{sP z9v0;Ja{w*t#bSN6HLiWI-PxD}unHhER*;w&CA)w%=rR2aiKyS(Qu&c_jYY99yp-e5Lz5R V^ZN5kaGOpC)zH3EeEWIe{{Xj9*na>3 literal 0 HcmV?d00001 diff --git a/images/results/output_10_grid_shuffled.png b/images/results/output_10_grid_shuffled.png new file mode 100644 index 0000000000000000000000000000000000000000..178b9284b8b4e7106127bbf6220af94bbd401bd3 GIT binary patch literal 3109 zcmb7Gc{r478>dpTL>deVbw*8$C4@Q5h-_mSS<=|X(h*q`#-3BY)Qk{9(TotXCmAs# z)mX|lwo&$FlEyYyi9<(;|h^L_?5dU@ZRPf6B^7)H`c6^9H&)G-2JHT0}Sh(`Z!si-eN*5_zYUjDnkm z537lExE{zX3pyu!o?^9o63Bf44<%Uj_LvF|%OotwjB zi0SkoQ_9FfPmi_fT9-O8N*2{~M+TJ=JkwV+LJ9Ts@JLQ+ayhMf?p$|Y-%7h0y?yVz z$T8OZyoZ^Y**uQ2t+pv;V5JkfFkE1fb@S#;Xv*OO`<$JfaTKB(_-2(!L^JHA(4UV$ zY6k_md$=3-3G-ioaaD5tiQ#hRzR$z;S3p+mR{*&2Gr)D9=U3p@%*K7LMK4*AK6=8r z-QWBA9vS6zdsjcNzzg1^X$Fk8x3*dr*+L3rQPwaq{(CgxXP1$d;cN3awn8~#)HJfw|l!=Wo2csHuCqjqyM)NP_byJr+wTVO`wqkv_Hi$-}WykDpW{YpOWfv6{(e4RV zIM#GBm%{2tl^eFL3ww`dG|kP;#R*C|I5<$T0iK?o*RFlb3V3^#cVD=B;BilltTfYn z{TvmrKoVjNOkai&rdA{1i6zCwlUj*xj~OLbvdm9UzB0k-;c#UAoGw-{1vk&(%vPK^ za|VC>sud{B1Oh?v-PNC;pV!9C$@)K*L-jPpi`V#%aC$9ucQ%+w4rS!h(so~sROBmZ z85ugAKCPAb(LM))T3uZgLT-#VCrBxZwK0=)(Yct|*w_;%P6!GL9*mvftj=nOE^JR_ zxZEepgZS##MzR?PuD-UIvCZ?}|M z0&}6){`e#6$EPz&8S!1jxPpQLC#4LAcl1iPac{)46>?&?mwHx5Q&UrQb#(xBskyyf zAQ0iE`b0YXYqF?<-6c?yN;|M`RBQ#50C704rnbB`6j0$%Ju~2$?O~tpTKVl#2_xAA zQr7!WU34TtN;6JBXEQqW$rIEhq80=DhNHj}wZj&lSr?hFPxQ$3vm5r1NTd?)9a-K_ zCxqAMhcbYvPce>UUwF{BcDn8!j8`8D^7i$O;;;(wlq1c|B@Tx(F)>jexsFDo6Mr2A zt2&lf5Y&j(ndIM$1rF|~socKc`nlYhj#xjAXQov9d?x9H_ppM!s|Q_GFqbb^N3Q!% z%G;@cx96(pUYTnV^)bOZZ6rjbhNfnQN~MP1VBhGd^pSf!dO&9P-g)v~S9BPQULX1L z1>6!iFfaf)osyc`J2XTYZ~FQ!S*6k|v)@ZyG9j)`Q5GQ{=e9c$$h%A7{)-~N$RsPq z_3zjvtBbJ$;fI{PxKkhi{2=1y!MKpb!AM!fe*Ef9AvGn;V2{Mh^B~D#ub+cmYm4C8i@|%4o%Y8>~{<}y5qqQG7Q&|PIm*})z^!f z$Y%}79*uKL!3)<-^==!egdKV;y6>kj{L4iyvO)1HjL8}a{&ZNOkcI_Y8-bW#-Yj>f zd|Tg|$3>!0hjS2tJDd>G^fXC6&9sDJI%(=*1=<22z~Q)8enG4c+hDOI05aGnzJotW zqHYn2!zDg=@Gz3FwX@4?NfHteSk$2}Z;y}5;?HAJo~wCwQn!D!Mbsm8bsO^Yr_!!q z@lEbuFPBoO)ZdH~O;Y;$`;9nLS4ZkX4hcy=bv%!DugxS9%PJ~DDI;lVY3ddk>FLPD zcl*(WK|7q)uL+eVV*~EUZm(c>_fxf~1Nfz0^)l-syHZT@1D7J(V%y@^{vq$J$78zt z`*E8;rdTZ27Q3N=OeVj1^Y{%&RaJFnW(K*t!Q6l7$P32I+S(dGVQ~=XLa7JMy)rl? zP~C#!q?B%PmCZ!<@4a(X0Vfy% z;>GX=WWV-mOQOuJjoWXQw%Xgx{rvo{U%&3-Gpjik#l%s3yuA+|I&?DK$w>ZM6&x%e zA|5U)tQE6M?YL$mpT>9h1F*loWD<$ALOgjLWCm*bN=gJ79=@Zp{uuU+%`2H;q^+&Z z&(DvS`k;{38m0sACDoF-v;l~^QQpab2j|b94_W!%4bX*OTxnxxx1iv3RtGthjh!sz zjK>Ns)Xgf@V`Y+BvNALKOYLp!?NP(5KXw%K^NMjJfF^vL2kCTrYWGPYl!Vd+Rn}nGTXAuPWyNwfk1dY`VxJN@9yN{1T90! zc`_U+7B4C)3MY0p`FT^@-78^cldrew?rD!6wae+R5aDU29yLRL8bi9Ny#AWQDcFxZ zRr<30f7#yu>4SeK`663{U;A1tD5Vb%$bY?{>eANESj=9uot;=W-rU@LW9L><%SBG^ z?&v7CE+;4F#S1U{BbL3vuXx1gyxGf}P(mFb>PJO$td?}>Vxpn|=stBt7#IfIf~oue zX}^E}>_42lrXG_5`Lvc5?on-PzzO&|fIN#=3FS&F>#7-g3Od!Zwl$Jnaj6pCl(&IxQWDeGdRZ;Yzvhz$*{`5!#n8 zn5$QrfPa(yhJdqKB_4)o?~$6#OG37`wQ1kl4)gb4VD%PG*_;%?Hv)rb*JF- z*9QTx9Y1o9W`)6k@dlW5=h3xuPs}AGB;LEKY!aY^Gw7!;N=tQca~KyF7fVa}D`!5y zo*Q=7hVDs6+-JT460H1oRYEadu1~Ua6p$) z4=fxGmy?qN5ExVeR27LyNg0nGc~uXdA%%TlF2U-|VV7Zd^#OCs?S>j!C7U#|*YD|{ zga^CH3V&y_Gp2JhFF}J@J^51(AvMqLcgFmF?wngCCM83+9y>NR*5Fn7B|m<3GG4jP zsl3dw2KzP&uB+>EB;$j@sqU_>OExxYG3xT!m!FM{j4&7sK)&LR5o`5CEqcwKJj@@L z45)Sh4F2Os6GKD8(9oLqu9&rhKwSh7lyY;5&(I2iKmfQ72?=SdE)L?pBde~d*|5u* zRfXmMPFxtQ_Q{2+3LHx?0vISBtx<5H)C7`3B>JgKIyyRLNBuy-pWd&nt@WgnERcY> z13wTt7F}chJ9~yv5>bC^b%xOgfnth9WsU;wMv9sD8Y=?v9PevH*B12iP(q_SEk95_7uQzZt2nxq{0D~m6!$B y|Ex{_PL4(8*r{-=|2esa;iFc`Qlj+bJ#Lsft>QVZFCVD;xlo2^gK~Y>JO2em_TcOQ literal 0 HcmV?d00001 diff --git a/images/results/output_4_grid.png b/images/results/output_4_grid.png new file mode 100644 index 0000000000000000000000000000000000000000..e73a2090c88b8efaeadd4a859c72f28932766aeb GIT binary patch literal 2107 zcmb`J`8(U$7soTETB@-x{ZijnrK(lb7D_dp+Lw?)f<{gCYpp%hzH~}!DP^Lflyoo+ zk|3B;Vj1~PY6&7DmSR#`wX_mNNC}-!`!CGg=Q;25Jm;5tpL<^Kb8im9#SVN_^(Y7g z0z24Sy8*fXYeucbZTSaI;SaaM$W-DV-IV)NNN_c;y6k9W*uqUg3pfE3+|I&xWMv-@d%R(x`TFTUsVzE5_CEW43^5EKX9)>lzh=1nzO z?N$#ocMALN|8&M-%ovV^Ad#uuy_MzVLP(CcxS0+;u;tPT=^j`Fd^n==Oy6%`eABbP z)@^VB-Z=2#jLgNQS|X9}%^-`FDjCtfi2MCax$1T5!rgzQr;gdwR)0!0GdOult)ILV z`KL%RNQ1VKa5#Qy|p&e7SI1HRV0kQb&D1TtsxKy z&!6AV%2L4fG&D3|vGQ#*>Pkw2{k`~%UP>hQit6=dqD4H<=VjFV{@#v(fq{mGMoUYJ zUI~dpp%4gxYyvz}AoY+wHa_m(8(*KcH$ITJFD$UG`2 zusHE(WMsrLd0PTA@I2edutPF)I&3@LFY=;)V4x$`*xY<@?Xajw#n;{49Sg~c`7eYF z>z$qT0rv3l@UXRgU?v4qF(y4F62rs8hlhv3U@(eQtR(ei$br(;H8&qsZWc|8Xp_3* zSxP}gBbj(h9esUR?DFX6%_X>25hBaZi}Zr1qVGGkwmnCUpBx`2quX45*MyLf-W*1W z{#x8#YxvCS>S|_^f%Yt&dVnMH^O0OCYnFTcaxIU?+t}D3lgYcgyHB1RHz;t=rlg64 zEH=BkZ|n1BP(h#3#f$5L51tO#g|}}NT`mUbvZ0xoaD`oS@19tX9f?SEsW7Lb z&0#=$x!tMB12a3eyp{n6GAvIWMw_ii!e?SeYM-xyHG) zin|8~zoO{mmvbPR7{KbkrFXg2%V^Cy;@}0;o0Ds4{j;vDP-uGYH{ldzd4M%WmgLx6)&$Q z61#(RTaNmph9qVqK>xy_=b2igebIZVNA;i89-w0s!hNn~E((%*Qa&4+o3CQpwY0Qo zG#WH)ijue)2sq4F=V@u_3ZFk|tZQHpy}v8S6fhKc!%t1Aacdh%U4bpr!Ny5lz9o#} z$B#YGXfy(G(^yq9xSciEl{^{I=7il^pSP-{?d~)-`FlUg%L@Z~4Uh#u#N(w2Ryph% zSAcQ`gF)@+kO;n{si_&n#OvzlC`9RiHT3=h@Ck#W-Q2L#VbD@%U?5po>m-MRL?YF%q&)GwuTzgsgS~q7>Ooc(b7W*}6nnNed54#umv_Q4$z#Y# z+~)Vh6=WUpHJ1xBF)6O7fZICZ54k?OlDheR7@#@AR3sA<6VuhzRYn+QGG&e(8=si) z?M_Kt8mZ{K=(7Pyc-Q|6U`4+yO@vobQ4yU^*DGLw@*h`L z+7mwALLdq$ediEfa&mHjRf)tACNn-R&bK4B4sb+kD{6{!z%`ddd3yfTlV+)>=ko5Y zqBgm(u&})R#J+0ihl%=-kdVC{e%tkW4u|vcUF$l;%g3j@q-1I48q1E$gclZ7d#Uid zD~}(00F1$<_wS)5Ccw;YnRZ5&5x_DsrlzJRVQHOBO{|^0`FX#6mz6hfZYCuqrKNRC zgy&H2G*)RxM@L&(S@|}W5L3vg57l-Bq6WBt;;h9UC_1~jx#``6T8N`QgQeqvGbtmU3dxAqm0$N*)X^)>SWvRHZXLah0;4@B0b#$6s`0Wk;?sNlzTRp1W-bAjIg O$ic?Ny5ZW*dw&C|Boo*G literal 0 HcmV?d00001 diff --git a/images/results/output_5_grid_keep_aspect_ratio.png b/images/results/output_5_grid_keep_aspect_ratio.png new file mode 100644 index 0000000000000000000000000000000000000000..6fd05cb5b4020c41aebd3571d8ad2b8fa3ce7d27 GIT binary patch literal 7520 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 literal 0 HcmV?d00001 diff --git a/samples/1.png b/images/samples/1.png similarity index 100% rename from samples/1.png rename to images/samples/1.png diff --git a/samples/10.png b/images/samples/10.png similarity index 100% rename from samples/10.png rename to images/samples/10.png diff --git a/samples/2.png b/images/samples/2.png similarity index 100% rename from samples/2.png rename to images/samples/2.png diff --git a/samples/3.png b/images/samples/3.png similarity index 100% rename from samples/3.png rename to images/samples/3.png diff --git a/samples/4.png b/images/samples/4.png similarity index 100% rename from samples/4.png rename to images/samples/4.png diff --git a/samples/5.png b/images/samples/5.png similarity index 100% rename from samples/5.png rename to images/samples/5.png diff --git a/samples/6.png b/images/samples/6.png similarity index 100% rename from samples/6.png rename to images/samples/6.png diff --git a/samples/7.png b/images/samples/7.png similarity index 100% rename from samples/7.png rename to images/samples/7.png diff --git a/samples/8.png b/images/samples/8.png similarity index 100% rename from samples/8.png rename to images/samples/8.png diff --git a/samples/9.png b/images/samples/9.png similarity index 100% rename from samples/9.png rename to images/samples/9.png diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a6d4d60 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Pillow==9.2.0