From a70ef2cf2e01595c05380e5bdc1d3d2a5c202ed5 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Wed, 19 Jun 2024 20:58:01 +0530 Subject: [PATCH 01/10] extracting difference between two images --- experiments/extract_difference_image.py | 45 ++++++++++++++++++++++++ experiments/winCalNew.png | Bin 0 -> 16821 bytes experiments/winCalOld.png | Bin 0 -> 21555 bytes 3 files changed, 45 insertions(+) create mode 100644 experiments/extract_difference_image.py create mode 100644 experiments/winCalNew.png create mode 100644 experiments/winCalOld.png diff --git a/experiments/extract_difference_image.py b/experiments/extract_difference_image.py new file mode 100644 index 000000000..8ba39ea3b --- /dev/null +++ b/experiments/extract_difference_image.py @@ -0,0 +1,45 @@ +from PIL import Image +import numpy as np +import cv2 +from skimage.metrics import structural_similarity as ssim + +def extract_difference_image( + new_image: Image.Image, + old_image: Image.Image, + tolerance: float = 0.05, +) -> Image.Image: + """Extract the portion of the new image that is different from the old image.""" + new_image_np = np.array(new_image.convert('L')) + old_image_np = np.array(old_image.convert('L')) + + # Compute the SSIM between the two images + score, diff = ssim(new_image_np, old_image_np, full=True) + diff = (diff * 255).astype("uint8") + + # Threshold the difference image to get the regions that are different + thresh = cv2.threshold(diff, 255 * (1 - tolerance), 255, cv2.THRESH_BINARY_INV)[1] + + # Find contours of the different regions + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # Create a mask of the differences + mask = np.zeros_like(new_image_np) + cv2.drawContours(mask, contours, -1, (255), thickness=cv2.FILLED) + + # Apply the mask to the new image to extract the different regions + diff_image_np = cv2.bitwise_and(np.array(new_image), np.array(new_image), mask=mask) + + return Image.fromarray(diff_image_np) + +# Example usage: +# new_image = Image.open('path_to_new_image') +# old_image = Image.open('path_to_old_image') +# difference_image = extract_difference_image(new_image, old_image, tolerance=0.05) +# difference_image.show() + +new_image = Image.open('./winCalNew.png') +old_image = Image.open('./winCalOld.png') +difference_image = extract_difference_image(new_image, old_image, tolerance=0.05) +difference_image.show() +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/experiments/winCalNew.png b/experiments/winCalNew.png new file mode 100644 index 0000000000000000000000000000000000000000..804968440bbe7a6e376998b2990d39e0dba40e98 GIT binary patch literal 16821 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�*Ni^!-&1qWhw<;mV5wPDwLh~ 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 literal 0 HcmV?d00001 diff --git a/experiments/winCalOld.png b/experiments/winCalOld.png new file mode 100644 index 0000000000000000000000000000000000000000..0a7cd00687f90ca2f2b0ff2fa73e38b5f334a527 GIT binary patch literal 21555 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 literal 0 HcmV?d00001 From dda26991d813ab6689a55f21953ed3105c9fbd26 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Thu, 20 Jun 2024 18:27:23 +0530 Subject: [PATCH 02/10] feat:add combine segmentation in visual.py and extract difference in vision.py --- experiments/winCalNew.png | Bin 16821 -> 16522 bytes experiments/winCalOld.png | Bin 21555 -> 16206 bytes openadapt/strategies/visual.py | 50 +++++++++++++++++++++++++++++++++ openadapt/vision.py | 28 ++++++++++++++++++ 4 files changed, 78 insertions(+) diff --git a/experiments/winCalNew.png b/experiments/winCalNew.png index 804968440bbe7a6e376998b2990d39e0dba40e98..90d66f7712968417ac1c7c3f7d98a8224fdf0c5e 100644 GIT binary patch literal 16522 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 literal 16821 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�*Ni^!-&1qWhw<;mV5wPDwLh~ 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 diff --git a/experiments/winCalOld.png b/experiments/winCalOld.png index 0a7cd00687f90ca2f2b0ff2fa73e38b5f334a527..fb772536fa15adf6a77a4f201ec170ca020450b7 100644 GIT binary patch literal 16206 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%0ATNfIPT_qaCg#4g1@h9rSGU&Zp-O#qgxmDY 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 diff --git a/openadapt/strategies/visual.py b/openadapt/strategies/visual.py index a19aa93d2..b9815ff3c 100644 --- a/openadapt/strategies/visual.py +++ b/openadapt/strategies/visual.py @@ -47,9 +47,11 @@ from dataclasses import dataclass from pprint import pformat import time +from xml.etree.ElementPath import find from loguru import logger from PIL import Image, ImageDraw +from typing import List import numpy as np from openadapt import adapters, common, models, plotting, strategies, utils, vision @@ -317,6 +319,7 @@ def get_active_segment( return active_index + def find_similar_image_segmentation( image: Image.Image, min_ssim: float = MIN_SCREENSHOT_SSIM, @@ -353,6 +356,33 @@ def find_similar_image_segmentation( return similar_segmentation, similar_segmentation_diff + + +def combine_segmentations( + previous_segmentation: Segmentation, + new_descriptions: List[str], + new_masked_images: List[Image.Image], + new_masks: List[np.ndarray] +) -> Segmentation: + + combined_image = previous_segmentation.image + combined_masked_images = previous_segmentation.masked_images + new_masked_images + combined_descriptions = previous_segmentation.descriptions + new_descriptions + + previous_bounding_boxes, previous_centroids = vision.calculate_bounding_boxes(previous_segmentation.masks) + new_bounding_boxes, new_centroids = vision.calculate_bounding_boxes(new_masks) + + combined_bounding_boxes = previous_bounding_boxes + new_bounding_boxes + combined_centroids = previous_centroids + new_centroids + + return Segmentation( + image=combined_image, + masked_images=combined_masked_images, + descriptions=combined_descriptions, + bounding_boxes=combined_bounding_boxes, + centroids=combined_centroids + ) + def get_window_segmentation( action_event: models.ActionEvent, @@ -382,6 +412,26 @@ def get_window_segmentation( # TODO XXX: create copy of similar_segmentation, but overwrite with segments of # regions of new image where segments of similar_segmentation overlap non-zero # regions of similar_segmentation_diff + new_image = vision.extract_difference_image( + original_image, + similar_segmentation.image, + tolerance=0.05, + ) + new_masks = vision.get_masks_from_segmented_image(new_image) + new_masked_images = vision.extract_masked_images(new_image, new_masks) + new_descriptions = prompt_for_descriptions( + new_image, + new_masked_images, + action_event.active_segment_description, + exceptions, + ) + updated_segmentation = combine_segmentations( + similar_segmentation, + new_descriptions, + new_masked_images, + new_masks, + ) + similar_segmentation = updated_segmentation return similar_segmentation segmentation_adapter = adapters.get_default_segmentation_adapter() diff --git a/openadapt/vision.py b/openadapt/vision.py index 65c3660ec..0023c1125 100644 --- a/openadapt/vision.py +++ b/openadapt/vision.py @@ -127,6 +127,34 @@ def refine_masks(masks: list[np.ndarray]) -> list[np.ndarray]: logger.info(f"{len(refined_masks)=}") return refined_masks +@cache.cache() +def extract_difference_image( + new_image: Image.Image, + old_image: Image.Image, + tolerance: float = 0.05, +) -> Image.Image: + """Extract the portion of the new image that is different from the old image.""" + new_image_np = np.array(new_image.convert('L')) + old_image_np = np.array(old_image.convert('L')) + + # Compute the SSIM between the two images + score, diff = ssim(new_image_np, old_image_np, full=True) + diff = (diff * 255).astype("uint8") + + # Threshold the difference image to get the regions that are different + thresh = cv2.threshold(diff, 255 * (1 - tolerance), 255, cv2.THRESH_BINARY_INV)[1] + + # Find contours of the different regions + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # Create a mask of the differences + mask = np.zeros_like(new_image_np) + cv2.drawContours(mask, contours, -1, (255), thickness=cv2.FILLED) + + # Apply the mask to the new image to extract the different regions + diff_image_np = cv2.bitwise_and(np.array(new_image), np.array(new_image), mask=mask) + + return Image.fromarray(diff_image_np) @cache.cache() def filter_thin_ragged_masks( From 7dada60a6f9f965e1119a3c55d342202254989f2 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Thu, 20 Jun 2024 19:53:12 +0530 Subject: [PATCH 03/10] fix: filter out the masked_image and the desc that are not relevant for the new image, removed unnecessary contours in extract_difference_image --- experiments/extract_difference_image.py | 45 ----------------- experiments/winCalNew.png | Bin 16522 -> 0 bytes experiments/winCalOld.png | Bin 16206 -> 0 bytes openadapt/strategies/visual.py | 62 ++++++++++++++++-------- openadapt/vision.py | 23 ++++----- 5 files changed, 52 insertions(+), 78 deletions(-) delete mode 100644 experiments/extract_difference_image.py delete mode 100644 experiments/winCalNew.png delete mode 100644 experiments/winCalOld.png diff --git a/experiments/extract_difference_image.py b/experiments/extract_difference_image.py deleted file mode 100644 index 8ba39ea3b..000000000 --- a/experiments/extract_difference_image.py +++ /dev/null @@ -1,45 +0,0 @@ -from PIL import Image -import numpy as np -import cv2 -from skimage.metrics import structural_similarity as ssim - -def extract_difference_image( - new_image: Image.Image, - old_image: Image.Image, - tolerance: float = 0.05, -) -> Image.Image: - """Extract the portion of the new image that is different from the old image.""" - new_image_np = np.array(new_image.convert('L')) - old_image_np = np.array(old_image.convert('L')) - - # Compute the SSIM between the two images - score, diff = ssim(new_image_np, old_image_np, full=True) - diff = (diff * 255).astype("uint8") - - # Threshold the difference image to get the regions that are different - thresh = cv2.threshold(diff, 255 * (1 - tolerance), 255, cv2.THRESH_BINARY_INV)[1] - - # Find contours of the different regions - contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - # Create a mask of the differences - mask = np.zeros_like(new_image_np) - cv2.drawContours(mask, contours, -1, (255), thickness=cv2.FILLED) - - # Apply the mask to the new image to extract the different regions - diff_image_np = cv2.bitwise_and(np.array(new_image), np.array(new_image), mask=mask) - - return Image.fromarray(diff_image_np) - -# Example usage: -# new_image = Image.open('path_to_new_image') -# old_image = Image.open('path_to_old_image') -# difference_image = extract_difference_image(new_image, old_image, tolerance=0.05) -# difference_image.show() - -new_image = Image.open('./winCalNew.png') -old_image = Image.open('./winCalOld.png') -difference_image = extract_difference_image(new_image, old_image, tolerance=0.05) -difference_image.show() -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/experiments/winCalNew.png b/experiments/winCalNew.png deleted file mode 100644 index 90d66f7712968417ac1c7c3f7d98a8224fdf0c5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16522 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 diff --git a/experiments/winCalOld.png b/experiments/winCalOld.png deleted file mode 100644 index fb772536fa15adf6a77a4f201ec170ca020450b7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16206 zcmd_RWmME}|1PT1-Q9?Q(jd~{0D?$&gGdU}3?iv?iZs$G%>aVHPyzzdAu!V2Al-HD z@%KFcv(H)Q#oo_8Ywf+(e&Ir9hMDio-1qgl>JC#=k;lWP#=djs4xYkuS&ch)?n>Xe zbB`Po9o)if5I7Hh+;!EEm%dXzK)V5cLA8b`L+;$EjK;Y#Lj%8KIX%~Ry>kb@9r^EW zmt&#Doja0C3bGI_50jl491pFP`X@JRfp;F>gFM7&#YX4C5TqSlG-W5-!wh#(o77m= z7*;)%LnYgvG`u_ERigLwMGb>L`LvcgDp~fUd*M&Z3{=oahuJg3(X|+9QvwMO2Cr9r z;u5+IT=hO%jQ4rnawmNqPx<`m9ltB=w8DyY=jfLzPXd2_{-(6bV=)=*flZ!{{;&CU zmewK@6`vjuJ(#7uFgI`zVO>sxW0mo-BedsA1I<`W3`&6qxTLws?M4Hs+GM#jNXTanQ@&AJemk5++UN`uX8o_)G`&A zqf&Z`Qr8qZhaJc8LPIr__a7GYexNsBw$MLFho3-V;vQ%a=w>|#C46%{s};;hl>gX* zwyNAjA9kjf#AItK5s8%}7MaLM6vOrDo&4Z!Sqg;$?Co&j!_OE}ah-El%BIS)@fRb} z3^5}|=o-^`>7(u2ysUDep>G1VIV5+ss~F!*==bB35KW*@=uR)LX%MVo9gtEnR}oba z6pZI}=h5ZwP!NnXEam#j1y_qS8Fwg522VWkionKM?%F-w;$qH$*INGwpTufAY%pbP z@)LZjk|C~7?#s;9dlN#4Yy9HHi)FvH{^3(*=3=iJ@<0&@?$|%q<)7>kA0OegJ{HmU z9};``fUSpHH7%Z`gKcGWnr`IyYfJa7x0`P)^6TG%*>z3?CT8ctVvwtU3uJCs5WY2N26NoEw&tTsC3Mh}hQZo`IS3!>Ak z8MbVg!^aZN(wjTiaw8{y5~_E5$bIw)wk#DdvQaerPwRe6j23CGT%XRt&SV^C8}+UF zQ}SU^xTjPSp4GF(B2rSjZ8$umcCt4!KBqIWadG$QMBSW>o#-4$9-q#<&Fo7TLkQwl z5+D|y>}%OUlAUw(;N>QqYvu1C1Af+(ZiX#-o>x21>i60ojMyFaj=Y(YZpFxIgaWQ5_g=Rf+ zb4sEdgif*>B6idk;BEF(-?Yngt{Up-e4`-&J#58&gLZSxO}Lc2S(AfU*G&{$26?cD zAzQ;KKX~srkn}p;399wI_SF96gySHyzk=I7i3=%AVEo~x;JlA@zWsI3^6kXIQZ&Zs z%39`HPgkbTL%+FdV|Ty(g!mst&qBZd?CXn1n0P7gI)~hm**YTdI}4z`|5?%p9!V`& z%p57rR7cKj#EyoBHq!9c5z24{k-S*H?-TKR+G8nd^R#i}OX}z#*q?D1ba%>hx2L{Z zN0*v38I+kbFdh*et)6yXfBpKkB*^TmRezo?%;eFu-|gkxR+E;d&&82Cl%RJ0NqnPi znO&L=kMe+ILvEVpHw#Na&7_G5>+VCE1B(Mi)1M^_e|Q>}@0hl(HtI(FLrpDSPc%NpNFx8k zwL#cft@G^z#l*tvx>=jq#(G?uCrUE|SIx5BJri{9?s0Z9WvJPrA0~%)o(a`^=Wn-E z>AalmCKl1xAU@S!vavEG5?#;8p$W5c-x@DliPuyaM(B){8468QnupAWM`Mw&7M4G` zxyGY;Qs~2FP_35az}>LT52X?vc>S&a`29Ew$|nuTdUDp+R3jQQJu%~@s(B7kU!!kt zt_$AG!j%tB{CHAp-^AHZ^-4cOTfjOls@NBe-rK32(@9z|(0R{&lqTZZ1K*S?=H`(` zT~OEM{rGmPthOhF!ME_n$E_&TR`b^v-HiNNLWymxfI64VXZ{EtU1m@- zg3x)pHxCgxovxaj=om;B8_O5)zK_P&$;Ov0@b19kcvl`Sk^D(saisfA>QHH7oZg%EIeXshw{?yAGujN;iqVG)BM0j{b(%f?hsgUznjAuM_Tx!Pj*|yeAZWQ1D zK{Cb)l|~{SIzH_>wLkh_RM7FSs5`5}Zpw#oCp~~QvNlK!CQz9AJ~J`g*R9WL`Z*@D z;WA6Lp<(1^G?%+dh?&g(yT&f*XlF{d243;feJo8l{Igayrm5$~1G40)TE`@x%afw5 zX~u0Q6XttZOYih}5ADQ{hGdGLg=Psph%rJ@72}cJ?`H0ks%?OEOjwu6WEhNyHi1ec& z&t=EW|Np<~?#Y1IZXu6bSM2j#`RMX9iPiqpf>YwLX&T2yCd~qsF+*kw6OU|8gX+Q_ zR|cGKl?(_(u)(?isqE{iZ)M^qzs+OQ)49qFYjrO1>BZSEPj=!cOuf~~IrXwmM3j?x zA>%Mp9tNMIm{}6qv!zq8J@)_eX=2g}{IKF6D5{8oLGHS|0l{!5M&B>)q8 ze#YECCVy~Wyi4l#%Jnn9Ww_^#iA(o`D(gY|2{^Rm&D1yLxfd*KZ1QmE))x~38X<@H z%iR{!>+7Yvrh6Iu5Mi4kMkFR$SXpgKj7gkMyOQzPO#2R`iP+M~v($c4dQ9U_$P|i# z_(&&qlu<0|zRljBA{eKf`eZ{QVL;;4sJp-4l9H8Ayo<=j1ONMUy=!|j`My!(^~u!O zMTJ@WB5^QgTl@yK)YV@5BUBu0>`@|=h2WJjT~lp&%4*z9-;38ib}{5!2J3<^SgKKp z{4sTF(aM6ZuDlUjPEt}_3fSb;_i%7rl<+ntilS zM+RN`1rh6+iwqx$`%{H3C7_0G8{o)OW@=LJy$L1Z@)v^0_oWd?&X_y_Z_LH6B2;m1OZXrM7{auf18kIT% zhmcJBKOufJ<{8=2enQg(Q*|&B^qeFvL!Etr{YzJuKwA<6|Ml)DIx(Fl{7R)XVZK+2 z1Tbs2+~JhSF4Px2{nP{ulH#w3?zdjg`Ci)B|AB8k>Ri9$SD}(FT2%QQwkTvf`dn}V z1D8^5_(jL{9)_!!CzA)*d)j>UD64bde<#zRQvV5<66uoNZ&<`yArD5sM|Fy##7u=b z8-zpg;`Gl9hcBzIpE$k%s|=~5t}!%$`qrHr@TIWg(_ngr;3nV0)VXzun|xWq9L9I= z-r3T9s4{CuC*!pd3<_p;hnXCx)hv+x$uWmqmWN>FU>-35&ko z7aIkSoNTf09)?I=wH`lf0H{yKJ&=nF|M|r$Qtj!}VCm3c`p-i-(pd{{{|x7b%o;+2 zDQ?FBdl?m-U&}?qNG{3%?8iX>?A{ZzQv$JHF46acR?14n?_0M^Ihzp(_<(3>qpW26;7q zMmVo;-6wZW73>^69m=WD-aYAi3n5b2{y0pS zCS`)vJ!wpsl#T+TZ~v5X6Wv+0eyPO$7_(w$8y?(tj42sN>MIn zso^4Vpm5II+c}@+M30uAPHx}pP~cF^XTK(K5$Vu?JF*@*`GI=^T*c4J5JNf<6l)t| z!W7)(9tVu^MBX+<3vXq%e!4|9WnJ!cx=HN+ZVS?||8CAF5R@H3HU5-RR5g_kr|nZL zg-P6kiaDS)r=t(r2qUPb%JCiw>A#vsi&B$K`W2W&6&-;J=0D?&nvF5D)6kKe?(dx+ zvnk>EmH|%RWw8z7FeTk;_8YP-qx7p&*dIL_v4gIyraJ2QvDKm#XsT_teW2Q<-1lF* z6D-l^kFLT#J-2m-AtD}68s!6}9Ys%M2-8z_Z)@Gt>R&3q?Doy!z1h>OSPqT*o#D>y z4S(PzuNsML`Bm+`HWj(TK2}+7U}ZHDY-STO@zuJ-jPB_B)%R(f4qa4I$#!q0aNjD-AduENLl#9zK4+q+zF zV>sMhdV3pvdqz-0zZs`0Ic&EgFobE%=`n3ZE1)w_OTQ?tJD#pxeLuG>IN*-pPou010d_VIvqAzVO zMfxn+5~!dTs({3MfDfkAPB@;EU{ug4^`lr2A6KKS*bewbRufVT}?&ZjAG zdf*DzbP$NgC~@FIqUUg0LeA@1E9`J&hD^>#sW49mO4RD;W~=Y4-UucTLqh)unkad+ zingPe1$W~&e&;18!**P8#;?xdh}5C-zl#u2ZEqD-S>nA*MPT!8Q6lcFseiEJ!MDme zJ9N<_Nsm5{D?zN+sDuW|0~cm=mx(;4<)=SPA2WrrNgAx$q>EB`Uqrrc(sbd6-97wR z`h3R1$WfZ`Gy*Xvl`k3nDVD(Cp0E%5V^lt^Js>!W!yZK@E#jSJ?=jEIJLRL(ZZrGP zo54()MxDs;&#uI!nP2Fy$1F6pugd7*k$g<14C_kHA_@2uTAHN6(Dy=inF9I$E=CsS zEk4Y!pPsnV%vJRN2i#ATO^q#zrw2gp-mS{khgP#L2q!@2U7Q{U{XUbfz= zp>y<-D}6~wtiD)dKc(|U=`(YZkxLkonKWsWk&^xd5Vss7PahL4dGUO=Oazswp;Iu|E9SYbTJ}p2U_F@)*pJu-$h7qN6yIhyZ*uFG9QMz4~e%oNboCULavSu2TnSpNG{} z!wQJ7RzcO*ug3=YfT?fP?9Bq&(4?b{C^tS%kpl0mSkvKa;pKkK1H8~7H zzT2K1dzWEf>u$>=<1dKf@YOiGHo)!G(|h{+$Lta&YaL;THdh)&+|wr3;lI~+VB7TU zu*|IqXb8K6Zw@oXh%3PS)zjP7@ZrkIMy~yWp8a%^u0!@Y%javE2)i4w?+OQi^m_r* zmOk~^94+!J5%s=xW_OFoJ%5ULj{NM_SXDE&FPsf><)UKszglgSx!7Pe?|0IaFNK0~eZ9EJkghDNmCDO1?7poB7HISQjJItKLKa){o8R)!7|-4oiTgSg&z{SM z@wu)lA$A$>dJ|yX8{$Ca>!ge(d<RbN$dCe-Zv`@8(MqwIqem?qD`Qz)Vrop^lTH8-fXiD^63S(2?nzsOL!9% zW&zF57SG;^&wJCF`d!yt$Y4MqkPmA(7SnakF2IUtN}$!)*?(FZ{dy=10(4x`&)=2i ze)fScqsP$`?wNU*s+(Mxz1wggc8I>wGo5zoj9Kjy2k8J0FK z*Q=B^ruI7hGiDb?5sgr{5BDPU1+M0!YncXrAPQpI+S+X81>AxT4-Zj@JRM&7h&v#W z)wfe<&O7&%WngoxWNep?+U9iD3+Bo100ii6QMnzWym*U@MevPtWV7d2fdmEx?`SF9>&g`H#0K0^jO za-6XHsFJ~PX=b5>vUnrEerZ>9YL=lcSZDft@zsx;Ty6C}=w{m-!K%f9>(NJl2qUgW z<;u4qV43};DpiL66bvq%sns9>u$`eBfq!_dm@19Dc4|gS^}mMiWHruTNoEW^ zcz&0-*ZiJa#IXI7yB9Z?yBOtA%HA!^vRR(yvm9nzWlaAmUIXQ^7leyR(=C2}9Vl`U zKT8ZfwtrWCPcvD3kl1A|%=Tg^{|5s<`;heUi<%zk<$%@0QQ1l$>So?zj8luslOIu*62Iyw6<(Gwie>y06`36e5=XnPcTm1QBul|^O zgpuqzYO})e0-=K`U53WweRIBMtHh|Wd4Gd%E9fw?yyeET#>lyY?86qn{H2F< z_W3Qa=u7UqeKKeUS5XRxg51o=cfB>BFA8e?*~Yu#aC3Z=Z2A}9UjpamCvXjlIw=@D z*O`3mE6#lA#=f|1_^id3(hY9)r$Bn~zlzia0(ZR^P?D0g`&wFBd9gDh#viA4C%($l zwN`wby4`W~i-R>^y!eYB^tD|Wz|{zVJK)1EZV7I242VdZF&wzxg>UFLKB_t+pN{~L{;!^gxOnev6*g6^11wZeaJYUil z+Vh6-edIi|5+N59AH`r`3+#Bg!)!itWxjLeT#_1RNBG2M=qXtuvrM`gPe4rs{y$G| zGm|DB@F|DlA}?>f!r`XU{l;S~JS0Z=`x#2dN9bX6&0$xG!~YZmHen2K0tS5*&`A@( zr{^?jM5Y_%u%gY6Vt>>SBt&Df^T53N4KtO}ud)aezd2jd(9>fVu=QOj2r4h{~pOpgd@Ce+)b0;vs& zobby(4ZA%@8^4a})wx;&FbIlu%yQDj-)Z2}iRO)y=!;dSbw$yzNnK71<^c*}3v8;( zQIfq@3b?GB*>$baI2Bc-yt?>+-X6rm3Qv*S1e zzEqmJaK54(#>tsslg;y@(WB++K=CuLng3*luyc;lyOo54sks}S=F2}~H9Rr?dwH_T zK+(#b1KPR__J?kg)9^Ej?n?8nVpOyCV7w9uv*D`pEdz8x%-k|dX0#%AR#eCOmBhjf z85=w#_AxjW7hP^Bu{u}9FnnARYT2oZJZs#Q>GBds_p6ILAT`og)abI z^^-sE{$L|vw8TmB=Ep3tND40Ly#NA9H6}~~iK9=G_%ATg@4~6qzoDH6rE==^t}~%x z5YTf1;%}3X_|n0ld@_2g^|95#!NI6IAsRPk?VE|5kKB|bvTrH5Ag-jXvexbzu5Nk%$@1rLzR98lgX|>BJy`{M8UDH|AY=3R1ie*5Q%4J;0ab zQWf71oc!YAA>D>d1S}sr@wQ&4;hdM`mtCCf80Hs{V_y>a*3E#?LB38H_!K=mwPCl} z5So2^asov?IeeSA0Dlo?C8bme_xzeE}ERdBqZA` z)czNp9BjZXE`T=p1A(-guG8|nvxShcC*hIyq!a>~>)cC0J^&g^w(JA=#(4tQiU8% zEO{AJ)!agci61-7j_fV8j~N~kGhwc|E#!?K4v4{e6uuYE06GpU7ux;4FPeSv{R4_N zJ~xypbd5;5x<)B4YwY7LXVC+Xs)eQ>$R+FYk@zDkCCQIC5_v~)h_hIuAdkBB6~>A} zSc+NVz=Y@%9xK;CIYM~UhXr@2Pb^d|fH0>BtnEs7%7)$1qPUdEL;q}y(ftn?^r4%^ z?E8sskkD4WK4FnL*EcU;p_==9#O720Yg}o4D#QQEAD@4`Uut|HGY_ZDqKz$Isx;NF zG+oLB@^h2uKZqG9G4k{CJ)>T={u@pajN-mmbtbtsq!bieg5%ue|2+QyT%;bIshI&a zbL44ELVUc|JLW3)VY9z5i&ch%*+u;4D&#@{f}x_$i-EK4lg|SF!O=1YmGVq^n^Bml zDfMHUEb?M(c{5t$Jr=k2$@+hAI!sGwkihBza}}CYazbdK(vNayAa7U%1T=SLjL|8TeX8q2IToLj*o_ho1W0Eo|s>w zZAbfa+Lu=!Uj5h1`A>tKrtfHGz|mew&^3u6x-YjbL8@v$S*?D2y=8j)2(ZwEFcP*+ zyKUDly0?X*FCog@Emtl+coVE@kTakI6mBkCZVgie?Y^99g-~#3R@|h&oqbn4plj+= zb9GYNlE49L)S&ZzGn%JB0x;81i_3lcHxnSfUz>y9kQ6c^E$Fd0ekm!_-6HI=&tNia zaUGbC6>Hmm*loU48M|1Mx~-r5)(<9!Hr~s%f%K6IGntw{fdQRglvA0xw_CONx$KQt zBc7ryByt%Zr8LOXzmvT=AN12{{8VJ+^b)e=g`8H=O>p;}P0u@>kj4EjhUmJA-~=Qe zemz#409PU}H=T`7NJtopzUlPnkKk!$kD%Zg_mZV^?)pf_f!t2Q;TUMm&u)PWZ7VTc z6G0AdrwsN06kofSoycE92)FiXQ@2H9#$wGCN6>RUJ7oD{3x)vfZ+%dt(Ig!@Z2MFCFR(l# zTnvrcPsL#HR-?(d^Ga*XRDn!SP^O1YR61<(zAqUMAMF zqo+Vn7mTaVjh7krg3-wXCbHJ0G8f-KIEte2i4L)G!|PvP6qY)pj&-&{9`=R$3ADe& zE{(fsH!Zei>0MI^Z8Mitj48hB>St?Ybj=RZk^|r{mF%*lgJ=%^2Tb(X)Km^SG52^J z?z*Q?yPK_+8%2$FEN=JXMQx9OU5UP6P{B z-f@i1dL|O^O4ej9X%yo|DIf+8c5iQX#|${v7tP42RNG{uUD2|i*`OA2$DkHDU`kp8 za1xejEt`r+%y|uEd-2IONvY8w@IyI0`jOPxJPN`gm1?d{p;9Xh8iogN6kf9jriXT? z#xTb|^=13(CkrYU1#h28pL8%A^*SxlpO%5pQ`2)e^Fv(bS7R zL?YsSR$)L8w8iUu`Ze=fX_rZ;+ui@&%YeOaQx0*U)=-E4bg4U(d=O~?MhXlR{tn4) z=vPB|Y5Xc+EtyL0`(5&a{x_hhV;^!cv+G-+B6jakN40t7>m%C8-G*IB=x5G*pkXvo z*Vm_14}|G)hRbiiaeeD!G^tPREahz+mjm$HwbwJ7>pI&&v`iHF{`1~nki**

