" T4 R2 e" y9 @% Q \& x# o , a" a$ e r( L- ? c附注。作者在5月份更新了代码,现在最新版本号是1.3.0,博主亲测,源码在Windows 10和Ubuntu 16.04上正常运行。+ v9 k7 X3 m4 l+ `3 W: A" j; b
+ a( b/ X: @% T {: R. Y* u, ?& Y
: s, d% ]1 b$ g. Q+ `; |3 ~ {具体的安装查看Github教程:https://github.com/wkentaro/labelme/#installation( {" ]! Q% D9 G0 n
/ m% i8 A% O+ l: ?5 } y
5 j. c) n: m y在原作者的github下载源码:https://github.com/tzutalin/labelImg [7 g! t: ?. M; c( e( z+ o
。解压名为labelImg-master的文件夹,进入当前目录的命令行窗口,输入如下语句依次打开软件。 2 m3 c8 q# \7 t0 { - l( H- i/ k4 k 0 }1 ~! Z3 `/ Apython labelImg.py0 g j% y6 o1 Y" X$ m' R
1 * z4 {4 T# J( z" c* \, s6 o3 F 8 ^9 s; Y/ _& U |/ R8 e8 F2 s7 |, J# h* L6 O0 W( X/ t% H
& [( ?; S0 p" A; {
/ `, U& M+ `. ]7 s
具体使用 , I: H2 U: Y8 `: c2 C修改默认的XML文件保存位置,使用快捷键“Ctrl+R”,更改为自定义位置,这里的路径一定不能包含中文,否则不会保存。5 B6 Q2 t% f: P: ?: N
1 V3 E9 N! f' s- u* R - w* f1 z3 p* [) Z# p- @. L使用notepad++打开源文件夹中的data/predefined_classes.txt,修改默认分类,如person、car、motorcycle这三个分类。5 K. e$ K1 I) ]3 Z/ M, x
2 X6 ~& C4 J9 j; b/ U将xml文件提取图像信息 ; ]( Y8 S$ o( N下面列举如何将xml文件提取图像信息,图片保存到image文件夹,xml保存标注内容。图片和标注的文件名字一样的。 0 A3 M; Y j* t / M, ^- A/ V! k7 }$ r/ D0 _1 n4 Z( b0 p! @: A$ \
( X a( P8 _; z2 |) s $ I- Q; X! A1 X) ~下面是images图片中的一个。8 E" l% S3 ~& V" D
/ N9 d' ?3 L$ Z: A0 A2 O/ h9 q$ B5 |! \7 \ ; R8 y# w7 v: Z下面是对应的xml文件。' c. l( V: W f
/ `! L( Z* c6 r( S+ f% }& m" {5 N! H0 y* G' `/ g
<annotation> $ \$ D9 Q) [. ~% X/ Z4 I0 `- U <folder>train</folder> % x: _1 ^. S( n2 }% @ <filename>apple_30.jpg</filename>- g! q9 ~+ S; t3 s( `' q- n
<path>C:\tensorflow1\models\research\object_detection\images\train\apple_30.jpg</path> 7 ]- j& S, @$ n: X3 K$ R <source> 5 }- }6 m/ w7 F6 j- B <database>Unknown</database> ) n0 \5 s a9 O& G! A1 c7 d- } </source> % S) G& e t, o" U$ f <size>, v6 d, d) s7 \, F8 A, r
<width>800</width> 6 c3 |' n+ C2 W1 V; a; b <height>800</height> + D. |/ \- n F) v <depth>3</depth>8 ^; ?7 s5 v( T0 }; i7 {% b
</size>: K! }. J# K/ \& b+ t
<segmented>0</segmented> 3 u- c a7 H o3 D <object> 2 Y! H2 q9 S/ |+ B% Q/ i3 f+ o <name>apple</name> ' ]( u# O6 B) W2 J <pose>Unspecified</pose>% I& e* I9 H% u
<truncated>0</truncated>5 B6 b0 @ V1 E; U5 d
<difficult>0</difficult> % t9 b, T/ ] W. J- ~ <bndbox>1 i7 l1 b' Q* o7 O {" J8 B
<xmin>254</xmin> ( G4 O+ Z c% a* ]! r <ymin>163</ymin>9 G( S3 r' M0 D0 t+ J# f
<xmax>582</xmax>" D& G. C# E( }' X1 z" l, Z
<ymax>487</ymax>* k+ e" y2 o$ d c
</bndbox>% ~5 i2 Q5 u9 C- M: T5 l
</object>) u1 v0 ]5 v5 s8 y* K
<object> " S! |# s$ H7 @3 f5 ] <name>apple</name>. F8 F8 v5 K) r. m& J' ]
<pose>Unspecified</pose> ' E. [1 W3 f! [ C, @) F0 A. c <truncated>0</truncated> * \! @. K7 t$ l' r2 l& X3 g4 |0 N! g <difficult>0</difficult> 6 P- d( Y( i5 {5 y) l1 D# W <bndbox> |; T3 E6 z) {% r+ F <xmin>217</xmin>9 A% _. S+ y" S
<ymin>448</ymin>6 z( h0 K% F# c! Y
<xmax>535</xmax> , D4 g7 l5 M* G* ~7 w <ymax>713</ymax> ' \/ I$ b2 ]/ }5 ?& K9 y0 m+ \; v# h2 r </bndbox> ; m' A6 H; w$ Q& k, ~7 I; e </object>' t/ s7 u' k( r# t1 }6 N
<object> 2 Q- g9 Z6 h. {# ^3 p' u <name>apple</name> ; m' [" M* L8 H+ z1 ? <pose>Unspecified</pose> ; o9 s. T z+ n; x) D8 ~+ u7 U <truncated>1</truncated> ( b/ H- p2 ^+ E5 n. n& Y <difficult>0</difficult> ! \! O. f/ Z4 o9 e8 V <bndbox> 6 d! Y4 ]: R; { <xmin>603</xmin> 5 `/ c; O# E F1 g: E <ymin>470</ymin>8 \2 K' ]# V+ t& l
<xmax>800</xmax> 0 K* i" l. V) a; {( Y& z( `0 Z* P <ymax>716</ymax> M5 R7 z% L4 W </bndbox>7 O$ X( Z/ A5 a& g: { o8 I& @: u
</object> # k8 A# x, E: M1 j <object> 9 Q, F$ B) j" D5 E3 k0 m& {/ S# r <name>apple</name> 4 S# E" R/ }5 J$ Z6 o <pose>Unspecified</pose># b' b# R- G1 L2 x9 p
<truncated>0</truncated> 8 S' ?& l4 O# X7 C, ~6 R6 q1 e <difficult>0</difficult> 6 O, A* C( r8 @% z; a <bndbox>8 j7 e1 |& t: @3 Z
<xmin>468</xmin> : ^3 t: Z( B) ]3 H* ]6 w3 D5 }; h4 l <ymin>179</ymin>$ T8 H/ y/ \+ _0 w4 L
<xmax>727</xmax>6 R5 L# r" n& @9 s$ v( k3 x
<ymax>467</ymax> 9 O" q" X/ f& E% r! G </bndbox> & J: V! y, B6 K b) m </object>6 [3 E) o9 `1 I$ Q5 T# E, N
<object>* S$ m6 w" O. U+ z0 j
<name>apple</name>. F) I" a4 a4 c
<pose>Unspecified</pose> . @! E9 H, m: n <truncated>1</truncated> - r8 k5 a$ q# f6 ^ <difficult>0</difficult>3 H l7 m& u n
<bndbox> 6 [( o; n, K* o' ~9 p- W# j5 d <xmin>1</xmin>+ T$ }1 E2 w5 B, e" u! k2 f
<ymin>63</ymin> $ a, N! b1 u) f+ b <xmax>308</xmax> q) q2 `% O. _$ I0 ^% g* h7 \
<ymax>414</ymax> - D: X, C+ p& G# g' P; j3 M( V/ \ </bndbox>8 }9 d& o3 T M
</object>" m, f( \$ |# [) {
</annotation> 8 h* ~2 Q+ M u5 L' h3 f1" a% `0 T1 k0 Y# E: r7 [" ~. _
2 ' n; C8 u; c/ o3 ! Y+ }' }; t( ~" M, `! C g4 5 {( G5 M5 t: z7 B! l1 B; {5 ' n3 X" @. G1 a5 t6 . q- X2 a& x+ p: [& J8 S9 u7 ' X1 L2 _8 U0 Q' f$ X' R8 ! u3 |6 v: b" m/ f9 3 `6 G. f. D- s. K, P @10, c4 K/ i% W' n' B1 J/ E
110 B+ k& w( ]6 G% ?9 S0 ]
12) j; B/ z J# \
138 O g. T( o# N
14 " n5 n1 _' S1 e: l/ |+ U: A15 & s3 C& r( d/ Z/ l5 m% A4 @ Z16 / v9 z8 P0 _5 F) l$ I17 ' o8 Q9 E/ \3 i18 ! c, }9 V0 @8 w! e" N8 b: |195 O7 b7 `. w3 g: n
20 9 _- J8 v# d9 s( X21+ K( F' a* \9 c, g; j$ k* U
22 ; Y: s5 a/ f% I8 r23 4 V( e; h. |3 M24& e1 G k8 b: a4 T
256 Z3 e8 a% }# ~ t* m9 l
269 r. O; r* Y: n: h
27 7 N$ e, P/ j5 M; M' S7 k28 4 H5 C c" }3 O8 ~( S1 c7 k29 8 r2 ~; z9 g! Z; X- T. U30 ; L9 s* m6 M* Y P31 0 c& w; z" k, S9 Z8 _; O0 d V32( q" ^: n* i' t) G$ z! M% L
33& A# v* w( Z0 u# `
34 z" g/ S. P+ T& e6 D6 d
35 8 t) X" b# }$ x1 M# ^; ~/ w- l8 l36: T% Y& }# r6 ]! W
37 5 \" \+ k* {$ k0 b38 , P( o. d) {9 g39 , b) ^3 N8 |% |6 [' s. c40 9 \9 r) U: S* B ~ P41 - M" K/ w9 H; O- V; H w42 - A$ q3 L {2 r/ s7 |" m43 + ~, U9 L/ T. ]# [: @+ Z3 R: L44' k2 h/ @6 Y* `( u
45: Z$ _4 n7 z/ Y3 |
46 4 W3 r3 U- n* \8 X, {, g' X47 . ?% ^) T4 Y g' _& M) u48( x9 n9 s( P. I& U/ B5 h2 v
49 ! K9 k+ c+ s2 j% } s, X* K; m( }% u50% ~% V0 @6 t( @+ x$ I( p1 ]
51& Y" B: y* E( X# M+ f
52+ p, r0 t$ h5 j3 N
53 $ L3 \0 ?5 p% P* N542 F7 Q+ Y9 e9 B
55 $ r# c6 v+ L1 v2 ] P561 D ^) d# n" A# ?
57 , G6 g" a, m6 X7 T& D588 f' I" c, q- k" k' \# N7 u+ Q/ }+ ]- x
59 0 }8 Y0 ]1 K0 L- |! U \2 w7 a5 }60 9 D9 s/ K7 z1 O k617 i* |0 {4 u- X' S. [& k
625 i2 u5 y4 P4 d# D N' U
63 & O5 ?; i) I" ?64 {4 n' h. Z1 x- x" G+ z! |
65 - \- O+ }$ e* [0 R& |66 7 [1 N" z0 D0 {9 l67 : G( J6 A t' T. n68 $ r0 D! L9 T5 {( J" J, K69 / {! z9 _/ X* g8 Y709 T: v- j. z3 p" _/ a# J5 h4 r8 L
71- s1 \) B# D$ d
72 3 k" g1 {; \1 L0 f5 L9 F73* u/ y/ @6 r/ S9 o% k1 D
743 O6 a* K" u2 L* x( x; |
将xml文件提取图像信息,主要使用xml和opencv,基于torch提取,代码比较凌乱。 $ r& b ^6 E* {7 D1 H4 Z& K' r " P5 m& H7 Y; K) a, k 0 Z0 T5 m; N# p- _; S4 D3 M2 t; Rimport os5 G! i" j! u1 u% R. R( f
import numpy as np0 z9 j9 I7 h! e6 I
import cv29 G# F I" @, j3 }; @: }* S
import torch( e' E( c- D7 {% x* D) U5 T
import matplotlib.patches as patches ' Y7 k' D7 I) K. T2 Q+ Dimport albumentations as A & F- e; U1 G$ u, @; b' W& Gfrom albumentations.pytorch.transforms import ToTensorV2* i2 S O' w" L0 {) z
from matplotlib import pyplot as plt 1 F) ?; \6 a% G( G! J' j H, a* d N* Cfrom torch.utils.data import Dataset ! N% i( ^) L+ F: efrom xml.etree import ElementTree as et6 B$ I$ |. o( v) c4 x; p9 }
from torchvision import transforms as torchtrans # m2 o* K% i% W$ U4 g 7 E" N# ]8 m, J8 u2 i: p+ w" u6 H; p" u, S4 d: x" j
# defining the files directory and testing directory / b6 N. d. x; S/ Ftrain_image_dir = 'train/train/image' 5 a" R, G8 K+ f% M3 ntrain_xml_dir = 'train/train/xml' F4 x* B4 V( U1 h- H$ V) L5 M# test_image_dir = 'test/test/image'6 t, \0 L' G+ e5 L; m. b
# test_xml_dir = 'test/test/xml' U m& o. m$ a0 J 5 H+ V8 t# v6 o& ~$ V) F% w" \$ G/ O# J1 o: E( y
class FruitImagesDataset(Dataset):! [2 X# P [& f5 C) V
4 P2 V9 h0 S% T. h6 q e v
! Q! T) F+ i! {% p) [2 d- p def __init__(self, image_dir, xml_dir, width, height, transforms=None):3 x# i2 e/ o, X
self.transforms = transforms/ r4 R. }7 ~4 x" n, u! |; ^9 ]
self.image_dir = image_dir 2 V* a$ ^: a, h9 ^6 S* `1 g self.xml_dir = xml_dir7 r; l+ n$ L: Y I# L1 T
self.height = height- {/ M% P4 p& y6 M# ?$ l
self.width = width2 `1 P0 J5 ^* E$ P5 h3 n
3 B9 f4 v$ d3 x
& N. H1 I/ s7 F! r
# sorting the images for consistency ! g* I# a1 M. y8 Y) D( a # To get images, the extension of the filename is checked to be jpg 8 _1 M( S) Q. i+ q self.imgs = [image for image in os.listdir(self.image_dir)7 W9 r v' K+ {1 R$ I+ a7 n
if image[-4:] == '.jpg']5 R$ G, f4 @: ]
self.xmls = [xml for xml in os.listdir(self.xml_dir) / u8 t; M! g) t. }) `4 ^+ S, I if xml[-4:] == '.xml'] 9 E9 a: a8 ~- j8 L& x! R+ G. D$ s/ K5 J" u, k
0 b5 N7 }- ^' |* o # classes: 0 index is reserved for background / k/ w& G0 ?/ K+ b0 d self.classes = ['apple', 'banana', 'orange']' X u z1 b: B. ^/ n
5 b) R7 _6 @$ I- i$ K0 h, @! H$ O3 Z# E8 _8 b3 [( w N
def __getitem__(self, idx):" k: j. [$ B9 L
; C, u4 Y1 E& h: M$ _2 L) o $ b( t: r! b1 C u3 T2 h img_name = self.imgs[idx] 2 I9 M! W/ b. `( J" X$ W* x image_path = os.path.join(self.image_dir, img_name) $ a$ w. m6 p- ?3 p4 l4 H8 \2 T7 q
4 G) h9 V0 ~' q7 e9 M, v
# reading the images and converting them to correct size and color & P. @3 ]- ^, ^1 r$ S img = cv2.imread(image_path) ( \* p/ j* K$ H+ a4 k* Y img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32)4 I) u, Q. C4 v" g o
img_res = cv2.resize(img_rgb, (self.width, self.height), cv2.INTER_AREA) 4 m& t# ]) K" I% @, o8 X' N# W # diving by 2551 q# {! i4 L* K1 h+ s9 T
img_res /= 255.0! c+ H U. V, i/ S4 ~+ X
+ A9 c& Y- u& ~+ H: |% i 0 o! m( A. I1 a # annotation file 1 q( _; N+ L, y* u8 B ` annot_filename = img_name[:-4] + '.xml', n7 E3 X- x% K/ C# B' H" L! {
annot_file_path = os.path.join(self.xml_dir, annot_filename) 3 m$ g7 k/ m1 w; }8 p3 }7 L. h G d8 @! L) Z/ C4 {) I+ c+ f' I. i! C3 r8 ] x# }
boxes = []5 F. t$ ?) h8 [( _" a6 B
labels = []& _* P" l' a+ W2 U8 E( c
tree = et.parse(annot_file_path) ( D; Y: C7 A) ~/ C8 Y- {0 k$ l! j root = tree.getroot()2 @( g7 U: W: {6 u
: V; p2 `! U) E- h5 f. a3 ?" v
( l. W9 W1 ?2 N. C% a5 ~ # cv2 image gives size as height x width4 |4 P, d, X; C# ^8 R1 ?" ^
wt = img.shape[1] Y% x O" V$ z0 y' R ht = img.shape[0] & n( T7 `3 ~% A% i9 K# Q4 Z 6 H0 f2 u( [- M7 [, C" _( m: a M! D3 x1 a. _: I. J% l # box coordinates for xml files are extracted and corrected for image size given 8 t& @' w: c H1 ~$ k. F( @) Z, i for member in root.findall('object'): * U) e7 A3 j3 X: b labels.append(self.classes.index(member.find('name').text)) 6 U* j0 g: M1 _4 j } 0 z; i4 R1 b. V! t # E( ^# |, v" L/ J # bounding box, X' v% |, g( J
xmin = int(member.find('bndbox').find('xmin').text)4 l' o* k, m8 v; k6 L# v
xmax = int(member.find('bndbox').find('xmax').text) 8 {0 ]" ^- }4 {7 ?# t7 N' G2 t8 M7 [9 s
, |4 M# R: S: u0 [ ymin = int(member.find('bndbox').find('ymin').text)- {1 e) R4 E1 l5 v# j. W- J9 m
ymax = int(member.find('bndbox').find('ymax').text)3 z: q! Q; I9 r- p$ z$ G2 l- v, R7 P
0 o; r T0 t k9 e8 h4 i* o
/ a* ^: H6 T5 F: ?: K' l0 f
xmin_corr = (xmin / wt) * self.width2 R+ F4 ]: r3 p
xmax_corr = (xmax / wt) * self.width$ I- s0 o. `+ E" `
ymin_corr = (ymin / ht) * self.height & ~7 }8 T6 \1 X% w2 Z ymax_corr = (ymax / ht) * self.height, C, O2 Z! }$ u
boxes.append([xmin_corr, ymin_corr, xmax_corr, ymax_corr]) 1 Q7 h2 F7 T5 }) L 1 c: ], Y( t: ?8 e, [$ o, \( ` ]. a; O& \
# convert boxes into a torch.Tensor6 n; ]9 X( \7 E- r; Y5 v5 L! I
boxes = torch.as_tensor(boxes, dtype=torch.float32) 5 _3 q9 v& o- C8 ?5 { o 7 p2 k) g9 L/ c7 Z% M5 T( V) M7 y: U$ h
# getting the areas of the boxes 2 D6 f- {! s2 F6 S, n) c area = (boxes[:, 3] - boxes[:, 1]) * (boxes[:, 2] - boxes[:, 0]) 0 W8 \/ f9 i2 o S) b. e9 U7 F, F- M$ ?2 c1 S# H
5 V% D5 ?1 y, m/ K3 A7 A& ~( \ P # suppose all instances are not crowd 8 H( r/ w! L: F; } iscrowd = torch.zeros((boxes.shape[0],), dtype=torch.int64)( m; i+ S1 V% D' ` z) @" i1 [
- p" L+ h. e7 O5 F; {. B7 |
, R5 [+ r( l6 h( s! ]6 N8 G labels = torch.as_tensor(labels, dtype=torch.int64) % E7 U7 ]4 S8 i 3 |; ?; B. o5 g- d8 _7 ~: v4 ^" s0 b 6 D, }" l$ R! ]/ o, E target = {}) d. C# [" A( `# g
target["boxes"] = boxes . _; a- H" N4 E, S. G9 \ target["labels"] = labels2 S, B; I0 h4 D Y2 D9 [7 C
target["area"] = area * A% X, b+ V% W- C6 G target["iscrowd"] = iscrowd & |/ Z4 ?& w; J) b3 [+ P # image_id% S4 z+ X' ?* L4 R
image_id = torch.tensor([idx]) $ K$ H% m$ l) ~, r5 \ target["image_id"] = image_id8 s; P$ b6 R) x l6 q' H P
- R# v$ V. e8 k/ X8 q) h/ i" x- m7 f n% c! M* y
if self.transforms:) O( ?) @/ e/ W2 Y
sample = self.transforms(image=img_res, 1 v) u8 z$ s n6 J: _5 y bboxes=target['boxes'],( `. _! \+ @. x6 _
labels=labels)% e, O9 P7 h% V I% G6 J4 g6 S" S
3 S0 U+ [/ M" \# s
- F# w; B+ \) ? B' c
img_res = sample['image']/ V, K! x/ M* e. ]& m$ Y, A
target['boxes'] = torch.Tensor(sample['bboxes']) ' r& u/ u U8 I P- z: Y) ]0 a4 d4 ?/ c) y! E! L4 S
0 i# A# J+ T0 p: Z return img_res, target7 k9 ~7 }2 T7 _2 l& J4 g$ k+ J
f* I* d/ @8 n5 |; n( x
. E% r0 y. X2 ?3 _8 l! R$ F' v
def __len__(self): & I+ r* `( c0 z1 F return len(self.imgs) 1 Q9 r7 D: g, q) K& b0 f2 _2 D5 w( v' J! n
& y* P2 Q. N: |6 `, x
# function to convert a torchtensor back to PIL image 1 ]/ ?7 N3 d/ Udef torch_to_pil(img): : k' x: O. P) n$ x& k# l: G return torchtrans.ToPILImage()(img).convert('RGB')5 b: M8 ]% E5 W8 a8 e% t
$ \1 G: d# } d: ]5 F* B
% f( P8 m# R8 G& C : l% b2 ?% G7 J. w 6 R8 c- }3 s/ S1 w2 g7 ?def plot_img_bbox(img, target):" s3 b; M# p+ _
# plot the image and bboxes! }5 S# r' M& Z4 n( {" f
fig, a = plt.subplots(1, 1) / v" t! A0 E3 d) A$ y2 M+ u fig.set_size_inches(5, 5). {4 @. u3 U0 K4 h: ~
a.imshow(img) ) l8 M' i9 _& f for box in (target['boxes']): ! N H8 b% r; O3 d x, y, width, height = box[0], box[1], box[2] - box[0], box[3] - box[1] / q( k b1 `. l& j% s4 c5 R% y" X1 I rect = patches.Rectangle((x, y),6 Z# }4 s- q1 b4 i5 y/ ]
width, height, / h( W9 I) s" [" s" q5 r8 B linewidth=2, - s& b, W1 r4 C% y: |, l( s. z6 h edgecolor='r',- r/ Y8 k, M( ^ k4 C* T9 x
facecolor='none') ; e( w" X; k" Z$ J 3 M$ o( y- o1 U$ T- c" K' a. }0 Y- ]$ F5 Q+ A! N
# Draw the bounding box on top of the image8 q) P- T" G9 \ R X" F, }3 U' y/ x
a.add_patch(rect) $ S- p! `4 u7 }2 ]+ @ P) c: o" F+ { plt.show() 7 R2 A% a7 ^ _; q ( C" y- W) D% q- J5 n" z, c2 W! @! F! ?* w3 \) m' G. w- o
/ n" v# E% a0 t- T- c
' d2 S) Y. m6 |, V9 ]
def get_transform(train):7 P% h; }7 @1 O' l( W
if train: 1 s! x$ x0 k+ {3 [9 @( p+ M# S" T. x return A.Compose([8 v2 {/ v/ ~; a- Y& u+ W! v
A.HorizontalFlip(0.5),9 ?, t" @3 n( U
# ToTensorV2 converts image to pytorch tensor without div by 255 , D X$ Y% Y4 E1 q4 z4 B4 r' i' ~/ d ToTensorV2(p=1.0) 7 s0 f8 B6 H! b' ] ], bbox_params={'format': 'pascal_voc', 'label_fields': ['labels']}), W# H; O3 v1 U- {3 E C0 F
else: 1 q0 o- L5 p, j5 v2 }5 h5 e r return A.Compose([ : |) @+ f2 Y2 w ToTensorV2(p=1.0)5 S3 j6 G& Q$ i3 A/ i7 N
], bbox_params={'format': 'pascal_voc', 'label_fields': ['labels']}) - E& e: M ]/ T4 j/ o. p/ D # V# H/ A. C6 |& l7 d+ s5 \! O) z" p2 r5 C9 \
$ |+ {, R) M3 R: T) ~. `
x6 z+ F7 X, o0 C$ l
2 W% Z4 I! F5 ~1 s1 m- k2 _- j