From f8b2a0e1047c5b1760dba84c56ee391c96fbe8d1 Mon Sep 17 00:00:00 2001 From: Adrien Bourmault Date: Fri, 9 Aug 2019 17:07:26 +0200 Subject: [PATCH] Working on image display --- src/Invert.png | Bin 0 -> 553 bytes src/ihm.py | 16 --- src/main.py | 277 +++++++++++++++++++++++++++++++++++++++++-- src/rotateLeft.png | Bin 0 -> 750 bytes src/rotateLeft1.png | Bin 0 -> 450 bytes src/rotateRight.png | Bin 0 -> 738 bytes src/rotateRight1.png | Bin 0 -> 428 bytes src/zoomIn.png | Bin 0 -> 772 bytes src/zoomIn20.png | Bin 0 -> 1245 bytes src/zoomIn50.png | Bin 0 -> 1217 bytes src/zoomOut.png | Bin 0 -> 735 bytes src/zoomOut20.png | Bin 0 -> 1013 bytes src/zoomOut50.png | Bin 0 -> 1147 bytes 13 files changed, 267 insertions(+), 26 deletions(-) create mode 100644 src/Invert.png create mode 100644 src/rotateLeft.png create mode 100644 src/rotateLeft1.png create mode 100644 src/rotateRight.png create mode 100644 src/rotateRight1.png create mode 100644 src/zoomIn.png create mode 100644 src/zoomIn20.png create mode 100644 src/zoomIn50.png create mode 100644 src/zoomOut.png create mode 100644 src/zoomOut20.png create mode 100644 src/zoomOut50.png diff --git a/src/Invert.png b/src/Invert.png new file mode 100644 index 0000000000000000000000000000000000000000..f9b3fb3c11b40fe1cb64b936cdffe679868134d7 GIT binary patch literal 553 zcmV+^0@nSBP)@B~OuQCJj& zJO@t$Dc~7MYNSv*MG&IJk`r`7iIOcB(nY8WUbb?_Vy za==F-T66UX_jABQu~c$(7u_82LRAOuieBfc5Tn)_d=Hy*fDbUr0e3}Y=1j-ANTI)T zz_-O(T+0Ce!bdYUjae!4v-TEbpi7JytIP9eGR3^6n=@00000NkvXXu0mjfk&*I4 literal 0 HcmV?d00001 diff --git a/src/ihm.py b/src/ihm.py index e254cfe..59c9d25 100644 --- a/src/ihm.py +++ b/src/ihm.py @@ -32,7 +32,6 @@ import PIL.Image, PIL.ImageTk import logger # logger.py import globs # globs.py -import image # image.py controlKeys = ["Escape", "Right", "Left", "Up", "Down", "Home", "End", "BackSpace", "Delete", "Inser", "Shift_L", "Shift_R", "Control_R", "Control_L"] @@ -178,21 +177,6 @@ class LauncherWindow(Tk): def exit(self): self.after(1000, self.destroy) -class AutoScrollbar(ttk.Scrollbar): - - def set(self, lo, hi): - if float(lo) <= 0.0: - if float(hi) >= 1.0: - self.grid_remove() - self.grid() - ttk.Scrollbar.set(self, lo, hi) - - def pack(self, **kw): - raise TclError('Cannot use pack with the widget ' + self.__class__.__name__) - - def place(self, **kw): - raise TclError('Cannot use place with the widget ' + self.__class__.__name__) - class ResizeableCanvas(Canvas): def __init__(self,parent,**kwargs): Canvas.__init__(self,parent,**kwargs) diff --git a/src/main.py b/src/main.py index fd53870..de45c84 100644 --- a/src/main.py +++ b/src/main.py @@ -41,7 +41,6 @@ import logger # logger.py import mrz # mrz.py import globs # globs.py import pytesseract # pytesseract.py -from image import * # image.py # Global handler logfile = logger.logCur @@ -57,6 +56,7 @@ class mainWindow(Tk): self.mrzDecided = False self.Tags = [] self.compliance = True + self.corners = [] # Hide during construction self.withdraw() @@ -146,16 +146,98 @@ class mainWindow(Tk): "INDIC" : self.indic, } - # The STATUS indicator + image display + # The STATUS indicator self.STATUT = ttk.Labelframe(self, text='Affichage de documents et statut') self.STATUT.grid_columnconfigure(0, weight=1) + self.STATUT.grid_columnconfigure(1, weight=0) self.STATUT.grid_rowconfigure(0, weight=1) + self.STATUT.grid_rowconfigure(1, weight=1) + self.STATUT.grid_rowconfigure(2, weight=1) self.STATUT.frame = Frame(self.STATUT) self.STATUT.frame.grid(column=0, row=0, sticky='NSEW') self.STATUT.frame.grid_columnconfigure(0, weight=1) self.STATUT.frame.grid_rowconfigure(0, weight=1) - self.STATUT.ZONE = ihm.ResizeableCanvas(self.STATUT.frame, bg=self["background"]) - self.STATUT.ZONE.pack(fill="both", expand=True) + # + toolbar + self.toolbar = ttk.Frame(self.STATUT) + self.toolbar.grid_columnconfigure(0, weight=1) + self.toolbar.grid_columnconfigure(1, weight=1) + self.toolbar.grid_columnconfigure(2, weight=1) + self.toolbar.grid_columnconfigure(3, weight=1) + self.toolbar.grid_columnconfigure(4, weight=1) + self.toolbar.grid_columnconfigure(5, weight=1) + self.toolbar.grid_columnconfigure(6, weight=1, minsize=10) + self.toolbar.grid_columnconfigure(7, weight=1) + self.toolbar.grid_columnconfigure(8, weight=1, minsize=10) + self.toolbar.grid_columnconfigure(9, weight=1) + self.toolbar.grid_columnconfigure(10, weight=1) + self.toolbar.grid_columnconfigure(11, weight=1) + self.toolbar.grid_columnconfigure(12, weight=1) + self.toolbar.grid_columnconfigure(13, weight=1, minsize=10) + self.toolbar.grid_columnconfigure(14, weight=1) + self.toolbar.grid_rowconfigure(0, weight=1) + + self.toolbar.zoomIn50Img = ImageTk.PhotoImage(PIL.Image.open("zoomIn50.png")) + self.toolbar.zoomIn50 = ttk.Button(self.toolbar, image=self.toolbar.zoomIn50Img, command=self.zoomInScan50) + self.toolbar.zoomIn50.grid(column=0, row=0) + + self.toolbar.zoomIn20Img = ImageTk.PhotoImage(PIL.Image.open("zoomIn20.png")) + self.toolbar.zoomIn20 = ttk.Button(self.toolbar, image=self.toolbar.zoomIn20Img, command=self.zoomInScan20) + self.toolbar.zoomIn20.grid(column=1, row=0) + + self.toolbar.zoomInImg = ImageTk.PhotoImage(PIL.Image.open("zoomIn.png")) + self.toolbar.zoomIn = ttk.Button(self.toolbar, image=self.toolbar.zoomInImg, command=self.zoomInScan) + self.toolbar.zoomIn.grid(column=2, row=0) + + self.toolbar.zoomOutImg = ImageTk.PhotoImage(PIL.Image.open("zoomOut.png")) + self.toolbar.zoomOut = ttk.Button(self.toolbar, image=self.toolbar.zoomOutImg, command=self.zoomOutScan) + self.toolbar.zoomOut.grid(column=3, row=0) + + self.toolbar.zoomOut20Img = ImageTk.PhotoImage(PIL.Image.open("zoomOut20.png")) + self.toolbar.zoomOut20 = ttk.Button(self.toolbar, image=self.toolbar.zoomOut20Img, command=self.zoomOutScan20) + self.toolbar.zoomOut20.grid(column=4, row=0) + + self.toolbar.zoomOut50Img = ImageTk.PhotoImage(PIL.Image.open("zoomOut50.png")) + self.toolbar.zoomOut50 = ttk.Button(self.toolbar, image=self.toolbar.zoomOut50Img, command=self.zoomOutScan50) + self.toolbar.zoomOut50.grid(column=5, row=0) + + self.toolbar.invertImg = ImageTk.PhotoImage(PIL.Image.open("invert.png")) + self.toolbar.invert = ttk.Button(self.toolbar, image=self.toolbar.invertImg, command=self.negativeScan) + self.toolbar.invert.grid(column=7, row=0) + + self.toolbar.rotateLeftImg = ImageTk.PhotoImage(PIL.Image.open("rotateLeft.png")) + self.toolbar.rotateLeft = ttk.Button(self.toolbar, image=self.toolbar.rotateLeftImg, command=self.rotateLeft) + self.toolbar.rotateLeft.grid(column=9, row=0) + + self.toolbar.rotateLeft1Img = ImageTk.PhotoImage(PIL.Image.open("rotateLeft1.png")) + self.toolbar.rotateLeft1 = ttk.Button(self.toolbar, image=self.toolbar.rotateLeft1Img, command=self.rotateLeft1) + self.toolbar.rotateLeft1.grid(column=10, row=0) + + self.toolbar.rotateRight1Img = ImageTk.PhotoImage(PIL.Image.open("rotateRight1.png")) + self.toolbar.rotateRight1 = ttk.Button(self.toolbar, image=self.toolbar.rotateRight1Img, command=self.rotateRight1) + self.toolbar.rotateRight1.grid(column=11, row=0) + + self.toolbar.rotateRightImg = ImageTk.PhotoImage(PIL.Image.open("rotateRight.png")) + self.toolbar.rotateRight = ttk.Button(self.toolbar, image=self.toolbar.rotateRightImg, command=self.rotateRight) + self.toolbar.rotateRight.grid(column=12, row=0) + + self.toolbar.goOCR = ttk.Button(self.toolbar, text="OCR", command=self.goOCRDetection) + self.toolbar.goOCR.grid(column=14, row=0) + + self.toolbar.grid(column=0, row=2, padx=0, pady=0) + + # + image with scrollbars + self.STATUT.hbar = ttk.Scrollbar(self.STATUT, orient='horizontal') + self.STATUT.vbar = ttk.Scrollbar(self.STATUT, orient='vertical') + self.STATUT.hbar.grid(row=1, column=0, sticky="NSEW") + self.STATUT.vbar.grid(row=0, column=1, sticky="NSEW") + + self.STATUT.ZONE = ihm.ResizeableCanvas(self.STATUT.frame, bg=self["background"], xscrollcommand=(self.STATUT.hbar.set), + yscrollcommand=(self.STATUT.vbar.set)) + self.STATUT.ZONE.grid(sticky="NSEW") + + self.STATUT.hbar.config(command=self.STATUT.ZONE.xview) + self.STATUT.vbar.config(command=self.STATUT.ZONE.yview) + self.STATUSimg = self.STATUT.ZONE.create_image(0,0, image=None) self.STATUStxt = self.STATUT.ZONE.create_text(0,0, text='', font='Times 24', fill='#FFBF00') @@ -191,7 +273,7 @@ class mainWindow(Tk): self.speed731.grid_columnconfigure(9, weight=1) self.speed731.grid_rowconfigure(0, weight=1) self.speed731text = Entry(self.speed731, font='Terminal 14') - self.speed731text.grid(column=0, row=0, sticky='NEW', padx=5) + self.speed731text.grid(column=0, row=0, sticky='NEW', padx=5, pady=5) self.speedResult = Text((self.speed731), state='disabled', width=1, height=1, wrap='none', font='Terminal 14') self.speedResult.grid(column=2, row=0, sticky='NEW') @@ -206,8 +288,8 @@ class mainWindow(Tk): self.monitor.grid_rowconfigure(0, weight=1) # All the items griding - self.lecteur_ci.grid(column=0, row=0, sticky='EWNS', columnspan=2, padx=5, pady=5) - self.STATUT.grid(column=2, row=0, sticky='EWNS', columnspan=1, padx=5, pady=5) + self.lecteur_ci.grid(column=2, row=0, sticky='EWNS', columnspan=1, padx=5, pady=5) + self.STATUT.grid(column=0, row=0, sticky='EWNS', columnspan=2, padx=5, pady=5) self.terminal.grid(column=0, row=2, sticky='EWNS', columnspan=2, padx=5, pady=5) self.terminal2.grid(column=0, row=1, sticky='EWNS', columnspan=2, padx=5, pady=5) self.monitor.grid(column=2, row=1, sticky='EWNS', columnspan=1, rowspan=2, padx=5, pady=5) @@ -260,12 +342,17 @@ class mainWindow(Tk): # Use PIL (Pillow) to convert the NumPy ndarray to a PhotoImage photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(cv_img)) self.statusUpdate("EN ATTENTE", "#FFBF00", photo, setplace=True) - + self.STATUT.imagePath = "background.png" + self.STATUT.imgZoom = 1 + self.STATUT.rotateCount = 0 + self.STATUT.blackhat = False # Some bindings self.termtext.bind('', self.entryValidation) self.termtext.bind('<>', self.pasteValidation) self.speed731text.bind('', self.speedValidation) + self.STATUT.ZONE.bind("", self.rectangleSelectScan) + logfile.printdbg('Initialization successful') def statusUpdate(self, msg, color, image=None, setplace=False): @@ -279,6 +366,8 @@ class mainWindow(Tk): self.STATUT.ZONE.move(self.STATUSimg, self.STATUT.ZONE.winfo_reqwidth() / 2, self.STATUT.ZONE.winfo_reqheight() / 2) self.STATUT.ZONE.move(self.STATUStxt, self.STATUT.ZONE.winfo_reqwidth() / 2, self.STATUT.ZONE.winfo_reqheight() / 2) + self.STATUT.ZONE.configure(scrollregion=self.STATUT.ZONE.bbox("all")) + def stringValidation(self, keysym): # analysis # If we must decide the type of the document @@ -433,6 +522,37 @@ class mainWindow(Tk): return "break" + def goOCRDetection(self): + cv_img = cv2.imread(self.STATUT.imagePath) + cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) + if self.STATUT.blackhat: + self.negativeScan() + if not self.STATUT.blackhat: + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width, channels_no = cv_img.shape + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width, channels_no = cv_img.shape + else: + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width = cv_img.shape + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width = cv_img.shape + # Rotate + rotationMatrix=cv2.getRotationMatrix2D((width/2, height/2),int(self.STATUT.rotateCount*90),1) + cv_img=cv2.warpAffine(cv_img,rotationMatrix,(width,height)) + # Resize + dim = (int(width * (self.STATUT.imgZoom + 100) / 100), int(height * (self.STATUT.imgZoom + 100) / 100)) + cv_img = cv2.resize(cv_img, dim, interpolation = cv2.INTER_AREA) + + x0 = int(self.corners[0][0]) + self.STATUT.ZONE.coords(self.STATUSimg)[0] + x1 = int(self.corners[0][1]) + self.STATUT.ZONE.coords(self.STATUSimg)[0] + y0 = int(self.corners[1][0]) + self.STATUT.ZONE.coords(self.STATUSimg)[1] + y1 = int(self.corners[1][1]) + self.STATUT.ZONE.coords(self.STATUSimg)[1] + + crop_img = cv_img[y0:y1, x0:x1] + cv2.imshow("cropped", crop_img) + cv2.waitKey(0) + def speedValidation(self, event): """ Computation of the speed entry @@ -466,8 +586,145 @@ class mainWindow(Tk): ('TIF files', '*.tiff'), ('JPEG files', '*.jpg'), ('JPEG files', '*.jpeg'))) - self.mrzdetected = '' - self.mrzdict = {} + # Load an image using OpenCV + self.STATUT.imagePath = path + self.STATUT.imgZoom = 1 + self.STATUT.blackhat = False + self.STATUT.rotateCount = 0 + cv_img = cv2.imread(path) + cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width, channels_no = cv_img.shape + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width, channels_no = cv_img.shape + # Use PIL (Pillow) to convert the NumPy ndarray to a PhotoImage + photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(cv_img)) + self.statusUpdate("", "#FFBF00", photo) + + def zoomInScan50(self, quantity = 50): + self.STATUT.imgZoom += quantity + self.resizeScan() + + def zoomOutScan50(self, quantity = 50): + self.STATUT.imgZoom -= quantity + self.resizeScan() + + def zoomInScan20(self, quantity = 20): + self.STATUT.imgZoom += quantity + self.resizeScan() + + def zoomOutScan20(self, quantity = 20): + self.STATUT.imgZoom -= quantity + self.resizeScan() + + def zoomInScan(self, quantity = 1): + self.STATUT.imgZoom += quantity + self.resizeScan() + + def zoomOutScan(self, quantity = 1): + self.STATUT.imgZoom -= quantity + self.resizeScan() + + def rotateLeft(self): + self.STATUT.rotateCount -= 1 + if self.STATUT.rotateCount < 0: + self.STATUT.rotateCount = 4 + self.resizeScan() + + def rotateRight(self): + self.STATUT.rotateCount += 1 + if self.STATUT.rotateCount > 4: + self.STATUT.rotateCount = 0 + self.resizeScan() + + def rotateRight1(self): + self.STATUT.rotateCount += 0.1 + if self.STATUT.rotateCount > 4: + self.STATUT.rotateCount = 0 + self.resizeScan() + + def rotateLeft1(self): + self.STATUT.rotateCount -= 0.1 + if self.STATUT.rotateCount < 0: + self.STATUT.rotateCount = 4 + self.resizeScan() + + def rectangleSelectScan(self, event): + canvas = event.widget + self.corners.append([canvas.canvasx(event.x), canvas.canvasy(event.y)]) + if len(self.corners) == 2: + self.select = self.STATUT.ZONE.create_rectangle(self.corners[0][0], self.corners[0][1], self.corners[1][0], self.corners[1][1], outline ='cyan', width = 2) + print("y") + if len(self.corners) > 2: + self.corners = [] + self.STATUT.ZONE.delete(self.select) + + print(self.corners) + + def negativeScan(self): + # Load an image using OpenCV + cv_img = cv2.imread(self.STATUT.imagePath) + cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) + if not self.STATUT.blackhat: + self.STATUT.blackhat = True + cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2GRAY) + cv_img = cv2.GaussianBlur(cv_img, (3, 3), 0) + cv_img = cv2.bitwise_not(cv_img) + else: + self.STATUT.blackhat = False + self.resizeScan(cv_img) + + def resizeScan(self, cv_img = None): + try: + if not hasattr(cv_img, 'shape'): + # Load an image using OpenCV + cv_img = cv2.imread(self.STATUT.imagePath) + cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) + if self.STATUT.blackhat: + self.negativeScan() + + if not self.STATUT.blackhat: + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width, channels_no = cv_img.shape + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width, channels_no = cv_img.shape + else: + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width = cv_img.shape + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width = cv_img.shape + # Rotate + rotationMatrix=cv2.getRotationMatrix2D((width/2, height/2),int(self.STATUT.rotateCount*90),1) + cv_img=cv2.warpAffine(cv_img,rotationMatrix,(width,height)) + # Resize + dim = (int(width * (self.STATUT.imgZoom + 100) / 100), int(height * (self.STATUT.imgZoom + 100) / 100)) + cv_img = cv2.resize(cv_img, dim, interpolation = cv2.INTER_AREA) + # Use PIL (Pillow) to convert the NumPy ndarray to a PhotoImage + photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(cv_img)) + self.statusUpdate("", "#FFBF00", photo) + except Exception as e: + logfile.printerr("Error with opencv : {}".format(e)) + traceback.print_exc(file=sys.stdout) + try: + # Reload an image using OpenCV + path = self.STATUT.imagePath + self.STATUT.imgZoom = 1 + self.STATUT.blackhat = False + self.STATUT.rotateCount = 0 + cv_img = cv2.imread(path) + cv_img = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width, channels_no = cv_img.shape + # Get the image dimensions (OpenCV stores image data as NumPy ndarray) + height, width, channels_no = cv_img.shape + # Use PIL (Pillow) to convert the NumPy ndarray to a PhotoImage + photo = PIL.ImageTk.PhotoImage(image = PIL.Image.fromarray(cv_img)) + self.statusUpdate("", "#FFBF00", photo) + except Exception as e: + logfile.printerr("Critical error with opencv : ".format(e)) + traceback.print_exc(file=sys.stdout) + showerror("Erreur OpenCV (traitement d'images)", "Une erreur critique s'est produite dans le gestionnaire de traitement d'images OpenCV utilisé par CNIRevelator. L'application va se réinitialiser") + self.initialize() def newEntry(self): self.initialize() diff --git a/src/rotateLeft.png b/src/rotateLeft.png new file mode 100644 index 0000000000000000000000000000000000000000..7653faa8a5bc8290de02bb48c62838824d1e9e81 GIT binary patch literal 750 zcmVG*q7fe?mPL;JMTC%I?MwHW-jNP z=iYPAd+&2I%3~v1(23JHhITX|gKwC?D?GtCrm$E+?o@2Re%!!2%woBW{lze@Mib4f zh4=zKV!i^xq|IRr=c^&yhTAF0mhcOoh0?x{Y$6)_8Tlk*dZV1HB8NLd$>Irq;1Mq3 zFq+X2Su+ly2M;iWquwxCRd9+02@tot>mv>kezPWf~3`MhEFSaTy`y8&J zk-`}*LjA0-mB`j($#VcMA8m_fC$C}ZNnHa}S!nvAe z144t;0tYb}BYxJ1b0wS?h4Z==_#~Wqd9>FkZ&@^8RIIyewW6v~FXEhVujescFXCO; z6#G9>FX96VPr{e=AkN~Xl@-t8X&rcrj?59Xtrk#=YUJ=dMmmq{qDvwTEx3f8>mY3r zlt=Pf>?{?q8-s!pc`QhqYLSab;}U8KT}mj=WWRYG}ev#(#2lC3Y6w%L69#d5-bT4c2d}@(a6T5u^H*|wyzS> zIL->Xc`lKx5}S`E)Kv{(9vO7tvLFnSvbWR|W<>a8R(--$^0sLZ)JO-;;J9#~Zdd$h g8x*9@lrr+^AH8D$g%Bu!ZU6uP07*qoM6N<$f-&Pq{{R30 literal 0 HcmV?d00001 diff --git a/src/rotateLeft1.png b/src/rotateLeft1.png new file mode 100644 index 0000000000000000000000000000000000000000..922496343c9b7348fed9764e206cac015af02308 GIT binary patch literal 450 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3EX7WqAsj$Z!;#Vf2?- zqae&Edv@0fAVadmHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IX1_nlZPZ!4! zkIuJOt-V+r1&)23zetU19lyvyQ6BMw+!7jTYE{hdAB%?6%yqosEFrm~vyorGCG5I` z!9>#+ax$su=~r&%{4+RX5Lj#6-1ceKhh5JkC68M@KeqPJ-O_8;2?{GN%~or&RUw%lyDHi<##n#T3-3 zIa@lVM|9nvJLg8r>L2rdoj>-zDcDR*^`twhSW@Yo3w^L|dtK{C^(uJvdkE4u?UOUY+{}>UlUEuPKgxLRgQZ>F`8w-?xlF+DX7F_Nb6Mw<&;$T%>9xiH literal 0 HcmV?d00001 diff --git a/src/rotateRight.png b/src/rotateRight.png new file mode 100644 index 0000000000000000000000000000000000000000..3e008dc9f94ab9e85d4e7ed9d892663dd6fc697b GIT binary patch literal 738 zcmV<80v-K{P)IR6!I5@ZTnDBhi?c2tlw=G%Df?BBEA;*jt*y zh}QlIY9oRe8#_%3ZTtspjM_y+BZwd|G1`bC1mgo^-VckJC3kN&yPM4=9}IhEcIJF% z=AJp{yE&DSM+^4iEKXt%w#NU*@Ch&R1RpSp*(z+8L=O9K4X-hU#R_d2Z*c<$QCAD` zc?@Bu3c_@o#b;bz4Pg_yOBh?gIKE*7gRzaov43$+dfnY`PXRuSa0SM7Mlp^j;dR7Crp(kZ?2+yJu^1_u#MsL5`cA7>bo+}Q}`S`3& zxTncL>d1h!fCKohc;}0CXcsQKIQUq|K3QQ-$YjfKGvUcNx-ukf6#{8aYR7pY_ZIL6 z?=m875dw6V@cN377ZQE}6Zn!5aXoIu-;$WMbOeLtCT5A6sz+ADKN9^7D&*8GiANIs zjo7hX#4i&4MjTo%;yxjn6Lx%Bb(Lt%Xv2V|$R5M7^&qZSx=3u!z;?x3Q>rZXSX)`# zjw_XH~?ldOV=IPnb!(!c{@pT%c4+ZMugPcCmj8#aLg;>KU?1V UT6}_D=Kufz07*qoM6N<$f&y7XIsgCw literal 0 HcmV?d00001 diff --git a/src/rotateRight1.png b/src/rotateRight1.png new file mode 100644 index 0000000000000000000000000000000000000000..a7de8622c26497031a13c5d22bed69dd6fb922cf GIT binary patch literal 428 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3EX7WqAsj$Z!;#Vf2?- zqae&Edv@0fAVadmHKHUqKdq!Zu_%?Hyu4g5GcUV1Ik6yBFTW^#_B$IX1_nk=PZ!4! zkIuJOz4cfEMUH)x50p)OaM!6t!e}Fd(6W}J5pAj+3U3aau=DZp2uQmm$E+;K_Fc_t z%d6G9l0yDJSvF}`{JEd+Udv=w2Z zmTQaTx5Z}1tMohDTaC|_#=R_GcA@K}c~{HdSf;bdE*6)c>)qn1&fqZrV|(z$`Zb*I z%7wSgOej(8JE-*0L4V!8k7|3KtL(}x)vlZFbzAdfyngu7C3ef%#8&&*^hO_LP2o|m zIFRLa!ARBk`{tB9uBor2taFQ}8t6_r>gW52vwemSZquFCq~#gd|DGkReHujFDs>Gvs0IYr1Ejy^qWNzMR^>?_TS-*YI8I z6r`hwt{90~7=s4X;2j>~JPzOno>j2b1cj96wOEKb3Fjhi;t4*XExKb^a-mPyg*|v# z%>{nNARNVY%tJSnQn; zU_`mYlBmL0_*~|^H`a)B5OP_E&3KzUyMoo4D%R`7X4#%D;hXf(y%7c1C?>VZ7hJ~E z)NkfxWWZq>Y$F?K60t#hhN4!7~bXFgJ^s6Z3HrJ@b&4i(x;s znNEj@k60{y(fAEDVnXd#LEI~#AhwZnSSLFjzo8D>M4wKm((x2gOdqARP!>93J?;rb z8jKE_7G5hq1!b-1@735MQZ`nFP^TRfF&zs;&h9I}>Y+J)9P4c`US`5$&-O(=a>Xwir5KCoah+PX830un7l+gtjQUVynpi z-)^sejc(#k!U-uG$^S$z>=xBu->L;2zrh*Iz@zVW8F{HLVEN1d0000EuGYds#dEX z2s%P7MMNngQVEfuL@q*tSW*c>D$ztq$C99;_R`u~w6vC5hGL?+Os6fkt(p7!aBkbt zx!1in{F66t-t+#?^PKbSL1`pt#7Im=3!2dnf8ZB0URe81ETg+)hrE0-#V}s1IF(eYgT>$)Spl~h z-xuWfL4pqPCJhw`s|JUNu3276aVP%75fupQ5zX3Nh8M(Qv_fCs??eW-ij5&DMJk{k z7hqr+-eA03;QlK{ivBpa9I-;2kGECWC~g#c>WKfSbOhQlO=X2fz1R|86o-Lo5inXz zwFgugtc{p0M&j5i6;HxSY!$m**;DS0fw&!K4IJc|Gp1R%M}q^fAtaVOmbJRT%ba^aD! z4QthPtv6Gm=n2Mry3lK*Pw?vU&ph?@5(|G@t>f@xjNO)VWJkNYmhp~l8Y;JVfyqnXGOuN# zd`*p$$4&O?hYKvmFkD}_M4!ns6KDwCW?h)RzrEi%#Vzj66qeLl28Ws&)NZ0}6X?6VvH=OclrDon}uqc6mJwmSni4p`R;j zSlh`ny-#=Q4E1{im!qRIXFjBa;x9wcV`?LXeZ}ijkgfAtm4Q&u-L(nc|S~q+TOaf>57x zlvEOeG|d- z?D4|%tmeSI^5*0dX~K-O+x5xszO%opaE0KZH1!4K6S>*9S~mSQlajnyJvFAM!MfTW zGgH3lT(TH4?g|yKi$*|Tg172EPnkPEulws`S8h~00000NkvXX Hu0mjf*Sb`T literal 0 HcmV?d00001 diff --git a/src/zoomIn50.png b/src/zoomIn50.png new file mode 100644 index 0000000000000000000000000000000000000000..21acbdcda6dadcb34bbb5ce0b248e3e60029af9c GIT binary patch literal 1217 zcmV;y1U~zTP)Ud6i1A+Fbp?h1gcSmzp(?~Vi`7JPdl5Pw)ZG?BLmI z)M6F-bmU+kEXRxJ*+Hc(7%aH^L%|az5n~~~5nTIU&>L$oNAV{*j&azEk!1)jDGb*X zln*IJh*60etSpe*LsvYEUL|-NgP7c9tP5 zKIPjUHVTnGu$Z8;u^0c8A*>_Vj>EX5t<9_uh5K==jr~D*Qg{wRx$KQaI8<2MfXAF( zu?`euwwcpeB#OK4pb&5^#iU2Ug7w&4Slc3$%u~@)j*}v?jTH(FpHMYvpOH2aE?vdH0mrPHKR=tSL0 z@Xi$a^u!W8m*H1TC`Bw2x1bIK${{}@H2VwxqtXimBc>`oXq<#9p+kMBo#NgOB4HX? zjpvkpj+4+EON2bVs)LHh3c++lkrZl;wp`I2kE5nrPjB8ZQEk^=C#qVW125-nLZ)V_ z!DsYvb;CvzHm`TxfkrPR&H7-p5Lop{+fv+CZ{3gkvHaYN+#hAl{JCCb6v=0@4mBOt zfY*BU)vrqz6LxJ=RXs%Auu*+7t%{&x9_P6n-7YSwQJc5Q3WxZ@YiX=32;YYp!=Sku?FZmj>z(ksSGGg;$?47YOA zYF`UM`XE*mj5)>PRChddN6vIK#>`6-Ymin{ITn-LHp7N2Yx7zAUdq`E7mxCm$hO_v z{Ze>?uVmD8uF?g63AJGW-otIeU~nAq-u+gjsSOp($tI?uvH6JCg~!>At(mM@Y1T6_ zi>EwlI7E@&qE?n<9E<1_SE?wi9KbzThi8QndP4C#3mk4fYJS9}aqQM+<}ZHQ!NSSa z4!f^{85!vyeN{! zse7jJmTLSB&j7J@xIuCyB>9ACgaaMcSfw+dSXCmja`^56qfmT zAkIW4rHT4RR_#lDoJ79Bbcpi?B;+D$qr~%KAM8J9F>H=~8nG~qEzDD!(%i(Nx?IuD fwG%Ut!DasfMX5d&IBhQ<00000NkvXXu0mjff^j%1 literal 0 HcmV?d00001 diff --git a/src/zoomOut.png b/src/zoomOut.png new file mode 100644 index 0000000000000000000000000000000000000000..0eca4be3d8530c66b778c182f961b882b90949ed GIT binary patch literal 735 zcmV<50wDc~P)6|e z>}*+SHkOnP!a^bo7E1?FNb`r!*+;TBHeA>Nl5s{^@|`-8C#E0dVpXu>;uM}LgMjO2m7 z;|Px9W0nW_lgT)bMy$q2RHVfA#^~fBn{ZIfQf4OMB2Hjv#=c{578g*JrDGM&U>AmD z6fIDLV>l^hqEqtNjT>TWyTb@!y3Nv;=r?tEj@oXD7Fd*sPfQEQqp#R5=Y)I9K?9y( zd8wdLxQh8@5-qSEH`5yP!bmxh5Xwn|aC&u`a1B17t<1vB_==gu8Y_egKjBA__Ho#z zw9^|;4;SiOYjyv#`JEAPQz1dkX~r~ML%&uofPMCR&t^mxrrSZkfrko?8jX!$SAy6 zIQUy~otZ({DUQi{;r4+#8n2hwhBL(*HYf3W@d)#jnCi5KJeFduShJU6{o5CN)06K; z92TcsZ)_FQz6tlT2p6andAb%0Fm$t_Th@ISFc@Wr1+C?NlHcXKdHu1vF=s9dQiAk)a}dg`WJL&ail7f RrCtC4002ovPDHLkV1h|nq)gvBe2eWET8?8ie#Cpu z|D9#=71kpjsMD+1oOiwu3>LEX{>eEsiOp7zT);g4hi2Z!yLM?93Zg5*;`sh|M|3=67_cfOA%8uR%o!+A+Vt zTAKnV&Z0}<7V5$`imOq3k+q)q{i0D2{Qm=gV`8Dj%W+yzu1ca_sGFkN{TuGCF!96K zRLUq3^tDomunSG43@^dIxT8YE58wc*24eh>F!=i||1xle(8JTparyw?3mG^cjKVK? zrCi4>*5GGBO3T3%A!Da;Rk_53!dQw^cpaCOupcL=+X+01XM}s$m8D1uVSK|pYisI^ z&SZ3U9JVjF+KPYv(uSHTcbPqAtchutsFmh;1rb{%QA?Nb}2!|HWz%b7O!F}2w$dpCaW*|zKjKiQWcDB8ycKxt1k9%}0#VNZN;`zk2D$9N{Zuygs=l~x_>}p9YIukB(FiMR$`_~8@?1z{z7ViJmY!ddh~HXlCt6OjsWyBBY;0L# jZ4qLEF5H>l#Pt6FC))G-mKAi`00000NkvXXu0mjfM8)CJ literal 0 HcmV?d00001 diff --git a/src/zoomOut50.png b/src/zoomOut50.png new file mode 100644 index 0000000000000000000000000000000000000000..f753234c5699ad9fb3ebab0a2c670a871bb21173 GIT binary patch literal 1147 zcmV->1cdvEP))KwJ6@z4K1Q%VtJsj^84E~p6jLO>#>q8LqB zL?1K|eIN=vxIL)Rs7*RPxL{O_k!Tvlia{PwBko%i2t?5!vL&dXL@g*7N{h5~n9kpa zJ8X5PQ0VVvGXH!2_j~TS_nve9ks>6&HZt=%TkpbS~E6F{QWYsII|Ij6Xx@ z5lzDPxUiH#jB?Dtrv>Bg&?fwhnVlI8#wuLVP0%B{QwVuc#pqx7{j;0F8*!d++KYmR zVINw$8T>n1F{apHS~!KxI9z1B3eV{LYW#={_IG%GK*(qVOM%;kl^0X9aK$P+0skVT z(T<7=^R|})?-usC9RGG0Z^Db6Ks(a_^gu(g!5F*H7iXO&pp?!>PvL461@9Ni+@;+J zjfASTRXDgM!E10AQr!d|fa|d2WQJq$D{k#3@GPvv&{HvfSV(iD&k%YUe#0F~HyS6X z9Ipti%*fLKMnaKUhlS|fm6y-KYeKras*B($!eMH|wI?&~c$ix7HAbMOXNAEdM%z8= z3S|?gDw|j>yj5lP%5qOj;$WNY`lh%qt%VOMyf6&ha8?nE$VTz zaAQsc&#(1_M9UwIo~^3X2buPE%VcedB1uJWjgzL^*4X6ZN?bCB!bSKHhg zuf%(7Pv{r3{m3LqUPBa{&|y&ra5omiZ>irWl_26 zB=$=}9yheJIz-l{IB(vZ8Xu<8$|J7EBPG|(9-#+YD1`mKIF&Ld;j8^yt&{aaM4x=S zab