YE7!QGm2*AOR9rLoC(xx(xibl?(vX*b*r14Ctjl;LT(I(s=@0er#u%`>bqSy@YF z_o#h5{WCd{V(huVAhacX2R$)IFq%tKr9h3R*lSz6w!+xlJB}heB_m^iC)oKzt8S!7 z)=&A5w8LJ#mF^2bgvb`tG|R=UQNY^~>95jws~x})dlFF2&Y2ncM~wo^*0zXoq`cX= z!qOEo>LNS}p3JgB&SuFe>L+gsu~lLg6vDuq%K_Sm#6YO@@H-Ke?Q$uY&&M9JA5CnW z^}(m(eiJy}zty2U)<7|PGC6$W zb@B!&ab{mcnsBptE)YL%0B=FqdDeEf4v4I z&dt1?GgcS+4XhOL<55lEt39bnx5{nD8;$nRyH+SrP>FwYFRYAzX>x5pi1#}cLdj!y(0VAx zY4)4%;ef@r++Ob<0AaIf$cEochm(y9DKfsjxn2pTaZp~8$cWdVm)C}gQe^~f0np)m;|BMGXuqrbyh}!qKt2{; zb0bo^(cT>nh6SJ`G|PCzF^YrBP9nY0NgDLyIc48-F_AJkSowK)0G0wsjcJ2`Wyd zid%K^uN~rpz_8-8UZgNy0TZ(tAwoMtvWGm@}EeOEV0V!A|_%gi1Y0LqI(Gs{1v(l9M0mKhWDvS8fjcX5W> zaZWP4{-}S=6iy-xxJhQ5Xm|_rbUqZ9etR}bdg5#Lp&V!ADQxM=T$4P zk#vN@@tY~%Xx-FC14fVOqNfK;BoBv_n4ah zPU1G4^URjcbWh-4qiRwm@4rUXC)<#KPC&&_G}(k8r8A&JmJg&_mD`5KXF%dI3C6RP~1HNCm%G4%^C1(|jm@6QmdjUYY>foNG zcDU^h8C-Sn_-2nk^h|@E2BYwF=5}*`z>{`UkVYBer0lvINtAObUC9^6j)70BmZY@- zdULuMrnP@~IqC7V7Mv$@(Ehngzv*D6OGzv$Rj(F}Q7{ zL8m`nePSr^Nv_mR!}q5U?);wNeW2i@=+dT->0w&=IZtM0=#bZO2SoN?;CZ8Mj7sX1 z{NIb{&xY;@CPBw%bZk0K6kWrEZcThG*q&*q?RjVvZxFjFL*f6Z zaE*u+Wqg1kLw;TLh4=bBB6WKZoB#JaLe6Ri2E_<=oqtn7S3K!Pb%x#U+@ER%poPEA zg|vdwr5vES>quv)rr0Qe&@^XYQ4Rwt-Gn>N?fLi4rMoOuAZ{F7OF|k#XMLiaoE2iD zXveKxvtrQ$rN$QEQ45>FiTHM_cr}oyG^ne~H6?eh@Abu|9i9L`{}DaE(iWPkv`}ff z5b(HcHpjCdC^c&N&!O#IPCcprjRx&N$)yNqdGOPrMYOHyirR6fT#^+alF1f)rGo@<3annyN7GD zoT!ByN)s(&Ng<&W+-;5k{*W8FDXEQ0%C|9+Mo0yu%1mN!|7h{7Pfr_dOxBbj2{^6{ zfv|@X(D@RBYCM{?+uLtG^O>W!^FBk9#8kd>0+LUB2FcP>4Hl5dY1~Cp9kNhB0Ar^d zTa1xe^XrMK{AqA1h39OIt+B_}dGsfv!$4pUX(0DO4 z@mN4>N@8w}pipENaR1fp;{{b10}WqAm^MgtGvIHKo>JZ@_?sVb#5^{%MPbApsEqi1 z@t(1^Re`nxkprER^yC(XTub#aOwvpZ< z&x1jp^%C?!gq|Z$BdH6S4~CMxA)35X&@(SC9LQ)D8$j$2dGhK9CiKfa@<8ThZ+fVV zVvGhs0B^^_^1OA%JGsek_9ZGf&Z!Dt_ErZoVg7u;+zZC1>v84P1QACN6DkAfxITRJ zC>E6TyN;^~!)H%+rp7EYC4EL8=Dy_`#>noUY84y_m z%-k6n3Y?NLfc2N7f(dEtCs`_egZJf2o-Gl&Hlbl)w?CjkT+)8OnOT?*zWV2rGTdC3Hdc{u2Khjw2REf$;V`?u&JaC8I~Gzg8Qa7<%S<}bC26;1E*VZIJqMV ze>2dNs?_-C@130}&n+H2c#tv3gJAqYx7Tm1r}y%0ATNfIPT_qaCg#4g1@h9rSGU&Zp-O#qgxmDY zA-wt8NHW#!^K9|*Uyp&ZbTw`msNjn%6^~)+f1ug~YBy5GAj6A|r@G4?C3ZSus12M3 zVRH%z@rRX^|9mpiTkf0h)jgw_MQUz$j|{ZSAwTcC9I3Ib#l_Y{AnB;hOiJ3EE=--J zpBruf)9$b~rp?RG-w+HZvs)wBA~*e?PFeRCJNUiNoSL+eYTFWc$Yzc*vZQ5sZ-{1r z^H}9~>-*PzkTG#+I@Zzu_U5yw(@Xt2r|jkB<#oF-%vf$rAmO+sCMK%Dfv%>IMx-%; zG-;2!2QgcLCNG{uS^{RzmkE;@W#9hh1SgC>nciL?$flOMq7k*kNKOg&UI?NXeNZnm zbPQ0i`FxdzO4un&Ddn+(L(8>066h{m@{m9@7aMBS(!tHap@>A_+Rb3sMgpcfP_mi; z$66*qIjeDLLdD-Uz z#1NUk)l?v3TNU8X2S_<>f8gFpW&PaO*SCf+5Y}>IVC+&Ac4a%WCi;3fH***5lpywd zHD>w{og4)|)B5ALe~iciLCgxwJ*LnyYH-lga9f81R}hy@R0|28;Pe;5izEPST;dsK zC94k(M4~<*YAzPr4&;LY!?bvdUDSpM4tXjK0)VjxF+2CD0)Zh^ypB&pDf9z}2BGc- z&Z86}LfO;uM0{2o4+r9QrfRpi!sV>2zL?w(hdClR6Q|b-E=`UiiJ@VqJ%aBEKZ$z;zqC6cAy7+N$mT>La3*D8FemftYI=@e-E>$d~tg1s?o$s99 z*Z%%|h&9ufDl`cFR{agt`8z`a6&V>>_a`Qqql_2A!_)->b?FA02x0w20yf1f#guMh zHl^4sw6C7WwoUS*5fopuO%!uXry=jLh|)OLPw-=12SmCKN2!@x97oTMZbF!xSWqVV6V zmyx-rATLlfGc)U`Pl`{8eO1M>ErS{8rbLr*s`bIxSve~PHJXD)cs-s!8|acsxr zK>?!a!p_o;mF|@A$iMeTt`+S-c@`V@uXD$H^547 zowU=0UB4K&aCy07dSoDCVcKLqr`U9HDVdnFI^p@Y9`MTJVHrJ-KzF2y1(vGz$A2+O z6Q~&6yovU<`AYG!+ilk#O`Gp{c=QL~7 z6CP5FmlEDFgM%k!9Ev|Ks&jY%B?1 z#^TJ4gMU`0fpgKn5dS*1op-y10kvk%u?U(lQ;MYJF~q<1u6R8A?K!nyqteKWUbdAH z6|tS_1IAG{n?#FirEpOb)&~XYTx Segmentation: + """Combine the previous segmentation with the new segmentation of the differences. - combined_image = previous_segmentation.image - combined_masked_images = previous_segmentation.masked_images + new_masked_images - combined_descriptions = previous_segmentation.descriptions + new_descriptions + Args: + new_image: The new image which includes the changes. + previous_segmentation: The previous segmentation containing unchanged segments. + new_descriptions: Descriptions of the new segments from the difference image. + new_masked_images: Masked images of the new segments from the difference image. + new_masks: masks of the new segments. + + Returns: + Segmentation: A new segmentation combining both previous and new segments. + """ + def masks_overlap(mask1, mask2): + """Check if two masks overlap.""" + return np.any(np.logical_and(mask1, mask2)) - previous_bounding_boxes, previous_centroids = vision.calculate_bounding_boxes(previous_segmentation.masks) + # Calculate the bounding boxes and centroids for the new segments new_bounding_boxes, new_centroids = vision.calculate_bounding_boxes(new_masks) - combined_bounding_boxes = previous_bounding_boxes + new_bounding_boxes - combined_centroids = previous_centroids + new_centroids + # Filter out overlapping previous segments + filtered_previous_masked_images = [] + filtered_previous_descriptions = [] + filtered_previous_bounding_boxes = [] + filtered_previous_centroids = [] + for idx, prev_mask in enumerate(previous_segmentation.masks): + if not any(masks_overlap(prev_mask, new_mask) for new_mask in new_masks): + filtered_previous_masked_images.append(previous_segmentation.masked_images[idx]) + filtered_previous_descriptions.append(previous_segmentation.descriptions[idx]) + filtered_previous_bounding_boxes.append(previous_segmentation.bounding_boxes[idx]) + filtered_previous_centroids.append(previous_segmentation.centroids[idx]) + + # Combine filtered previous segments with new segments + combined_masked_images = filtered_previous_masked_images + new_masked_images + combined_descriptions = filtered_previous_descriptions + new_descriptions + combined_bounding_boxes = filtered_previous_bounding_boxes + new_bounding_boxes + combined_centroids = filtered_previous_centroids + new_centroids return Segmentation( - image=combined_image, + image=new_image, masked_images=combined_masked_images, descriptions=combined_descriptions, bounding_boxes=combined_bounding_boxes, @@ -412,20 +433,21 @@ def get_window_segmentation( # TODO XXX: create copy of similar_segmentation, but overwrite with segments of # regions of new image where segments of similar_segmentation overlap non-zero # regions of similar_segmentation_diff - new_image = vision.extract_difference_image( + difference_image = vision.extract_difference_image( original_image, similar_segmentation.image, tolerance=0.05, ) - new_masks = vision.get_masks_from_segmented_image(new_image) - new_masked_images = vision.extract_masked_images(new_image, new_masks) + new_masks = vision.get_masks_from_segmented_image(difference_image) + new_masked_images = vision.extract_masked_images(difference_image, new_masks) new_descriptions = prompt_for_descriptions( - new_image, + difference_image, new_masked_images, action_event.active_segment_description, exceptions, ) updated_segmentation = combine_segmentations( + difference_image, similar_segmentation, new_descriptions, new_masked_images, diff --git a/openadapt/vision.py b/openadapt/vision.py index 0023c1125..3824203b0 100644 --- a/openadapt/vision.py +++ b/openadapt/vision.py @@ -137,23 +137,19 @@ def extract_difference_image( new_image_np = np.array(new_image.convert('L')) old_image_np = np.array(old_image.convert('L')) - # Compute the SSIM between the two images - score, diff = ssim(new_image_np, old_image_np, full=True) - diff = (diff * 255).astype("uint8") + # Compute the absolute difference between the two images + diff = np.abs(new_image_np - old_image_np) - # Threshold the difference image to get the regions that are different - thresh = cv2.threshold(diff, 255 * (1 - tolerance), 255, cv2.THRESH_BINARY_INV)[1] + # Create a mask for the regions where the difference is above the tolerance + mask = diff > (255 * tolerance) - # Find contours of the different regions - contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + # Initialize an array for the difference image + diff_image_np = np.zeros_like(new_image_np) - # Create a mask of the differences - mask = np.zeros_like(new_image_np) - cv2.drawContours(mask, contours, -1, (255), thickness=cv2.FILLED) - - # Apply the mask to the new image to extract the different regions - diff_image_np = cv2.bitwise_and(np.array(new_image), np.array(new_image), mask=mask) + # Set the pixels that are different in the new image + diff_image_np[mask] = new_image_np[mask] + # Convert the numpy array back to an image return Image.fromarray(diff_image_np) @cache.cache() @@ -322,6 +318,7 @@ def calculate_bounding_boxes( return bounding_boxes, centroids + def get_image_similarity( im1: Image.Image, im2: Image.Image, From 65c876f2dfe2063e70e180561c347e036c2da931 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Sat, 22 Jun 2024 02:33:19 +0530 Subject: [PATCH 04/10] WIP --- openadapt/strategies/visual.py | 8 ++++++++ openadapt/vision.py | 27 +-------------------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/openadapt/strategies/visual.py b/openadapt/strategies/visual.py index c6d5dc9ba..07e27c60a 100644 --- a/openadapt/strategies/visual.py +++ b/openadapt/strategies/visual.py @@ -47,6 +47,7 @@ from dataclasses import dataclass from pprint import pformat import time +from turtle import title from loguru import logger from PIL import Image, ImageDraw import numpy as np @@ -585,3 +586,10 @@ def prompt_for_descriptions( # remove indexes descriptions = [desc for idx, desc in descriptions] return descriptions + +#Example usage for visualizing +image_1 = Image.open("./winCalOld.png") +image_2 = Image.open("./winCalNew.png") + +difference_image = vision.extract_difference_image(image_1, image_2, tolerance=0.05) +difference_image.show(title="difference Image") \ No newline at end of file diff --git a/openadapt/vision.py b/openadapt/vision.py index 3824203b0..78c6be4f7 100644 --- a/openadapt/vision.py +++ b/openadapt/vision.py @@ -127,30 +127,6 @@ def refine_masks(masks: list[np.ndarray]) -> list[np.ndarray]: logger.info(f"{len(refined_masks)=}") return refined_masks -@cache.cache() -def extract_difference_image( - new_image: Image.Image, - old_image: Image.Image, - tolerance: float = 0.05, -) -> Image.Image: - """Extract the portion of the new image that is different from the old image.""" - new_image_np = np.array(new_image.convert('L')) - old_image_np = np.array(old_image.convert('L')) - - # Compute the absolute difference between the two images - diff = np.abs(new_image_np - old_image_np) - - # Create a mask for the regions where the difference is above the tolerance - mask = diff > (255 * tolerance) - - # Initialize an array for the difference image - diff_image_np = np.zeros_like(new_image_np) - - # Set the pixels that are different in the new image - diff_image_np[mask] = new_image_np[mask] - - # Convert the numpy array back to an image - return Image.fromarray(diff_image_np) @cache.cache() def filter_thin_ragged_masks( @@ -318,7 +294,6 @@ def calculate_bounding_boxes( return bounding_boxes, centroids - def get_image_similarity( im1: Image.Image, im2: Image.Image, @@ -537,4 +512,4 @@ def filter_ui_components( ) filtered_masks.append(contour_mask) return filtered_masks -""" +""" \ No newline at end of file From 383002a63811c68734b83372e3befe0db23f9ba8 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Mon, 24 Jun 2024 03:49:05 +0530 Subject: [PATCH 05/10] testing visual strategy --- openadapt/strategies/visual.py | 167 +++++++++++++++++++-------------- openadapt/vision.py | 37 +++++++- 2 files changed, 130 insertions(+), 74 deletions(-) diff --git a/openadapt/strategies/visual.py b/openadapt/strategies/visual.py index 07e27c60a..4d5fa16d7 100644 --- a/openadapt/strategies/visual.py +++ b/openadapt/strategies/visual.py @@ -47,11 +47,20 @@ from dataclasses import dataclass from pprint import pformat import time -from turtle import title + from loguru import logger from PIL import Image, ImageDraw import numpy as np -from openadapt import adapters, common, models, plotting, strategies, utils, vision + +from openadapt import ( + adapters, + common, + models, + plotting, + strategies, + utils, + vision, +) DEBUG = False DEBUG_REPLAY = False @@ -67,6 +76,7 @@ class Segmentation: Attributes: image: The original image used to generate segments. + marked_image: The marked image (for Set-of-Mark prompting). masked_images: A list of PIL Image objects that have been masked based on segmentation. descriptions: Descriptions of each segmented region, correlating with each @@ -80,6 +90,7 @@ class Segmentation: """ image: Image.Image + marked_image: Image.Image masked_images: list[Image.Image] descriptions: list[str] bounding_boxes: list[dict[str, float]] # "top", "left", "height", "width" @@ -111,6 +122,7 @@ def add_active_segment_descriptions(action_events: list[models.ActionEvent]) -> def apply_replay_instructions( action_events: list[models.ActionEvent], replay_instructions: str, + # retain_window_events: bool = False, ) -> None: """Modify the given ActionEvents according to the given replay instructions. @@ -131,7 +143,7 @@ def apply_replay_instructions( prompt_adapter = adapters.get_default_prompt_adapter() content = prompt_adapter.prompt( prompt, - system_prompt, + system_prompt=system_prompt, ) content_dict = utils.parse_code_snippet(content) try: @@ -166,6 +178,7 @@ def __init__( """ super().__init__(recording) self.recording_action_idx = 0 + self.action_history = [] add_active_segment_descriptions(recording.processed_action_events) self.modified_actions = apply_replay_instructions( recording.processed_action_events, @@ -234,8 +247,16 @@ def get_next_action_event( target_mouse_y = target_centroid[1] / height_ratio + active_window.top modified_reference_action.mouse_x = target_mouse_x modified_reference_action.mouse_y = target_mouse_y + self.action_history.append(modified_reference_action) return modified_reference_action + def __del__(self) -> None: + """Log the action history.""" + action_history_dicts = [ + action.to_prompt_dict() for action in self.action_history + ] + logger.info(f"action_history=\n{pformat(action_history_dicts)}") + def get_active_segment( action: models.ActionEvent, @@ -352,26 +373,25 @@ def find_similar_image_segmentation( return similar_segmentation, similar_segmentation_diff - + def combine_segmentations( new_image: Image.Image, previous_segmentation: Segmentation, new_descriptions: list[str], new_masked_images: list[Image.Image], - new_masks: list[np.ndarray] + new_masks: list[np.ndarray], ) -> Segmentation: """Combine the previous segmentation with the new segmentation of the differences. - Args: new_image: The new image which includes the changes. previous_segmentation: The previous segmentation containing unchanged segments. new_descriptions: Descriptions of the new segments from the difference image. new_masked_images: Masked images of the new segments from the difference image. new_masks: masks of the new segments. - Returns: Segmentation: A new segmentation combining both previous and new segments. """ + def masks_overlap(mask1, mask2): """Check if two masks overlap.""" return np.any(np.logical_and(mask1, mask2)) @@ -386,9 +406,15 @@ def masks_overlap(mask1, mask2): filtered_previous_centroids = [] for idx, prev_mask in enumerate(previous_segmentation.masks): if not any(masks_overlap(prev_mask, new_mask) for new_mask in new_masks): - filtered_previous_masked_images.append(previous_segmentation.masked_images[idx]) - filtered_previous_descriptions.append(previous_segmentation.descriptions[idx]) - filtered_previous_bounding_boxes.append(previous_segmentation.bounding_boxes[idx]) + filtered_previous_masked_images.append( + previous_segmentation.masked_images[idx] + ) + filtered_previous_descriptions.append( + previous_segmentation.descriptions[idx] + ) + filtered_previous_bounding_boxes.append( + previous_segmentation.bounding_boxes[idx] + ) filtered_previous_centroids.append(previous_segmentation.centroids[idx]) # Combine filtered previous segments with new segments @@ -402,7 +428,7 @@ def masks_overlap(mask1, mask2): masked_images=combined_masked_images, descriptions=combined_descriptions, bounding_boxes=combined_bounding_boxes, - centroids=combined_centroids + centroids=combined_centroids, ) @@ -493,8 +519,13 @@ def get_window_segmentation( len(descriptions), len(centroids), ) + marked_image = plotting.get_marked_image( + original_image, + refined_masks, # masks, + ) segmentation = Segmentation( original_image, + marked_image, masked_images, descriptions, bounding_boxes, @@ -524,72 +555,62 @@ def prompt_for_descriptions( Returns: list of descriptions for each masked image. """ - prompt_adapter = adapters.get_default_prompt_adapter() + # TODO: move inside adapters.prompt + for driver in adapters.prompt.DRIVER_ORDER: + # off by one to account for original image + if driver.MAX_IMAGES and (len(masked_images) + 1 > driver.MAX_IMAGES): + masked_images_batches = utils.split_list( + masked_images, + driver.MAX_IMAGES - 1, + ) + descriptions = [] + for masked_images_batch in masked_images_batches: + descriptions_batch = prompt_for_descriptions( + original_image, + masked_images_batch, + active_segment_description, + exceptions, + ) + descriptions += descriptions_batch + return descriptions - # TODO: move inside adapters - # off by one to account for original image - if prompt_adapter.MAX_IMAGES and ( - len(masked_images) + 1 > prompt_adapter.MAX_IMAGES - ): - masked_images_batches = utils.split_list( - masked_images, - prompt_adapter.MAX_IMAGES - 1, + images = [original_image] + masked_images + system_prompt = utils.render_template_from_file( + "prompts/system.j2", ) - descriptions = [] - for masked_images_batch in masked_images_batches: - descriptions_batch = prompt_for_descriptions( + logger.info(f"system_prompt=\n{system_prompt}") + num_segments = len(masked_images) + prompt = utils.render_template_from_file( + "prompts/description.j2", + active_segment_description=active_segment_description, + num_segments=num_segments, + exceptions=exceptions, + ).strip() + logger.info(f"prompt=\n{prompt}") + logger.info(f"{len(images)=}") + descriptions_json = driver.prompt( + prompt, + system_prompt, + images, + ) + descriptions = utils.parse_code_snippet(descriptions_json)["descriptions"] + logger.info(f"{descriptions=}") + try: + assert len(descriptions) == len(masked_images), ( + len(descriptions), + len(masked_images), + ) + except Exception as exc: + exceptions = exceptions or [] + exceptions.append(exc) + logger.info(f"exceptions=\n{pformat(exceptions)}") + return prompt_for_descriptions( original_image, - masked_images_batch, + masked_images, active_segment_description, exceptions, ) - descriptions += descriptions_batch - return descriptions - - images = [original_image] + masked_images - system_prompt = utils.render_template_from_file( - "prompts/system.j2", - ) - logger.info(f"system_prompt=\n{system_prompt}") - num_segments = len(masked_images) - prompt = utils.render_template_from_file( - "prompts/description.j2", - active_segment_description=active_segment_description, - num_segments=num_segments, - exceptions=exceptions, - ) - logger.info(f"prompt=\n{prompt}") - logger.info(f"{len(images)=}") - descriptions_json = prompt_adapter.prompt( - prompt, - system_prompt, - images, - ) - descriptions = utils.parse_code_snippet(descriptions_json)["descriptions"] - logger.info(f"{descriptions=}") - try: - assert len(descriptions) == len(masked_images), ( - len(descriptions), - len(masked_images), - ) - except Exception as exc: - exceptions = exceptions or [] - exceptions.append(exc) - logger.info(f"exceptions=\n{pformat(exceptions)}") - return prompt_for_descriptions( - original_image, - masked_images, - active_segment_description, - exceptions, - ) - - # remove indexes - descriptions = [desc for idx, desc in descriptions] - return descriptions -#Example usage for visualizing -image_1 = Image.open("./winCalOld.png") -image_2 = Image.open("./winCalNew.png") - -difference_image = vision.extract_difference_image(image_1, image_2, tolerance=0.05) -difference_image.show(title="difference Image") \ No newline at end of file + # remove indexes + descriptions = [desc for idx, desc in descriptions] + return descriptions diff --git a/openadapt/vision.py b/openadapt/vision.py index 78c6be4f7..8329f4bb8 100644 --- a/openadapt/vision.py +++ b/openadapt/vision.py @@ -56,6 +56,41 @@ def get_masks_from_segmented_image( return masks +@cache.cache() +def extract_difference_image( + new_image: Image.Image, + old_image: Image.Image, + tolerance: float = 0.05, +) -> Image.Image: + """Extract the portion of the new image that is different from the old image. + + Args: + new_image: The new image as a PIL Image object. + old_image: The old image as a PIL Image object. + tolerance: Tolerance level to consider a pixel as different (default is 0.05). + + Returns: + A PIL Image object representing the difference image. + """ + new_image_np = np.array(new_image) + old_image_np = np.array(old_image) + + # Compute the absolute difference between the two images in each color channel + diff = np.abs(new_image_np - old_image_np) + + # Create a mask for the regions where the difference is above the tolerance + mask = np.any(diff > (255 * tolerance), axis=-1) + + # Initialize an array for the segmented image + segmented_image_np = np.zeros_like(new_image_np) + + # Set the pixels that are different in the new image + segmented_image_np[mask] = new_image_np[mask] + + # Convert the numpy array back to an image + return Image.fromarray(segmented_image_np) + + @cache.cache() def filter_masks_by_size( masks: list[np.ndarray], @@ -512,4 +547,4 @@ def filter_ui_components( ) filtered_masks.append(contour_mask) return filtered_masks -""" \ No newline at end of file +""" From ca30b158262e122539f3ee18c6d0743606d96635 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Thu, 27 Jun 2024 05:03:13 +0530 Subject: [PATCH 06/10] performance test using naivereplaystrategy --- openadapt/app/dashboard/api/recordings.py | 2 +- openadapt/app/tray.py | 2 +- openadapt/config.py | 11 +++++--- openadapt/db/crud.py | 26 ++++++++++++++++--- openadapt/models.py | 31 ++++++++++++++--------- openadapt/plotting.py | 8 +++--- openadapt/scripts/reset_db.py | 4 +-- openadapt/strategies/naive.py | 2 +- openadapt/vision.py | 16 ++++++++++++ 9 files changed, 74 insertions(+), 28 deletions(-) diff --git a/openadapt/app/dashboard/api/recordings.py b/openadapt/app/dashboard/api/recordings.py index 32b13debf..74a8c8252 100644 --- a/openadapt/app/dashboard/api/recordings.py +++ b/openadapt/app/dashboard/api/recordings.py @@ -36,7 +36,7 @@ def attach_routes(self) -> APIRouter: def get_recordings() -> dict[str, list[Recording]]: """Get all recordings.""" session = crud.get_new_session(read_only=True) - recordings = crud.get_all_recordings(session) + recordings = crud.get_recordings(session) return {"recordings": recordings} @staticmethod diff --git a/openadapt/app/tray.py b/openadapt/app/tray.py index 3aa03ab06..3d62bf21e 100644 --- a/openadapt/app/tray.py +++ b/openadapt/app/tray.py @@ -442,7 +442,7 @@ def populate_menu(self, menu: QMenu, action: Callable, action_type: str) -> None action_type (str): The type of action to perform ["visualize", "replay"] """ session = crud.get_new_session(read_only=True) - recordings = crud.get_all_recordings(session) + recordings = crud.get_recordings(session) self.recording_actions[action_type] = [] diff --git a/openadapt/config.py b/openadapt/config.py index 742aeaa88..6215397dd 100644 --- a/openadapt/config.py +++ b/openadapt/config.py @@ -1,6 +1,5 @@ """Configuration module for OpenAdapt.""" - from enum import Enum from typing import Any, ClassVar, Type, Union import json @@ -33,6 +32,7 @@ CAPTURE_DIR_PATH = (DATA_DIR_PATH / "captures").absolute() VIDEO_DIR_PATH = DATA_DIR_PATH / "videos" DATABASE_LOCK_FILE_PATH = DATA_DIR_PATH / "openadapt.db.lock" +DB_FILE_PATH = (DATA_DIR_PATH / "openadapt.db").absolute() STOP_STRS = [ "oa.stop", @@ -124,7 +124,8 @@ class SegmentationAdapter(str, Enum): # Database DB_ECHO: bool = False - DB_URL: ClassVar[str] = f"sqlite:///{(DATA_DIR_PATH / 'openadapt.db').absolute()}" + DB_FILE_PATH: str = str(DB_FILE_PATH) + DB_URL: ClassVar[str] = f"sqlite:///{DB_FILE_PATH}" # Error reporting ERROR_REPORTING_ENABLED: bool = True @@ -428,11 +429,13 @@ def show_alert() -> None: """Show an alert to the user.""" msg = QMessageBox() msg.setIcon(QMessageBox.Warning) - msg.setText(""" + msg.setText( + """ An error has occurred. The development team has been notified. Please join the discord server to get help or send an email to help@openadapt.ai - """) + """ + ) discord_button = QPushButton("Join the discord server") discord_button.clicked.connect( lambda: webbrowser.open("https://discord.gg/yF527cQbDG") diff --git a/openadapt/db/crud.py b/openadapt/db/crud.py index 96de81333..9b0285923 100644 --- a/openadapt/db/crud.py +++ b/openadapt/db/crud.py @@ -279,22 +279,27 @@ def delete_recording(session: SaSession, recording: Recording) -> None: delete_video_file(recording_timestamp) -def get_all_recordings(session: SaSession) -> list[Recording]: +def get_recordings(session: SaSession, max_rows=None) -> list[Recording]: """Get all recordings. Args: session (sa.orm.Session): The database session. + max_rows: The number of recordings to return, starting from the most recent. + Defaults to all if max_rows is not specified. Returns: list[Recording]: A list of all original recordings. """ - return ( + query = ( session.query(Recording) .filter(Recording.original_recording_id == None) # noqa: E711 .order_by(sa.desc(Recording.timestamp)) - .all() ) + if max_rows: + query = query.limit(max_rows) + return query.all() + def get_all_scrubbed_recordings( session: SaSession, @@ -350,6 +355,21 @@ def get_recording(session: SaSession, timestamp: float) -> Recording: return session.query(Recording).filter(Recording.timestamp == timestamp).first() +def get_recordings_by_desc(session: SaSession, description_str: str) -> list[Recording]: + """Get recordings by task description. + Args: + session (sa.orm.Session): The database session. + task_description (str): The task description to search for. + Returns: + list[Recording]: A list of recordings whose task descriptions contain the given string. + """ + return ( + session.query(Recording) + .filter(Recording.task_description.contains(description_str)) + .all() + ) + + BaseModelType = TypeVar("BaseModelType") diff --git a/openadapt/models.py b/openadapt/models.py index 76d424425..ff897e954 100644 --- a/openadapt/models.py +++ b/openadapt/models.py @@ -559,19 +559,26 @@ def to_prompt_dict(self, include_data: bool = True) -> dict[str, Any]: "You can help by uncommenting the lines below and pasting " "the contents of the window_dict into a new GitHub Issue." ) - # from pprint import pformat - # logger.info(f"window_dict=\n{pformat(window_dict)}") - # import ipdb; ipdb.set_trace() - window_state = window_dict["state"] - window_state["data"] = utils.clean_dict( - utils.filter_keys( - window_state["data"], - key_suffixes, + from pprint import pformat + + logger.info(f"window_dict=\n{pformat(window_dict)}") + import ipdb + + ipdb.set_trace() + if "state" in window_dict and "data" in window_dict["state"]: + window_state = window_dict["state"] + window_state["data"] = utils.clean_dict( + utils.filter_keys( + window_state["data"], + key_suffixes, + ) ) - ) - else: - window_dict["state"].pop("data") - window_dict["state"].pop("meta") + if "state" in window_dict: + if "data" in window_dict["state"]: + window_dict["state"].pop("data") + if "meta" in window_dict["state"]: + window_dict["state"].pop("meta") + return window_dict diff --git a/openadapt/plotting.py b/openadapt/plotting.py index f476c3b82..f7fd306d6 100644 --- a/openadapt/plotting.py +++ b/openadapt/plotting.py @@ -799,6 +799,7 @@ def get_marked_image( label_mode = "1" alpha = 0.1 anno_mode = [] + if include_masks: anno_mode.append("Mask") if include_marks: @@ -814,7 +815,6 @@ def get_marked_image( ) mask_map[mask == 1] = label - im = demo.get_image() - marked_image = Image.fromarray(im) - - return marked_image + im = demo.get_image() + marked_image = Image.fromarray(im) + return marked_image diff --git a/openadapt/scripts/reset_db.py b/openadapt/scripts/reset_db.py index 8bba91be2..a0e39fb08 100644 --- a/openadapt/scripts/reset_db.py +++ b/openadapt/scripts/reset_db.py @@ -14,8 +14,8 @@ def reset_db() -> None: """Clears the database by removing the db file and running a db migration.""" - if os.path.exists(config.DB_FPATH): - os.remove(config.DB_FPATH) + if os.path.exists(config.DB_FILE_PATH): + os.remove(config.DB_FILE_PATH) # Prevents duplicate logging of config values by piping stderr # and filtering the output. diff --git a/openadapt/strategies/naive.py b/openadapt/strategies/naive.py index 9a0aba170..67bb075b7 100644 --- a/openadapt/strategies/naive.py +++ b/openadapt/strategies/naive.py @@ -94,7 +94,7 @@ def get_next_action_event( # (fixed by disabling remove_move_before_click in events.py) # if action_event.name in common.MOUSE_CLICK_EVENTS: # time.sleep(self.double_click_interval_seconds + 0.01) - + logger.info(f"{action_event}") return action_event else: return None diff --git a/openadapt/vision.py b/openadapt/vision.py index 8329f4bb8..7c8a7026b 100644 --- a/openadapt/vision.py +++ b/openadapt/vision.py @@ -56,6 +56,22 @@ def get_masks_from_segmented_image( return masks +def extract_masked_image(image: Image.Image, mask: np.ndarray) -> Image.Image: + # Ensure the mask is in the correct format + mask = mask.astype(np.uint8) * 255 + + # Create a blank (transparent) image + masked_image = Image.new("RGBA", image.size) + + # Convert the mask to an image + mask_image = Image.fromarray(mask, mode="L") + + # Composite the original image and the blank image using the mask + masked_image = Image.composite(image.convert("RGBA"), masked_image, mask_image) + + return masked_image + + @cache.cache() def extract_difference_image( new_image: Image.Image, From 8450cea2852160537093d55c0dbb08b3088636ae Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Fri, 12 Jul 2024 23:22:39 +0530 Subject: [PATCH 07/10] feat: performance test --- openadapt/scripts/generate_db_fixtures.py | 132 ++++++++++++++++++++++ openadapt/window/__init__.py | 12 ++ tests/openadapt/test_performance.py | 88 +++++++++++++++ tests/openadapt/test_results.txt | 3 + 4 files changed, 235 insertions(+) create mode 100644 openadapt/scripts/generate_db_fixtures.py create mode 100644 tests/openadapt/test_performance.py create mode 100644 tests/openadapt/test_results.txt diff --git a/openadapt/scripts/generate_db_fixtures.py b/openadapt/scripts/generate_db_fixtures.py new file mode 100644 index 000000000..7cd20bd51 --- /dev/null +++ b/openadapt/scripts/generate_db_fixtures.py @@ -0,0 +1,132 @@ +import os +from sqlalchemy import create_engine, inspect +from sqlalchemy.orm import sessionmaker +from openadapt.db.db import Base +from openadapt.config import DATA_DIR_PATH, PARENT_DIR_PATH, RECORDING_DIR_PATH +import openadapt.db.crud as crud +from loguru import logger + + +def get_session(): + db_url = RECORDING_DIR_PATH / "recording.db" + print(f"Database URL: {db_url}") + engine = create_engine(f"sqlite:///{db_url}") + # SessionLocal = sessionmaker(bind=engine) + Base.metadata.create_all(bind=engine) + session = crud.get_new_session(read_only=True) + print("Database connection established.") + return session, engine + + +def check_tables_exist(engine): + inspector = inspect(engine) + tables = inspector.get_table_names() + expected_tables = [ + "recording", + "action_event", + "screenshot", + "window_event", + "performance_stat", + "memory_stat", + ] + for table_name in expected_tables: + table_exists = table_name in tables + logger.info(f"{table_name=} {table_exists=}") + return tables + + +def fetch_data(session): + # get the most recent three recordings + recordings = crud.get_recordings(session, max_rows=3) + recording_ids = [recording.id for recording in recordings] + + action_events = [] + screenshots = [] + window_events = [] + performance_stats = [] + memory_stats = [] + + for recording in recordings: + action_events.extend(crud.get_action_events(session, recording)) + screenshots.extend(crud.get_screenshots(session, recording)) + window_events.extend(crud.get_window_events(session, recording)) + performance_stats.extend(crud.get_perf_stats(session, recording)) + memory_stats.extend(crud.get_memory_stats(session, recording)) + + data = { + "recordings": recordings, + "action_events": action_events, + "screenshots": screenshots, + "window_events": window_events, + "performance_stats": performance_stats, + "memory_stats": memory_stats, + } + + # Debug prints to verify data fetching + print(f"Recordings: {len(data['recordings'])} found.") + print(f"Action Events: {len(data['action_events'])} found.") + print(f"Screenshots: {len(data['screenshots'])} found.") + print(f"Window Events: {len(data['window_events'])} found.") + print(f"Performance Stats: {len(data['performance_stats'])} found.") + print(f"Memory Stats: {len(data['memory_stats'])} found.") + + return data + + +def format_sql_insert(table_name, rows): + if not rows: + return "" + + columns = rows[0].__table__.columns.keys() + sql = f"INSERT INTO {table_name} ({', '.join(columns)}) VALUES\n" + values = [] + + for row in rows: + row_values = [getattr(row, col) for col in columns] + row_values = [ + f"'{value}'" if isinstance(value, str) else str(value) + for value in row_values + ] + values.append(f"({', '.join(row_values)})") + + sql += ",\n".join(values) + ";\n" + return sql + + +def dump_to_fixtures(filepath): + session, engine = get_session() + check_tables_exist(engine) + data = fetch_data(session) + + with open(filepath, "a", encoding="utf-8") as file: + if data["recordings"]: + file.write("-- Insert sample recordings\n") + file.write(format_sql_insert("recording", data["recordings"])) + + if data["action_events"]: + file.write("-- Insert sample action_events\n") + file.write(format_sql_insert("action_event", data["action_events"])) + + if data["screenshots"]: + file.write("-- Insert sample screenshots\n") + file.write(format_sql_insert("screenshot", data["screenshots"])) + + if data["window_events"]: + file.write("-- Insert sample window_events\n") + file.write(format_sql_insert("window_event", data["window_events"])) + + if data["performance_stats"]: + file.write("-- Insert sample performance_stats\n") + file.write(format_sql_insert("performance_stat", data["performance_stats"])) + + if data["memory_stats"]: + file.write("-- Insert sample memory_stats\n") + file.write(format_sql_insert("memory_stat", data["memory_stats"])) + print(f"Data appended to {filepath}") + + +if __name__ == "__main__": + + fixtures_path = PARENT_DIR_PATH / "tests/assets/fixtures.sql" + + dump_to_fixtures(fixtures_path) diff --git a/openadapt/window/__init__.py b/openadapt/window/__init__.py index ebde66d3f..645d73060 100644 --- a/openadapt/window/__init__.py +++ b/openadapt/window/__init__.py @@ -7,6 +7,7 @@ import sys from loguru import logger +import pywinauto from openadapt.config import config @@ -67,6 +68,17 @@ def get_active_window_state(read_window_data: bool) -> dict | None: return None +def get_active_window() -> pywinauto.application.WindowSpecification: + """Get the active window object. + + Returns: + pywinauto.application.WindowSpecification: The active window object. + """ + app = pywinauto.application.Application(backend="uia").connect(active_only=True) + window = app.top_window() + return window.wrapper_object() + + def get_active_element_state(x: int, y: int) -> dict | None: """Get the state of the active element at the specified coordinates. diff --git a/tests/openadapt/test_performance.py b/tests/openadapt/test_performance.py new file mode 100644 index 000000000..1d7201b6c --- /dev/null +++ b/tests/openadapt/test_performance.py @@ -0,0 +1,88 @@ +import pytest +import time +from loguru import logger +import logging +from openadapt.db.crud import ( + get_recordings_by_desc, + get_new_session, +) +from openadapt.replay import replay +from openadapt.window import ( + get_active_window, +) + +# logging to a txt file +logging.basicConfig( + level=logging.INFO, + filename="test_results.txt", + filemode="w", + format="%(asctime)s | %(levelname)s | %(message)s", +) + + +# parametrized tests +@pytest.mark.parametrize( + "task_description, replay_strategy, expected_value, instructions", + [ + ("test_calculator", "VisualReplayStrategy", "6", " "), + ("test_calculator", "VisualReplayStrategy", "8", "calculate 9-8+7"), + # ("test_spreadsheet", "NaiveReplayStrategy"), + # ("test_powerpoint", "NaiveReplayStrategy") + ], +) +def test_replay(task_description, replay_strategy, expected_value, instructions): + # Get recordings which contain the string "test_calculator" + session = get_new_session(read_only=True) + recordings = get_recordings_by_desc(session, task_description) + + assert ( + len(recordings) > 0 + ), f"No recordings found with task description: {task_description}" + recording = recordings[0] + + result = replay( + strategy_name=replay_strategy, + recording=recording, + instructions=instructions, + ) + assert result is True, f"Replay failed for recording: {recording.id}" + + def find_display_element(element, timeout=10): + """Find the display element within the specified timeout. + + Args: + element: The parent element to search within. + timeout: The maximum time to wait for the element (default is 10 seconds). + + Returns: + The found element. + + Raises: + TimeoutError: If the element is not found within the specified timeout. + """ + end_time = time.time() + timeout + while time.time() < end_time: + elements = element.descendants(control_type="Text") + for elem in elements: + if elem.element_info.name.startswith( + "Display is" + ): # Target the display element + return elem + time.sleep(0.5) + raise TimeoutError("Display element not found within the specified timeout") + + active_window = get_active_window() + element = find_display_element(active_window) + value = element.element_info.name[-1] + + element_value = value + assert ( + element_value == expected_value + ), f"Value mismatch: expected '{expected_value}', got '{element_value}'" + + result_message = f"Value match: '{element_value}' == '{expected_value}'" + logging.info(result_message) + + +if __name__ == "__main__": + pytest.main() diff --git a/tests/openadapt/test_results.txt b/tests/openadapt/test_results.txt new file mode 100644 index 000000000..37e0f372e --- /dev/null +++ b/tests/openadapt/test_results.txt @@ -0,0 +1,3 @@ +2024-07-12 23:11:21,853 | INFO | Value match: '6' == '6' +2024-07-12 23:11:45,640 | INFO | Value match: '8' == '8' +2024-07-12 23:11:46,388 | INFO | HTTP Request: POST https://api.anthropic.com/v1/messages "HTTP/1.1 401 Unauthorized" From da5d4104328090c08ae391782682d2871f81893c Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Fri, 12 Jul 2024 23:52:23 +0530 Subject: [PATCH 08/10] fix: remove unnecessary file changes --- openadapt/models.py | 31 ++++++------- openadapt/plotting.py | 8 ++-- openadapt/strategies/naive.py | 1 - openadapt/strategies/visual.py | 79 ---------------------------------- openadapt/vision.py | 51 ---------------------- 5 files changed, 16 insertions(+), 154 deletions(-) diff --git a/openadapt/models.py b/openadapt/models.py index 31f49eb4e..d019f1164 100644 --- a/openadapt/models.py +++ b/openadapt/models.py @@ -562,26 +562,19 @@ def to_prompt_dict(self, include_data: bool = True) -> dict[str, Any]: "You can help by uncommenting the lines below and pasting " "the contents of the window_dict into a new GitHub Issue." ) - from pprint import pformat - - logger.info(f"window_dict=\n{pformat(window_dict)}") - import ipdb - - ipdb.set_trace() - if "state" in window_dict and "data" in window_dict["state"]: - window_state = window_dict["state"] - window_state["data"] = utils.clean_dict( - utils.filter_keys( - window_state["data"], - key_suffixes, - ) + # from pprint import pformat + # logger.info(f"window_dict=\n{pformat(window_dict)}") + # import ipdb; ipdb.set_trace() + window_state = window_dict["state"] + window_state["data"] = utils.clean_dict( + utils.filter_keys( + window_state["data"], + key_suffixes, ) - if "state" in window_dict: - if "data" in window_dict["state"]: - window_dict["state"].pop("data") - if "meta" in window_dict["state"]: - window_dict["state"].pop("meta") - + ) + else: + window_dict["state"].pop("data") + window_dict["state"].pop("meta") return window_dict diff --git a/openadapt/plotting.py b/openadapt/plotting.py index f7fd306d6..f476c3b82 100644 --- a/openadapt/plotting.py +++ b/openadapt/plotting.py @@ -799,7 +799,6 @@ def get_marked_image( label_mode = "1" alpha = 0.1 anno_mode = [] - if include_masks: anno_mode.append("Mask") if include_marks: @@ -815,6 +814,7 @@ def get_marked_image( ) mask_map[mask == 1] = label - im = demo.get_image() - marked_image = Image.fromarray(im) - return marked_image + im = demo.get_image() + marked_image = Image.fromarray(im) + + return marked_image diff --git a/openadapt/strategies/naive.py b/openadapt/strategies/naive.py index 67bb075b7..97648b645 100644 --- a/openadapt/strategies/naive.py +++ b/openadapt/strategies/naive.py @@ -94,7 +94,6 @@ def get_next_action_event( # (fixed by disabling remove_move_before_click in events.py) # if action_event.name in common.MOUSE_CLICK_EVENTS: # time.sleep(self.double_click_interval_seconds + 0.01) - logger.info(f"{action_event}") return action_event else: return None diff --git a/openadapt/strategies/visual.py b/openadapt/strategies/visual.py index 4d5fa16d7..7aaa00cab 100644 --- a/openadapt/strategies/visual.py +++ b/openadapt/strategies/visual.py @@ -374,64 +374,6 @@ def find_similar_image_segmentation( return similar_segmentation, similar_segmentation_diff -def combine_segmentations( - new_image: Image.Image, - previous_segmentation: Segmentation, - new_descriptions: list[str], - new_masked_images: list[Image.Image], - new_masks: list[np.ndarray], -) -> Segmentation: - """Combine the previous segmentation with the new segmentation of the differences. - Args: - new_image: The new image which includes the changes. - previous_segmentation: The previous segmentation containing unchanged segments. - new_descriptions: Descriptions of the new segments from the difference image. - new_masked_images: Masked images of the new segments from the difference image. - new_masks: masks of the new segments. - Returns: - Segmentation: A new segmentation combining both previous and new segments. - """ - - def masks_overlap(mask1, mask2): - """Check if two masks overlap.""" - return np.any(np.logical_and(mask1, mask2)) - - # Calculate the bounding boxes and centroids for the new segments - new_bounding_boxes, new_centroids = vision.calculate_bounding_boxes(new_masks) - - # Filter out overlapping previous segments - filtered_previous_masked_images = [] - filtered_previous_descriptions = [] - filtered_previous_bounding_boxes = [] - filtered_previous_centroids = [] - for idx, prev_mask in enumerate(previous_segmentation.masks): - if not any(masks_overlap(prev_mask, new_mask) for new_mask in new_masks): - filtered_previous_masked_images.append( - previous_segmentation.masked_images[idx] - ) - filtered_previous_descriptions.append( - previous_segmentation.descriptions[idx] - ) - filtered_previous_bounding_boxes.append( - previous_segmentation.bounding_boxes[idx] - ) - filtered_previous_centroids.append(previous_segmentation.centroids[idx]) - - # Combine filtered previous segments with new segments - combined_masked_images = filtered_previous_masked_images + new_masked_images - combined_descriptions = filtered_previous_descriptions + new_descriptions - combined_bounding_boxes = filtered_previous_bounding_boxes + new_bounding_boxes - combined_centroids = filtered_previous_centroids + new_centroids - - return Segmentation( - image=new_image, - masked_images=combined_masked_images, - descriptions=combined_descriptions, - bounding_boxes=combined_bounding_boxes, - centroids=combined_centroids, - ) - - def get_window_segmentation( action_event: models.ActionEvent, exceptions: list[Exception] | None = None, @@ -460,27 +402,6 @@ def get_window_segmentation( # TODO XXX: create copy of similar_segmentation, but overwrite with segments of # regions of new image where segments of similar_segmentation overlap non-zero # regions of similar_segmentation_diff - difference_image = vision.extract_difference_image( - original_image, - similar_segmentation.image, - tolerance=0.05, - ) - new_masks = vision.get_masks_from_segmented_image(difference_image) - new_masked_images = vision.extract_masked_images(difference_image, new_masks) - new_descriptions = prompt_for_descriptions( - difference_image, - new_masked_images, - action_event.active_segment_description, - exceptions, - ) - updated_segmentation = combine_segmentations( - difference_image, - similar_segmentation, - new_descriptions, - new_masked_images, - new_masks, - ) - similar_segmentation = updated_segmentation return similar_segmentation segmentation_adapter = adapters.get_default_segmentation_adapter() diff --git a/openadapt/vision.py b/openadapt/vision.py index 7c8a7026b..65c3660ec 100644 --- a/openadapt/vision.py +++ b/openadapt/vision.py @@ -56,57 +56,6 @@ def get_masks_from_segmented_image( return masks -def extract_masked_image(image: Image.Image, mask: np.ndarray) -> Image.Image: - # Ensure the mask is in the correct format - mask = mask.astype(np.uint8) * 255 - - # Create a blank (transparent) image - masked_image = Image.new("RGBA", image.size) - - # Convert the mask to an image - mask_image = Image.fromarray(mask, mode="L") - - # Composite the original image and the blank image using the mask - masked_image = Image.composite(image.convert("RGBA"), masked_image, mask_image) - - return masked_image - - -@cache.cache() -def extract_difference_image( - new_image: Image.Image, - old_image: Image.Image, - tolerance: float = 0.05, -) -> Image.Image: - """Extract the portion of the new image that is different from the old image. - - Args: - new_image: The new image as a PIL Image object. - old_image: The old image as a PIL Image object. - tolerance: Tolerance level to consider a pixel as different (default is 0.05). - - Returns: - A PIL Image object representing the difference image. - """ - new_image_np = np.array(new_image) - old_image_np = np.array(old_image) - - # Compute the absolute difference between the two images in each color channel - diff = np.abs(new_image_np - old_image_np) - - # Create a mask for the regions where the difference is above the tolerance - mask = np.any(diff > (255 * tolerance), axis=-1) - - # Initialize an array for the segmented image - segmented_image_np = np.zeros_like(new_image_np) - - # Set the pixels that are different in the new image - segmented_image_np[mask] = new_image_np[mask] - - # Convert the numpy array back to an image - return Image.fromarray(segmented_image_np) - - @cache.cache() def filter_masks_by_size( masks: list[np.ndarray], From 929c31be6da87e5c326fd7bc41d2b523e401fddf Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Fri, 12 Jul 2024 23:54:55 +0530 Subject: [PATCH 09/10] fix: remove unnecessary file change --- openadapt/strategies/naive.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openadapt/strategies/naive.py b/openadapt/strategies/naive.py index 97648b645..9a0aba170 100644 --- a/openadapt/strategies/naive.py +++ b/openadapt/strategies/naive.py @@ -94,6 +94,7 @@ def get_next_action_event( # (fixed by disabling remove_move_before_click in events.py) # if action_event.name in common.MOUSE_CLICK_EVENTS: # time.sleep(self.double_click_interval_seconds + 0.01) + return action_event else: return None From 36e183f13e27a039d6e3d72162476f97d20e4099 Mon Sep 17 00:00:00 2001 From: Animesh404 Date: Mon, 15 Jul 2024 16:28:02 +0530 Subject: [PATCH 10/10] feat: a11y for macos --- openadapt/a11y/__init__.py | 49 +++++++++++ openadapt/a11y/_macos.py | 61 +++++++++++++ openadapt/a11y/_windows.py | 44 ++++++++++ openadapt/scripts/generate_db_fixtures.py | 101 +++++++++++++--------- openadapt/window/__init__.py | 12 --- tests/openadapt/test_performance.py | 44 ++-------- tests/openadapt/test_results.txt | 3 - 7 files changed, 219 insertions(+), 95 deletions(-) create mode 100644 openadapt/a11y/__init__.py create mode 100644 openadapt/a11y/_macos.py create mode 100644 openadapt/a11y/_windows.py delete mode 100644 tests/openadapt/test_results.txt diff --git a/openadapt/a11y/__init__.py b/openadapt/a11y/__init__.py new file mode 100644 index 000000000..56cb8e23f --- /dev/null +++ b/openadapt/a11y/__init__.py @@ -0,0 +1,49 @@ +"""This module provides platform-specific implementations for window and element + interactions using accessibility APIs. It abstracts the platform differences + and provides a unified interface for retrieving the active window, finding + display elements, and getting element values. +""" + +import sys + +from loguru import logger + +if sys.platform == "darwin": + from . import _macos as impl + + role = "AXStaticText" +elif sys.platform in ("win32", "linux"): + from . import _windows as impl + + role = "Text" +else: + raise Exception(f"Unsupported platform: {sys.platform}") + + +def get_active_window(): + """Get the active window object. + + Returns: + The active window object. + """ + try: + return impl.get_active_window() + except Exception as exc: + logger.warning(f"{exc=}") + return None + + +def get_element_value(active_window, role=role): + """Find the display of active_window. + + Args: + active_window: The parent window to search within. + + Returns: + The found active_window. + """ + try: + return impl.get_element_value(active_window, role) + except Exception as exc: + logger.warning(f"{exc=}") + return None diff --git a/openadapt/a11y/_macos.py b/openadapt/a11y/_macos.py new file mode 100644 index 000000000..5a6bade71 --- /dev/null +++ b/openadapt/a11y/_macos.py @@ -0,0 +1,61 @@ +import AppKit +import ApplicationServices + + +def get_attribute(element, attribute): + result, value = ApplicationServices.AXUIElementCopyAttributeValue( + element, attribute, None + ) + if result == 0: + return value + return None + + +def find_element_by_attribute(element, attribute, value): + if get_attribute(element, attribute) == value: + return element + children = get_attribute(element, ApplicationServices.kAXChildrenAttribute) or [] + for child in children: + found = find_element_by_attribute(child, attribute, value) + if found: + return found + return None + + +def get_active_window(): + """Get the active window object. + + Returns: + AXUIElement: The active window object. + """ + workspace = AppKit.NSWorkspace.sharedWorkspace() + active_app = workspace.frontmostApplication() + app_element = ApplicationServices.AXUIElementCreateApplication( + active_app.processIdentifier() + ) + + error_code, focused_window = ApplicationServices.AXUIElementCopyAttributeValue( + app_element, ApplicationServices.kAXFocusedWindowAttribute, None + ) + if error_code: + raise Exception("Could not get the active window.") + return focused_window + + +def get_element_value(element, role="AXStaticText"): + """Get the value of a specific element . + + Args: + element: The AXUIElement to search within. + + Returns: + str: The value of the element, or an error message if not found. + """ + target_element = find_element_by_attribute( + element, ApplicationServices.kAXRoleAttribute, role + ) + if not target_element: + return f"AXStaticText element not found." + + value = get_attribute(target_element, ApplicationServices.kAXValueAttribute) + return value if value else f"No value for AXStaticText element." diff --git a/openadapt/a11y/_windows.py b/openadapt/a11y/_windows.py new file mode 100644 index 000000000..974e5f3ec --- /dev/null +++ b/openadapt/a11y/_windows.py @@ -0,0 +1,44 @@ +from loguru import logger +import pywinauto +import re + + +def get_active_window() -> pywinauto.application.WindowSpecification: + """Get the active window object. + + Returns: + pywinauto.application.WindowSpecification: The active window object. + """ + app = pywinauto.application.Application(backend="uia").connect(active_only=True) + window = app.top_window() + return window.wrapper_object() + + +def get_element_value(active_window, role="Text"): + """Find the display element. + + Args: + active_window: The parent window to search within. + role (str): The role of the element to search for. + + Returns: + The found display element value. + + Raises: + ValueError: If the element is not found. + """ + try: + elements = active_window.descendants() # Retrieve all descendants + for elem in elements: + if ( + elem.element_info.control_type == role + and elem.element_info.name.startswith("Display is") + ): + # Extract the number from the element's name + match = re.search(r"[-+]?\d*\.?\d+", elem.element_info.name) + if match: + return str(match.group()) + raise ValueError("Display element not found") + except Exception as exc: + logger.warning(f"Error in get_element_value: {exc}") + return None diff --git a/openadapt/scripts/generate_db_fixtures.py b/openadapt/scripts/generate_db_fixtures.py index 7cd20bd51..e3a4da8b9 100644 --- a/openadapt/scripts/generate_db_fixtures.py +++ b/openadapt/scripts/generate_db_fixtures.py @@ -1,24 +1,36 @@ -import os from sqlalchemy import create_engine, inspect -from sqlalchemy.orm import sessionmaker from openadapt.db.db import Base -from openadapt.config import DATA_DIR_PATH, PARENT_DIR_PATH, RECORDING_DIR_PATH +from openadapt.config import PARENT_DIR_PATH, RECORDING_DIR_PATH import openadapt.db.crud as crud from loguru import logger def get_session(): + """ + Establishes a database connection and returns a session and engine. + + Returns: + tuple: A tuple containing the SQLAlchemy session and engine. + """ db_url = RECORDING_DIR_PATH / "recording.db" - print(f"Database URL: {db_url}") + logger.info(f"Database URL: {db_url}") engine = create_engine(f"sqlite:///{db_url}") - # SessionLocal = sessionmaker(bind=engine) Base.metadata.create_all(bind=engine) session = crud.get_new_session(read_only=True) - print("Database connection established.") + logger.info("Database connection established.") return session, engine def check_tables_exist(engine): + """ + Checks if the expected tables exist in the database. + + Args: + engine: SQLAlchemy engine object. + + Returns: + list: A list of table names in the database. + """ inspector = inspect(engine) tables = inspector.get_table_names() expected_tables = [ @@ -32,13 +44,21 @@ def check_tables_exist(engine): for table_name in expected_tables: table_exists = table_name in tables logger.info(f"{table_name=} {table_exists=}") - return tables + return tables def fetch_data(session): + """ + Fetches the most recent recordings and related data from the database. + + Args: + session: SQLAlchemy session object. + + Returns: + dict: A dictionary containing fetched data. + """ # get the most recent three recordings recordings = crud.get_recordings(session, max_rows=3) - recording_ids = [recording.id for recording in recordings] action_events = [] screenshots = [] @@ -63,17 +83,27 @@ def fetch_data(session): } # Debug prints to verify data fetching - print(f"Recordings: {len(data['recordings'])} found.") - print(f"Action Events: {len(data['action_events'])} found.") - print(f"Screenshots: {len(data['screenshots'])} found.") - print(f"Window Events: {len(data['window_events'])} found.") - print(f"Performance Stats: {len(data['performance_stats'])} found.") - print(f"Memory Stats: {len(data['memory_stats'])} found.") + logger.info(f"Recordings: {len(data['recordings'])} found.") + logger.info(f"Action Events: {len(data['action_events'])} found.") + logger.info(f"Screenshots: {len(data['screenshots'])} found.") + logger.info(f"Window Events: {len(data['window_events'])} found.") + logger.info(f"Performance Stats: {len(data['performance_stats'])} found.") + logger.info(f"Memory Stats: {len(data['memory_stats'])} found.") return data def format_sql_insert(table_name, rows): + """ + Formats SQL insert statements for a given table and rows. + + Args: + table_name (str): The name of the table. + rows (list): A list of SQLAlchemy ORM objects representing the rows. + + Returns: + str: A string containing the SQL insert statements. + """ if not rows: return "" @@ -94,35 +124,24 @@ def format_sql_insert(table_name, rows): def dump_to_fixtures(filepath): + """ + Dumps the fetched data into an SQL file. + + Args: + filepath (str): The path to the SQL file. + """ session, engine = get_session() check_tables_exist(engine) - data = fetch_data(session) - - with open(filepath, "a", encoding="utf-8") as file: - if data["recordings"]: - file.write("-- Insert sample recordings\n") - file.write(format_sql_insert("recording", data["recordings"])) - - if data["action_events"]: - file.write("-- Insert sample action_events\n") - file.write(format_sql_insert("action_event", data["action_events"])) - - if data["screenshots"]: - file.write("-- Insert sample screenshots\n") - file.write(format_sql_insert("screenshot", data["screenshots"])) - - if data["window_events"]: - file.write("-- Insert sample window_events\n") - file.write(format_sql_insert("window_event", data["window_events"])) - - if data["performance_stats"]: - file.write("-- Insert sample performance_stats\n") - file.write(format_sql_insert("performance_stat", data["performance_stats"])) - - if data["memory_stats"]: - file.write("-- Insert sample memory_stats\n") - file.write(format_sql_insert("memory_stat", data["memory_stats"])) - print(f"Data appended to {filepath}") + rows_by_table_name = fetch_data(session) + + for table_name, rows in rows_by_table_name.items(): + if not rows: + logger.warning(f"No rows for {table_name=}") + continue + with open(filepath, "a", encoding="utf-8") as file: + logger.info(f"Writing {len(rows)=} to {filepath=} for {table_name=}") + file.write(f"-- Insert sample rows for {table_name}\n") + file.write(format_sql_insert(table_name, rows)) if __name__ == "__main__": diff --git a/openadapt/window/__init__.py b/openadapt/window/__init__.py index 645d73060..ebde66d3f 100644 --- a/openadapt/window/__init__.py +++ b/openadapt/window/__init__.py @@ -7,7 +7,6 @@ import sys from loguru import logger -import pywinauto from openadapt.config import config @@ -68,17 +67,6 @@ def get_active_window_state(read_window_data: bool) -> dict | None: return None -def get_active_window() -> pywinauto.application.WindowSpecification: - """Get the active window object. - - Returns: - pywinauto.application.WindowSpecification: The active window object. - """ - app = pywinauto.application.Application(backend="uia").connect(active_only=True) - window = app.top_window() - return window.wrapper_object() - - def get_active_element_state(x: int, y: int) -> dict | None: """Get the state of the active element at the specified coordinates. diff --git a/tests/openadapt/test_performance.py b/tests/openadapt/test_performance.py index 1d7201b6c..ca58abccd 100644 --- a/tests/openadapt/test_performance.py +++ b/tests/openadapt/test_performance.py @@ -1,22 +1,13 @@ import pytest -import time from loguru import logger -import logging from openadapt.db.crud import ( get_recordings_by_desc, get_new_session, ) from openadapt.replay import replay -from openadapt.window import ( +from openadapt.a11y import ( get_active_window, -) - -# logging to a txt file -logging.basicConfig( - level=logging.INFO, - filename="test_results.txt", - filemode="w", - format="%(asctime)s | %(levelname)s | %(message)s", + get_element_value, ) @@ -47,41 +38,16 @@ def test_replay(task_description, replay_strategy, expected_value, instructions) ) assert result is True, f"Replay failed for recording: {recording.id}" - def find_display_element(element, timeout=10): - """Find the display element within the specified timeout. - - Args: - element: The parent element to search within. - timeout: The maximum time to wait for the element (default is 10 seconds). - - Returns: - The found element. - - Raises: - TimeoutError: If the element is not found within the specified timeout. - """ - end_time = time.time() + timeout - while time.time() < end_time: - elements = element.descendants(control_type="Text") - for elem in elements: - if elem.element_info.name.startswith( - "Display is" - ): # Target the display element - return elem - time.sleep(0.5) - raise TimeoutError("Display element not found within the specified timeout") - active_window = get_active_window() - element = find_display_element(active_window) - value = element.element_info.name[-1] + element_value = get_element_value(active_window) + logger.info(element_value) - element_value = value assert ( element_value == expected_value ), f"Value mismatch: expected '{expected_value}', got '{element_value}'" result_message = f"Value match: '{element_value}' == '{expected_value}'" - logging.info(result_message) + logger.info(result_message) if __name__ == "__main__": diff --git a/tests/openadapt/test_results.txt b/tests/openadapt/test_results.txt deleted file mode 100644 index 37e0f372e..000000000 --- a/tests/openadapt/test_results.txt +++ /dev/null @@ -1,3 +0,0 @@ -2024-07-12 23:11:21,853 | INFO | Value match: '6' == '6' -2024-07-12 23:11:45,640 | INFO | Value match: '8' == '8' -2024-07-12 23:11:46,388 | INFO | HTTP Request: POST https://api.anthropic.com/v1/messages "HTTP/1.1 401 Unauthorized"