From ea63f1e64a206157b61eaff1329324f03b3a4bd3 Mon Sep 17 00:00:00 2001 From: torzdf <36920800+torzdf@users.noreply.github.com> Date: Fri, 28 Jun 2024 13:50:14 +0100 Subject: [PATCH 1/2] Mask tool. Add ability to output custom imported masks --- locales/es/LC_MESSAGES/tools.mask.cli.mo | Bin 14916 -> 15278 bytes locales/es/LC_MESSAGES/tools.mask.cli.po | 41 +++++++++++++---------- locales/kr/LC_MESSAGES/tools.mask.cli.mo | Bin 14151 -> 14531 bytes locales/kr/LC_MESSAGES/tools.mask.cli.po | 40 ++++++++++++---------- locales/ru/LC_MESSAGES/tools.mask.cli.mo | Bin 18221 -> 18726 bytes locales/ru/LC_MESSAGES/tools.mask.cli.po | 41 +++++++++++++---------- locales/tools.mask.cli.pot | 28 +++++++++------- tools/mask/cli.py | 8 +++-- tools/mask/mask_output.py | 4 +++ 9 files changed, 92 insertions(+), 70 deletions(-) diff --git a/locales/es/LC_MESSAGES/tools.mask.cli.mo b/locales/es/LC_MESSAGES/tools.mask.cli.mo index 2b153b9c56767340632a1725fdbfdb9220ba1b4f..ad86f74687970000a91cee1fd14ce3bb86455225 100644 GIT binary patch delta 868 zcmY*WOKTHR7(HoiQ>&HM`sl;QXHfgFO`lk87g7YljTMCY0EIDgZAK?^)0qh+-K0vz zZ8hpnLR*5kwRzF8u+bpo@BDl1Mwuox?rf_nmX@on`;!%z|0?Rs}qc z1JA30LOpP&2I$@bX!5U{fk|TT4&V)d`4-?N@pvn6f%uNNfw;d7s3mrlv7bmuj1Vuz zfMdk-#0TxbCGx*_QIq@0-6-HEiTORiP7?XOzzyzi3ou9AwhtI5e~&oFfS(h<7#+_Y z1g_HIZa1*PfU5&Q2lg$kkAM6>Hzf3j;3;2tLKW*9438@2d(YGg#Ei5c7WaYh?uTIZ7@7Fo+IoB(9#@5P&6#)A#V9N`r ztbJv`7`a^z+yKZ_@Q*z80TX0HC6GfV$y{#GF+G+UdVl*77o zzdEd7(QKi&vpv)+WH~H%w8^r0R`n}|tYiPMJ2)5<_E1fkQH3IwjA$&ns%S!qWv4Qa e-=6E!il*r1P#`2})@~rrX+77kJFUZ3U&$Xc*L{My2mZ;He z4X3D#nE2AD>0>3NK?FrF&`R-GT$)QF?SXN>zTV#+{fgS zXk?ZdfP>5{%(fEXEXTiW1M+x%pcG#0;-3>`z)w!NxAR8k83KIFPt22?Z`}bLVuQ10 z;4~|Wa^M3i9<2oC* z9q@&*iKqT?Bd{URYbe>eIk5)V0ayijJaYo`hLS997`R!0_R;V?Q+t;E-snBv-J`hF zz0DrA$D@+s?7Cb-Ee^YrIunUb=XvUGcVBQvrFZCN}=6pBez5GblIpwak=d79)%;UkS>`h zc`4nazC*7P_-v}k*6UajX0%_X2S)8yPRquehU)!Rs;z6Px9-_j8xTeB4E5U6;x>~u zQeKzw-!_QpemM|X?Tw15Ws1jqGQ1>1Q4+zSDv}e6@rCPh!bkF2RF2KA2E!yH!!kHZ zG8`1)sGRH*gZ*MKQbY2|xCl?li4esXR^(Xh-_!E&q@Uz8D+JesW++WRS!6XB5(D>0 z+>VLqp_H`nhs12GDo|iH=L`RMx{bKD9C%bw*hVt86xYH{WJ$iOg+%O5s!6Ivzm6PT n(k+U)84=*SOb*MDK0WZmH^?LD{I8{S6>Z!sjoRCatGT}c-rZ1D delta 524 zcmYMw-%FEG7zgn0JGX|;4Syi%HtitI4b6I4o1$0gg%@4eU4cXqI!waN+{Bw=F6J2R zMtmt`7qVn9MK4}EMhjAiE((IW@-ER!+0Ook`VIvTJbcc1&N=V%ocH_7AJMg$q^$wi zvIBoDz;+X`ZUa`GK#c3ocHkk+b^u2JPOo#1Zgv7w^kx@erAc~?rZ3G=eiG}nM+5To z75(A{o-#k=1zNa%??n=QoRqqOb8h_Z;X(TO2Czay0+{6fCz@iT)?Q$YW_-XOR{R+N z_SxXZ9pD?iA7qof*Bk~8sG=GAVF+lXf#HirgO`pBvrxVBdrYtn_+Yd$>VrQ5ZAMD- z@bwpimBA`#VKf>0ny1mhnTz3+$J&|AHyw#4B8zj;S&@i|rFcXvKaWJk-26gpF;UOX zKa0PJ8xI@-nRaYjWYKW4t{ZFaUs_N;_B^p0|9sw%P;d5B zey8@XAXNHmEp1jyDYdy;&1I{(;%%YKazz$Kg}-91s6tt76f0%Z=<;{FD<2P4-W1hR aR=q9OQhQg|<&9Gje+zRVn()LTqE)A3`{N?IfoNkforN(fwtGp4BXWaf<-1mj2A zBGd4;Y9q90kp$+`8X6rzG72K@g$$B{*_H%Bv}sf4j`ktl`!4_Y&iS9;IqyCi(;iG{ zcY^T`HsHPkc$fvmoxnskP_`W~D87&fTq3R53#0(ta0A1nrwf70q_0Trq~3kN7Sh8T zT1`qw94GD6fO^sZai%;#AH}~HQJ%iT#fal28G0G8tc(YNw`9on0-s4&N#9d^`4I4f zI!)?8Ig#)AC`QA+r~%q2K6(;(PQY)cfNJt5&HyE(pBjL7#K}1e7TrE+Wy zlmNTwB`*MwAN0f6MMo{V2yEj6nkQd5)S@c_b-_+LF8)~a1-?Md~J&d zx_gX>S=AUbdtxRt!d=%YSyOu`%sMiOj_?)M-4gAL`k2|?5oPq4?IG403Y(#bN}2|X zMMIUW!H6(p9rD|#U@948ZGV!cs%ecyO`|*0p)q1oexpN6ILM;yMyxByT8&5~6zwr6 zLLD}@%pk9Bo>4gzg|9kForF=EC1ylQ%vLfn$4kmBQL6PFF(jxxRf8Zh$cSTZ;v%gj` zz2RXZAqT`Fl_%w>cq)g)oE($`WLAVs@R>+4G5a5Yr;nBt$YBDAc?FRDRFRPVR5ioj zcnh2fRYeH#gkSJ`d2gB9Cdas6FU*>kL;Rdx%-7g{ZI+Md^*T}5($v56EhT;HBTh4d cEXomLkMp#C-7zN?XwO;ArG0iw_?z>80k|zF&j0`b delta 503 zcmXYtOGsNm6o&taiJ>O3Y88`Q)LVRl@%E~SD1|DON>|p6f*`bLd>|%bBDyFwo+nuMXU}cTv@<| z8@O-)+c`kW1uXc0C}aOKpod&60@48f7W0l=_XB-oMF4P;2{N0Uf8-L$l1Pzb0{BF} zWu0sdm|^~13GW%7lpuj)3hXlATSoXC_({S0a^Q+=ssMg7uU7)+9PGA=#*U{U>N)K9 zdSHmLd#r2gnNP)1&N0KZr#?+xH3FUcTzL-IMEcCv8+onR+GJ-9IPX_>=* zD3EhP5*_mA03~F zPR29Y@ei@FnC$Y@sCmz}Lv6bMI8-RN*XwJnYiQ9;v(;>_HBGe|^b66TcC^)#F`v2UTGoSUbd4~+D_`$k+o;fT6@+P>%dOhv+{@WQ5>r7 R7c)+^QGMrBCyo6@{{bA>Z=nDH diff --git a/locales/ru/LC_MESSAGES/tools.mask.cli.po b/locales/ru/LC_MESSAGES/tools.mask.cli.po index 1f63fe42ff..6cabf81c53 100644 --- a/locales/ru/LC_MESSAGES/tools.mask.cli.po +++ b/locales/ru/LC_MESSAGES/tools.mask.cli.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-28 23:51+0000\n" -"PO-Revision-Date: 2024-03-29 00:07+0000\n" +"POT-Creation-Date: 2024-06-28 13:45+0100\n" +"PO-Revision-Date: 2024-06-28 13:48+0100\n" "Last-Translator: \n" "Language-Team: \n" "Language: ru\n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" -"X-Generator: Poedit 3.4.2\n" +"X-Generator: Poedit 3.4.4\n" #: tools/mask/cli.py:15 msgid "" @@ -175,7 +175,7 @@ msgstr "" "de alineaciones. Nota: 'custom' debe ser el 'masker' seleccionado y las " "máscaras deben tener el mismo formato que el 'input-type' (frames o faces)" -#: tools/mask/cli.py:135 tools/mask/cli.py:154 tools/mask/cli.py:174 +#: tools/mask/cli.py:135 tools/mask/cli.py:154 tools/mask/cli.py:176 msgid "import" msgstr "Импортировать" @@ -208,9 +208,11 @@ msgstr "" #: tools/mask/cli.py:156 msgid "" -"R|Import only. The centering to use when importing masks. Note: For any job " -"other than 'import' this option is ignored as mask centering is handled " -"internally.\n" +"R|Import/Output only. When importing masks, this is the centering to use. " +"For output this is only used for outputting custom imported masks, and " +"should correspond to the centering used when importing the mask. Note: For " +"any job other than 'import' and 'output' this option is ignored as mask " +"centering is handled internally.\n" "L|face: Centers the mask on the center of the face, adjusting for pitch and " "yaw. Outside of requirements for full head masking/training, this is likely " "to be the best choice.\n" @@ -222,9 +224,12 @@ msgid "" "the nose with and crops closely to the face. Can result in the edges of the " "mask appearing outside of the training area." msgstr "" -"R|Только импорт. Центрирование, используемое при импорте масок. Примечание. " -"Для любого задания, кроме «импорта», этот параметр игнорируется, поскольку " -"центрирование маски обрабатывается внутри.\n" +"R|Только импорт/вывод. При импорте масок это центрирование для " +"использования. Для вывода это используется только для вывода " +"пользовательских импортированных масок и должно соответствовать " +"центрированию, используемому при импорте маски. Примечание: для любого " +"задания, кроме «импорта» и «вывода», эта опция игнорируется, поскольку " +"центрирование маски обрабатывается внутренне.\n" "L|face: центрирует маску по центру лица с регулировкой угла наклона и " "отклонения от курса. Помимо требований к полной маскировке/тренировке " "головы, это, вероятно, будет лучшим выбором.\n" @@ -236,7 +241,7 @@ msgstr "" "приближает ее к лицу. Это может привести к тому, что края маски окажутся за " "пределами тренировочной зоны." -#: tools/mask/cli.py:179 +#: tools/mask/cli.py:181 msgid "" "Import only. The size, in pixels to internally store the mask at.\n" "The default is 128 which is fine for nearly all usecases. Larger sizes will " @@ -247,12 +252,12 @@ msgstr "" "использования. Большие размеры приведут к увеличению размера файлов " "выравниваний и более длительной обработке." -#: tools/mask/cli.py:187 tools/mask/cli.py:195 tools/mask/cli.py:209 -#: tools/mask/cli.py:223 tools/mask/cli.py:233 +#: tools/mask/cli.py:189 tools/mask/cli.py:197 tools/mask/cli.py:211 +#: tools/mask/cli.py:225 tools/mask/cli.py:235 msgid "output" msgstr "вывод" -#: tools/mask/cli.py:189 +#: tools/mask/cli.py:191 msgid "" "Optional output location. If provided, a preview of the masks created will " "be output in the given folder." @@ -260,7 +265,7 @@ msgstr "" "Необязательное местоположение вывода. Если указано, предварительный просмотр " "созданных масок будет выведен в указанную папку." -#: tools/mask/cli.py:200 +#: tools/mask/cli.py:202 msgid "" "Apply gaussian blur to the mask output. Has the effect of smoothing the " "edges of the mask giving less of a hard edge. the size is in pixels. This " @@ -273,7 +278,7 @@ msgstr "" "Примечание: влияет только на предварительный просмотр. Установите значение 0 " "для выключения" -#: tools/mask/cli.py:214 +#: tools/mask/cli.py:216 msgid "" "Helps reduce 'blotchiness' on some masks by making light shades white and " "dark shades black. Higher values will impact more of the mask. NB: Only " @@ -284,7 +289,7 @@ msgstr "" "часть маски. Примечание: влияет только на предварительный просмотр. " "Установите значение 0 для выключения" -#: tools/mask/cli.py:225 +#: tools/mask/cli.py:227 msgid "" "R|How to format the output when processing is set to 'output'.\n" "L|combined: The image contains the face/frame, face mask and masked face.\n" @@ -297,7 +302,7 @@ msgstr "" "L|masked: Вывести лицо/кадр как изображение rgba с маскированным лицом.\n" "L|mask: Выводить только маску как одноканальное изображение." -#: tools/mask/cli.py:235 +#: tools/mask/cli.py:237 msgid "" "R|Whether to output the whole frame or only the face box when using output " "processing. Only has an effect when using frames as input." diff --git a/locales/tools.mask.cli.pot b/locales/tools.mask.cli.pot index 54563a8620..f8024c88ee 100644 --- a/locales/tools.mask.cli.pot +++ b/locales/tools.mask.cli.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-03-28 23:51+0000\n" +"POT-Creation-Date: 2024-06-28 13:45+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -114,7 +114,7 @@ msgid "" "must be in the same format as the 'input-type' (frames or faces)" msgstr "" -#: tools/mask/cli.py:135 tools/mask/cli.py:154 tools/mask/cli.py:174 +#: tools/mask/cli.py:135 tools/mask/cli.py:154 tools/mask/cli.py:176 msgid "import" msgstr "" @@ -135,9 +135,11 @@ msgstr "" #: tools/mask/cli.py:156 msgid "" -"R|Import only. The centering to use when importing masks. Note: For any job " -"other than 'import' this option is ignored as mask centering is handled " -"internally.\n" +"R|Import/Output only. When importing masks, this is the centering to use. " +"For output this is only used for outputting custom imported masks, and " +"should correspond to the centering used when importing the mask. Note: For " +"any job other than 'import' and 'output' this option is ignored as mask " +"centering is handled internally.\n" "L|face: Centers the mask on the center of the face, adjusting for pitch and " "yaw. Outside of requirements for full head masking/training, this is likely " "to be the best choice.\n" @@ -150,25 +152,25 @@ msgid "" "mask appearing outside of the training area." msgstr "" -#: tools/mask/cli.py:179 +#: tools/mask/cli.py:181 msgid "" "Import only. The size, in pixels to internally store the mask at.\n" "The default is 128 which is fine for nearly all usecases. Larger sizes will " "result in larger alignments files and longer processing." msgstr "" -#: tools/mask/cli.py:187 tools/mask/cli.py:195 tools/mask/cli.py:209 -#: tools/mask/cli.py:223 tools/mask/cli.py:233 +#: tools/mask/cli.py:189 tools/mask/cli.py:197 tools/mask/cli.py:211 +#: tools/mask/cli.py:225 tools/mask/cli.py:235 msgid "output" msgstr "" -#: tools/mask/cli.py:189 +#: tools/mask/cli.py:191 msgid "" "Optional output location. If provided, a preview of the masks created will " "be output in the given folder." msgstr "" -#: tools/mask/cli.py:200 +#: tools/mask/cli.py:202 msgid "" "Apply gaussian blur to the mask output. Has the effect of smoothing the " "edges of the mask giving less of a hard edge. the size is in pixels. This " @@ -176,14 +178,14 @@ msgid "" "to the next odd number. NB: Only effects the output preview. Set to 0 for off" msgstr "" -#: tools/mask/cli.py:214 +#: tools/mask/cli.py:216 msgid "" "Helps reduce 'blotchiness' on some masks by making light shades white and " "dark shades black. Higher values will impact more of the mask. NB: Only " "effects the output preview. Set to 0 for off" msgstr "" -#: tools/mask/cli.py:225 +#: tools/mask/cli.py:227 msgid "" "R|How to format the output when processing is set to 'output'.\n" "L|combined: The image contains the face/frame, face mask and masked face.\n" @@ -191,7 +193,7 @@ msgid "" "L|mask: Only output the mask as a single channel image." msgstr "" -#: tools/mask/cli.py:235 +#: tools/mask/cli.py:237 msgid "" "R|Whether to output the whole frame or only the face box when using output " "processing. Only has an effect when using frames as input." diff --git a/tools/mask/cli.py b/tools/mask/cli.py index 4ec06e9ed3..cc14bb1b9d 100644 --- a/tools/mask/cli.py +++ b/tools/mask/cli.py @@ -153,9 +153,11 @@ def get_argument_list(): "default": "face", "group": _("import"), "help": _( - "R|Import only. The centering to use when importing masks. Note: For any job " - "other than 'import' this option is ignored as mask centering is handled " - "internally." + "R|Import/Output only. When importing masks, this is the centering to use. For " + "output this is only used for outputting custom imported masks, and should " + "correspond to the centering used when importing the mask. Note: For any job " + "other than 'import' and 'output' this option is ignored as mask centering is " + "handled internally." "\nL|face: Centers the mask on the center of the face, adjusting for " "pitch and yaw. Outside of requirements for full head masking/training, this " "is likely to be the best choice." diff --git a/tools/mask/mask_output.py b/tools/mask/mask_output.py index 344332a9b3..79ffb809e3 100644 --- a/tools/mask/mask_output.py +++ b/tools/mask/mask_output.py @@ -49,6 +49,7 @@ def __init__(self, arguments: Namespace, self._type: T.Literal["combined", "masked", "mask"] = arguments.output_type self._full_frame: bool = arguments.full_frame self._mask_type = arguments.masker + self._centering: CenteringType = arguments.centering self._input_is_faces = arguments.input_type == "faces" self._saver = self._set_saver(arguments.output, arguments.processing) @@ -445,6 +446,9 @@ def _get_mask_types(self, else: mask_types = [self._mask_type] + if self._mask_type == "custom": + mask_types.append(f"{self._mask_type}_{self._centering}") + final_masks = set() for idx in reversed(range(len(detected_faces))): face_idx, detected_face = detected_faces[idx] From b6ac7b8039a1474dfe1607601426289939b9b346 Mon Sep 17 00:00:00 2001 From: torzdf <36920800+torzdf@users.noreply.github.com> Date: Fri, 5 Jul 2024 18:46:10 +0100 Subject: [PATCH 2/2] bugfix: Manual tool. Allow working with image folders at EEN values > 1 --- docs/full/tools/manual.rst | 25 +- lib/image.py | 5 +- tools/manual/detected_faces.py | 56 ++- tools/manual/faceviewer/frame.py | 15 +- tools/manual/faceviewer/interact.py | 16 +- tools/manual/frameviewer/control.py | 46 +- tools/manual/frameviewer/editor/_base.py | 52 +- .../manual/frameviewer/editor/bounding_box.py | 25 +- .../manual/frameviewer/editor/extract_box.py | 23 +- tools/manual/frameviewer/editor/landmarks.py | 4 +- tools/manual/frameviewer/editor/mask.py | 49 +- tools/manual/frameviewer/frame.py | 57 ++- tools/manual/globals.py | 309 ++++++++++++ tools/manual/manual.py | 452 ++++++------------ 14 files changed, 657 insertions(+), 477 deletions(-) create mode 100644 tools/manual/globals.py diff --git a/docs/full/tools/manual.rst b/docs/full/tools/manual.rst index 9f3bed9d8f..4b35542559 100644 --- a/docs/full/tools/manual.rst +++ b/docs/full/tools/manual.rst @@ -23,11 +23,10 @@ The Manual Module is the main entry point into the Manual Editor Tool. .. autosummary:: :nosignatures: - + ~tools.manual.manual.Aligner ~tools.manual.manual.FrameLoader ~tools.manual.manual.Manual - ~tools.manual.manual.TkGlobals .. rubric:: Module @@ -43,7 +42,7 @@ detected_faces module .. autosummary:: :nosignatures: - + ~tools.manual.detected_faces.DetectedFaces ~tools.manual.detected_faces.FaceUpdate ~tools.manual.detected_faces.Filter @@ -55,6 +54,26 @@ detected_faces module :undoc-members: :show-inheritance: +globals module +============== + +.. rubric:: Module Summary + +.. autosummary:: + :nosignatures: + + ~tools.manual.globals.CurrentFrame + ~tools.manual.globals.TkGlobals + ~tools.manual.globals.TKVars + +.. rubric:: Module + +.. automodule:: tools.manual.globals + :members: + :undoc-members: + :show-inheritance: + + thumbnails module ================== diff --git a/lib/image.py b/lib/image.py index 4a2b524a15..897d8e2daa 100644 --- a/lib/image.py +++ b/lib/image.py @@ -1466,7 +1466,10 @@ def image_from_index(self, index): image = self._reader.get_data(index)[..., ::-1] filename = self._dummy_video_framename(index) else: - filename = self.file_list[index] + file_list = [f for idx, f in enumerate(self._file_list) + if idx not in self._skip_list] if self._skip_list else self._file_list + + filename = file_list[index] image = read_image(filename, raise_error=True) filename = os.path.basename(filename) logger.trace("index: %s, filename: %s image shape: %s", index, filename, image.shape) diff --git a/tools/manual/detected_faces.py b/tools/manual/detected_faces.py index ebd7218f81..7dcd90fc83 100644 --- a/tools/manual/detected_faces.py +++ b/tools/manual/detected_faces.py @@ -69,7 +69,6 @@ def __init__(self, logger.debug("Initialized %s", self.__class__.__name__) # <<<< PUBLIC PROPERTIES >>>> # - # << SUBCLASSES >> # @property def extractor(self) -> manual.Aligner: """ :class:`~tools.manual.manual.Aligner`: The pipeline for passing faces through the @@ -108,6 +107,11 @@ def tk_face_count_changed(self) -> tk.BooleanVar: return self._tk_vars["face_count_changed"] # << STATISTICS >> # + @property + def frame_list(self) -> list[str]: + """ list[str]: The list of all frame names that appear in the alignments file """ + return list(self._alignments.data) + @property def available_masks(self) -> dict[str, int]: """ dict[str, int]: The mask type names stored in the alignments; type as key with the @@ -343,7 +347,7 @@ def revert_to_saved(self, frame_index: int) -> None: self._tk_face_count_changed.set(True) else: self._tk_edited.set(True) - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) @classmethod def _add_remove_faces(cls, @@ -485,7 +489,7 @@ def __init__(self, detected_faces: DetectedFaces) -> None: def frame_meets_criteria(self) -> bool: """ bool: ``True`` if the current frame meets the selected filter criteria otherwise ``False`` """ - filter_mode = self._globals.filter_mode + filter_mode = self._globals.var_filter_mode.get() frame_faces = self._detected_faces.current_faces[self._globals.frame_index] distance = self._filter_distance @@ -505,7 +509,7 @@ def frame_meets_criteria(self) -> bool: def _filter_distance(self) -> float: """ float: The currently selected distance when Misaligned Faces filter is selected. """ try: - retval = self._globals.tk_filter_distance.get() + retval = self._globals.var_filter_distance.get() except tk.TclError: # Suppress error when distance box is empty retval = 0 @@ -514,22 +518,22 @@ def _filter_distance(self) -> float: @property def count(self) -> int: """ int: The number of frames that meet the filter criteria returned by - :attr:`~tools.manual.manual.TkGlobals.filter_mode`. """ + :attr:`~tools.manual.manual.TkGlobals.var_filter_mode.get()`. """ face_count_per_index = self._detected_faces.face_count_per_index - if self._globals.filter_mode == "No Faces": + if self._globals.var_filter_mode.get() == "No Faces": retval = sum(1 for fcount in face_count_per_index if fcount == 0) - elif self._globals.filter_mode == "Has Face(s)": + elif self._globals.var_filter_mode.get() == "Has Face(s)": retval = sum(1 for fcount in face_count_per_index if fcount != 0) - elif self._globals.filter_mode == "Multiple Faces": + elif self._globals.var_filter_mode.get() == "Multiple Faces": retval = sum(1 for fcount in face_count_per_index if fcount > 1) - elif self._globals.filter_mode == "Misaligned Faces": + elif self._globals.var_filter_mode.get() == "Misaligned Faces": distance = self._filter_distance retval = sum(1 for frame in self._detected_faces.current_faces if any(face.aligned.average_distance > distance for face in frame)) else: retval = len(face_count_per_index) logger.trace("filter mode: %s, frame count: %s", # type:ignore[attr-defined] - self._globals.filter_mode, retval) + self._globals.var_filter_mode.get(), retval) return retval @property @@ -554,22 +558,22 @@ def raw_indices(self) -> dict[T.Literal["frame", "face"], list[int]]: @property def frames_list(self) -> list[int]: """ list[int]: The list of frame indices that meet the filter criteria returned by - :attr:`~tools.manual.manual.TkGlobals.filter_mode`. """ + :attr:`~tools.manual.manual.TkGlobals.var_filter_mode.get()`. """ face_count_per_index = self._detected_faces.face_count_per_index - if self._globals.filter_mode == "No Faces": + if self._globals.var_filter_mode.get() == "No Faces": retval = [idx for idx, count in enumerate(face_count_per_index) if count == 0] - elif self._globals.filter_mode == "Multiple Faces": + elif self._globals.var_filter_mode.get() == "Multiple Faces": retval = [idx for idx, count in enumerate(face_count_per_index) if count > 1] - elif self._globals.filter_mode == "Has Face(s)": + elif self._globals.var_filter_mode.get() == "Has Face(s)": retval = [idx for idx, count in enumerate(face_count_per_index) if count != 0] - elif self._globals.filter_mode == "Misaligned Faces": + elif self._globals.var_filter_mode.get() == "Misaligned Faces": distance = self._filter_distance retval = [idx for idx, frame in enumerate(self._detected_faces.current_faces) if any(face.aligned.average_distance > distance for face in frame)] else: retval = list(range(len(face_count_per_index))) logger.trace("filter mode: %s, number_frames: %s", # type:ignore[attr-defined] - self._globals.filter_mode, len(retval)) + self._globals.var_filter_mode.get(), len(retval)) return retval @@ -677,7 +681,7 @@ def delete(self, frame_index: int, face_index: int) -> None: faces = self._faces_at_frame_index(frame_index) del faces[face_index] self._tk_face_count_changed.set(True) - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def bounding_box(self, frame_index: int, @@ -717,7 +721,7 @@ def bounding_box(self, face.top = pnt_y face.height = height face.add_landmarks_xy(self._extractor.get_landmarks(frame_index, face_index, aligner)) - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def landmark(self, frame_index: int, face_index: int, @@ -764,7 +768,7 @@ def landmark(self, face.landmarks_xy[idx] = lmk else: face.landmarks_xy[landmark_index] += (shift_x, shift_y) - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def landmarks(self, frame_index: int, face_index: int, shift_x: int, shift_y: int) -> None: """ Shift all of the landmarks and bounding box for the @@ -792,7 +796,7 @@ def landmarks(self, frame_index: int, face_index: int, shift_x: int, shift_y: in face.left += shift_x face.top += shift_y face.add_landmarks_xy(face.landmarks_xy + (shift_x, shift_y)) - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def landmarks_rotate(self, frame_index: int, @@ -818,7 +822,7 @@ def landmarks_rotate(self, rot_mat = cv2.getRotationMatrix2D(tuple(center.astype("float32")), angle, 1.) face.add_landmarks_xy(cv2.transform(np.expand_dims(face.landmarks_xy, axis=0), rot_mat).squeeze()) - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def landmarks_scale(self, frame_index: int, @@ -842,7 +846,7 @@ def landmarks_scale(self, """ face = self._faces_at_frame_index(frame_index)[face_index] face.add_landmarks_xy(((face.landmarks_xy - center) * scale) + center) - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def mask(self, frame_index: int, face_index: int, mask: np.ndarray, mask_type: str) -> None: """ Update the mask on an edit for the :class:`~lib.align.DetectedFace` object at @@ -862,7 +866,7 @@ def mask(self, frame_index: int, face_index: int, mask: np.ndarray, mask_type: s face = self._faces_at_frame_index(frame_index)[face_index] face.mask[mask_type].replace_mask(mask) self._tk_edited.set(True) - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def copy(self, frame_index: int, direction: T.Literal["prev", "next"]) -> None: """ Copy the alignments from the previous or next frame that has alignments @@ -903,7 +907,7 @@ def copy(self, frame_index: int, direction: T.Literal["prev", "next"]) -> None: faces.extend(copied) self._tk_face_count_changed.set(True) - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def post_edit_trigger(self, frame_index: int, face_index: int) -> None: """ Update the jpg thumbnail, the viewport thumbnail, the landmark masks and the aligned @@ -922,11 +926,11 @@ def post_edit_trigger(self, frame_index: int, face_index: int) -> None: face.clear_all_identities() aligned = AlignedFace(face.landmarks_xy, - image=self._globals.current_frame["image"], + image=self._globals.current_frame.image, centering="head", size=96) assert aligned.face is not None face.thumbnail = generate_thumbnail(aligned.face, size=96) - if self._globals.filter_mode == "Misaligned Faces": + if self._globals.var_filter_mode.get() == "Misaligned Faces": self._detected_faces.tk_face_count_changed.set(True) self._tk_edited.set(True) diff --git a/tools/manual/faceviewer/frame.py b/tools/manual/faceviewer/frame.py index 27737dbb30..5c6c8f024b 100644 --- a/tools/manual/faceviewer/frame.py +++ b/tools/manual/faceviewer/frame.py @@ -38,7 +38,7 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors Parameters ---------- - parent: :class:`ttk.PanedWindow` + parent: :class:`ttk.Frame` The paned window that the faces frame resides in tk_globals: :class:`~tools.manual.manual.TkGlobals` The tkinter variables that apply to the whole of the GUI @@ -48,7 +48,7 @@ class FacesFrame(ttk.Frame): # pylint:disable=too-many-ancestors The section of the Manual Tool that holds the frames viewer """ def __init__(self, - parent: ttk.PanedWindow, + parent: ttk.Frame, tk_globals: TkGlobals, detected_faces: DetectedFaces, display_frame: DisplayFrame) -> None: @@ -282,7 +282,7 @@ def __init__(self, parent: ttk.Frame, def face_size(self) -> int: """ int: The currently selected thumbnail size in pixels """ scaling = get_config().scaling_factor - size = self._sizes[self._globals.tk_faces_size.get().lower().replace(" ", "")] + size = self._sizes[self._globals.var_faces_size.get().lower().replace(" ", "")] scaled = size * scaling return int(round(scaled / 2) * 2) @@ -328,10 +328,11 @@ def _set_tk_callbacks(self, detected_faces: DetectedFaces): Updates the mask type when the user changes the selected mask types Toggles the face viewer annotations on an optional annotation button press. """ - for var in (self._globals.tk_faces_size, self._globals.tk_filter_mode): - var.trace_add("write", lambda *e, v=var: self.refresh_grid(v)) - var = detected_faces.tk_face_count_changed - var.trace_add("write", lambda *e, v=var: self.refresh_grid(v, retain_position=True)) + for strvar in (self._globals.var_faces_size, self._globals.var_filter_mode): + strvar.trace_add("write", lambda *e, v=strvar: self.refresh_grid(v)) + boolvar = detected_faces.tk_face_count_changed + boolvar.trace_add("write", + lambda *e, v=boolvar: self.refresh_grid(v, retain_position=True)) self._display_frame.tk_control_colors["Mesh"].trace_add( "write", lambda *e: self._update_mesh_color()) diff --git a/tools/manual/faceviewer/interact.py b/tools/manual/faceviewer/interact.py index ae8edf3707..124629320c 100644 --- a/tools/manual/faceviewer/interact.py +++ b/tools/manual/faceviewer/interact.py @@ -83,7 +83,7 @@ def on_hover(self, event: tk.Event | None) -> None: is_zoomed = self._globals.is_zoomed if (-1 in face or (frame_idx == self._globals.frame_index and (not is_zoomed or - (is_zoomed and face_idx == self._globals.tk_face_index.get())))): + (is_zoomed and face_idx == self._globals.face_index)))): self._clear() self._canvas.config(cursor="") self._current_frame_index = None @@ -125,14 +125,14 @@ def _select_frame(self) -> None: if frame_id is None or (frame_id == self._globals.frame_index and not is_zoomed): return face_idx = self._current_face_index if is_zoomed else 0 - self._globals.tk_face_index.set(face_idx) + self._globals.set_face_index(face_idx) transport_id = self._grid.transport_index_from_frame(frame_id) logger.trace("frame_index: %s, transport_id: %s, face_idx: %s", frame_id, transport_id, face_idx) if transport_id is None: return self._navigation.stop_playback() - self._globals.tk_transport_index.set(transport_id) + self._globals.var_transport_index.set(transport_id) self._viewport.move_active_to_top() self.on_hover(None) @@ -192,8 +192,8 @@ def __init__(self, viewport: Viewport, tk_edited_variable: tk.BooleanVar) -> Non "edited": tk_edited_variable} self._assets: Asset = Asset([], [], [], []) - self._globals.tk_update_active_viewport.trace_add("write", - lambda *e: self._reload_callback()) + self._globals.var_update_active_viewport.trace_add("write", + lambda *e: self._reload_callback()) tk_edited_variable.trace_add("write", lambda *e: self._update_on_edit()) logger.debug("Initialized: %s", self.__class__.__name__) @@ -205,7 +205,7 @@ def frame_index(self) -> int: @property def current_frame(self) -> np.ndarray: """ :class:`numpy.ndarray`: A BGR version of the frame currently being displayed. """ - return self._globals.current_frame["image"] + return self._globals.current_frame.image @property def _size(self) -> int: @@ -221,7 +221,7 @@ def _optional_annotations(self) -> dict[T.Literal["mesh", "mask"], bool]: def _reload_callback(self) -> None: """ If a frame has changed, triggering the variable, then update the active frame. Return having done nothing if the variable is resetting. """ - if self._globals.tk_update_active_viewport.get(): + if self._globals.var_update_active_viewport.get(): self.reload_annotations() def reload_annotations(self) -> None: @@ -249,7 +249,7 @@ def reload_annotations(self) -> None: self._update_face() self._canvas.tag_raise("active_highlighter") - self._globals.tk_update_active_viewport.set(False) + self._globals.var_update_active_viewport.set(False) self._last_execution["frame_index"] = self.frame_index def _clear_previous(self) -> None: diff --git a/tools/manual/frameviewer/control.py b/tools/manual/frameviewer/control.py index 8230f24b00..8315cf6a3a 100644 --- a/tools/manual/frameviewer/control.py +++ b/tools/manual/frameviewer/control.py @@ -48,11 +48,11 @@ def nav_scale_callback(self, *args, reset_progress=True): # pylint:disable=unus frame_count = self._det_faces.filter.count if self._current_nav_frame_count == frame_count: logger.trace("Filtered count has not changed. Returning") - if self._globals.tk_filter_mode.get() == "Misaligned Faces": + if self._globals.var_filter_mode.get() == "Misaligned Faces": self._det_faces.tk_face_count_changed.set(True) self._update_total_frame_count() if reset_progress: - self._globals.tk_transport_index.set(0) + self._globals.var_transport_index.set(0) def _update_total_frame_count(self, *args): # pylint:disable=unused-argument """ Update the displayed number of total frames that meet the current filter criteria. @@ -70,7 +70,7 @@ def _update_total_frame_count(self, *args): # pylint:disable=unused-argument logger.debug("Filtered frame count has changed. Updating from %s to %s", self._current_nav_frame_count, frame_count) self._nav["scale"].config(to=max_frame) - self._nav["label"].config(text="/{}".format(max_frame)) + self._nav["label"].config(text=f"/{max_frame}") state = "disabled" if max_frame == 0 else "normal" self._nav["entry"].config(state=state) @@ -106,7 +106,7 @@ def increment_frame(self, frame_count=None, is_playing=False): logger.debug("End of Stream. Not incrementing") self.stop_playback() return - self._globals.tk_transport_index.set(min(position + 1, max(0, frame_count - 1))) + self._globals.var_transport_index.set(min(position + 1, max(0, frame_count - 1))) def decrement_frame(self): """ Update The frame navigation position to the previous frame based on filter. """ @@ -116,11 +116,11 @@ def decrement_frame(self): if not face_count_change and (self._det_faces.filter.count == 0 or position == 0): logger.debug("End of Stream. Not decrementing") return - self._globals.tk_transport_index.set(min(max(0, self._det_faces.filter.count - 1), - max(0, position - 1))) + self._globals.var_transport_index.set(min(max(0, self._det_faces.filter.count - 1), + max(0, position - 1))) def _get_safe_frame_index(self): - """ Obtain the current frame position from the tk_transport_index variable in + """ Obtain the current frame position from the var_transport_index variable in a safe manner (i.e. handle for non-numeric) Returns @@ -129,32 +129,32 @@ def _get_safe_frame_index(self): The current transport frame index """ try: - retval = self._globals.tk_transport_index.get() + retval = self._globals.var_transport_index.get() except tk.TclError as err: if "expected floating-point" not in str(err): raise - val = str(err).split(" ")[-1].replace("\"", "") + val = str(err).rsplit(" ", maxsplit=1)[-1].replace("\"", "") retval = "".join(ch for ch in val if ch.isdigit()) retval = 0 if not retval else int(retval) - self._globals.tk_transport_index.set(retval) + self._globals.var_transport_index.set(retval) return retval def goto_first_frame(self): """ Go to the first frame that meets the filter criteria. """ self.stop_playback() - position = self._globals.tk_transport_index.get() + position = self._globals.var_transport_index.get() if position == 0: return - self._globals.tk_transport_index.set(0) + self._globals.var_transport_index.set(0) def goto_last_frame(self): """ Go to the last frame that meets the filter criteria. """ self.stop_playback() - position = self._globals.tk_transport_index.get() + position = self._globals.var_transport_index.get() frame_count = self._det_faces.filter.count if position == frame_count - 1: return - self._globals.tk_transport_index.set(frame_count - 1) + self._globals.var_transport_index.set(frame_count - 1) class BackgroundImage(): @@ -190,7 +190,7 @@ def refresh(self, view_mode): """ self._switch_image(view_mode) logger.trace("Updating background frame") - getattr(self, "_update_tk_{}".format(self._current_view_mode))() + getattr(self, f"_update_tk_{self._current_view_mode}")() def _switch_image(self, view_mode): """ Switch the image between the full frame image and the zoomed face image. @@ -206,10 +206,10 @@ def _switch_image(self, view_mode): self._zoomed_centering = self._canvas.active_editor.zoomed_centering logger.trace("Switching background image from '%s' to '%s'", self._current_view_mode, view_mode) - img = getattr(self, "_tk_{}".format(view_mode)) + img = getattr(self, f"_tk_{view_mode}") self._canvas.itemconfig(self._image, image=img) - self._globals.tk_is_zoomed.set(view_mode == "face") - self._globals.tk_face_index.set(0) + self._globals.set_zoomed(view_mode == "face") + self._globals.set_face_index(0) def _update_tk_face(self): """ Update the currently zoomed face. """ @@ -239,14 +239,14 @@ def _get_zoomed_face(self): if face_idx + 1 > faces_in_frame: logger.debug("Resetting face index to 0 for more faces in frame than current index: (" "faces_in_frame: %s, zoomed_face_index: %s", faces_in_frame, face_idx) - self._globals.tk_face_index.set(0) + self._globals.set_face_index(0) if faces_in_frame == 0: face = np.ones((size, size, 3), dtype="uint8") else: det_face = self._det_faces.current_faces[frame_idx][face_idx] face = AlignedFace(det_face.landmarks_xy, - image=self._globals.current_frame["image"], + image=self._globals.current_frame.image, centering=self._zoomed_centering, size=size).face logger.trace("face shape: %s", face.shape) @@ -254,9 +254,9 @@ def _get_zoomed_face(self): def _update_tk_frame(self): """ Place the currently held frame into :attr:`_tk_frame`. """ - img = cv2.resize(self._globals.current_frame["image"], - self._globals.current_frame["display_dims"], - interpolation=self._globals.current_frame["interpolation"])[..., 2::-1] + img = cv2.resize(self._globals.current_frame.image, + self._globals.current_frame.display_dims, + interpolation=self._globals.current_frame.interpolation)[..., 2::-1] padding = self._get_padding(img.shape[:2]) if any(padding): img = cv2.copyMakeBorder(img, *padding, cv2.BORDER_CONSTANT) diff --git a/tools/manual/frameviewer/editor/_base.py b/tools/manual/frameviewer/editor/_base.py index c4e9ebb6bf..d295c0d847 100644 --- a/tools/manual/frameviewer/editor/_base.py +++ b/tools/manual/frameviewer/editor/_base.py @@ -41,9 +41,9 @@ def __init__(self, canvas, detected_faces, control_text="", key_bindings=None): self._globals = canvas._globals self._det_faces = detected_faces - self._current_color = dict() + self._current_color = {} self._actions = OrderedDict() - self._controls = dict(header=control_text, controls=[]) + self._controls = {"header": control_text, "controls": []} self._add_key_bindings(key_bindings) self._add_actions() @@ -51,7 +51,7 @@ def __init__(self, canvas, detected_faces, control_text="", key_bindings=None): self._add_annotation_format_controls() self._mouse_location = None - self._drag_data = dict() + self._drag_data = {} self._drag_callback = None self.bind_mouse_motion() logger.debug("Initialized %s", self.__class__.__name__) @@ -80,7 +80,7 @@ def _is_active(self): def view_mode(self): """ ["frame", "face"]: The view mode for the currently selected editor. If the editor does not have a view mode that can be updated, then `"frame"` will be returned. """ - tk_var = self._actions.get("magnify", dict()).get("tk_var", None) + tk_var = self._actions.get("magnify", {}).get("tk_var", None) retval = "frame" if tk_var is None or not tk_var.get() else "face" return retval @@ -106,7 +106,7 @@ def _zoomed_dims(self): @property def _control_vars(self): """ dict: The tk control panel variables for the currently selected editor. """ - return self._canvas.control_tk_vars.get(self.__class__.__name__, dict()) + return self._canvas.control_tk_vars.get(self.__class__.__name__, {}) @property def controls(self): @@ -155,7 +155,7 @@ def _add_key_bindings(self, key_bindings): for key, method in key_bindings.items(): logger.debug("Binding key '%s' to method %s for editor '%s'", key, method, self.__class__.__name__) - self._canvas.key_bindings.setdefault(key, dict())["bound_to"] = None + self._canvas.key_bindings.setdefault(key, {})["bound_to"] = None self._canvas.key_bindings[key][self.__class__.__name__] = method @staticmethod @@ -187,7 +187,7 @@ def _get_anchor_points(bounding_box): for cnr in bounding_box) return display_anchors, grab_anchors - def update_annotation(self): # pylint:disable=no-self-use + def update_annotation(self): """ Update the display annotations for the current objects. Override for specific editors. @@ -233,7 +233,7 @@ def _object_tracker(self, key, object_type, face_index, """ object_color_keys = self._get_object_color_keys(key, object_type) tracking_id = "_".join((key, str(face_index))) - face_tag = "face_{}".format(face_index) + face_tag = f"face_{face_index}" face_objects = set(self._canvas.find_withtag(face_tag)) annotation_objects = set(self._canvas.find_withtag(key)) existing_object = tuple(face_objects.intersection(annotation_objects)) @@ -311,7 +311,7 @@ def _add_new_object(self, key, object_type, face_index, coordinates, object_kwar coordinates, object_kwargs) object_kwargs["tags"] = self._set_object_tags(face_index, key) item_id = getattr(self._canvas, - "create_{}".format(object_type))(*coordinates, **object_kwargs) + f"create_{object_type}")(*coordinates, **object_kwargs) return item_id def _set_object_tags(self, face_index, key): @@ -329,17 +329,17 @@ def _set_object_tags(self, face_index, key): list The generated tags for the current object """ - tags = ["face_{}".format(face_index), + tags = [f"face_{face_index}", self.__class__.__name__, - "{}_face_{}".format(self.__class__.__name__, face_index), + f"{self.__class__.__name__}_face_{face_index}", key, - "{}_face_{}".format(key, face_index)] + f"{key}_face_{face_index}"] if "_" in key: split_key = key.split("_") if split_key[-1].isdigit(): base_tag = "_".join(split_key[:-1]) tags.append(base_tag) - tags.append("{}_face_{}".format(base_tag, face_index)) + tags.append(f"{base_tag}_face_{face_index}") return tags def _update_existing_object(self, item_id, coordinates, object_kwargs, @@ -366,11 +366,11 @@ def _update_existing_object(self, item_id, coordinates, object_kwargs, """ update_color = (object_color_keys and object_kwargs[object_color_keys[0]] != self._current_color[tracking_id]) - update_kwargs = dict(state=object_kwargs.get("state", "normal")) + update_kwargs = {"state": object_kwargs.get("state", "normal")} if update_color: for key in object_color_keys: update_kwargs[key] = object_kwargs[object_color_keys[0]] - if self._canvas.type(item_id) == "image" and "image" in object_kwargs: + if self._canvas.type(item_id) == "image" and "image" in object_kwargs: # noqa:E721 update_kwargs["image"] = object_kwargs["image"] logger.trace("Updating coordinates: (item_id: '%s', object_kwargs: %s, " "coordinates: %s, update_kwargs: %s", item_id, object_kwargs, @@ -433,7 +433,7 @@ def _drag_start(self, event): # pylint:disable=unused-argument The tkinter mouse event. Unused but for default action, but available for editor specific actions """ - self._drag_data = dict() + self._drag_data = {} self._drag_callback = None def _drag(self, event): @@ -461,7 +461,7 @@ def _drag_stop(self, event): # pylint:disable=unused-argument event: :class:`tkinter.Event` The tkinter mouse event. Unused but required """ - self._drag_data = dict() + self._drag_data = {} def _scale_to_display(self, points): """ Scale and offset the given points to the current display scale and offset values. @@ -476,7 +476,7 @@ def _scale_to_display(self, points): :class:`numpy.ndarray` The adjusted x, y co-ordinates for display purposes rounded to the nearest integer """ - retval = np.rint((points * self._globals.current_frame["scale"]) + retval = np.rint((points * self._globals.current_frame.scale) + self._canvas.offset).astype("int32") logger.trace("Original points: %s, scaled points: %s", points, retval) return retval @@ -499,7 +499,7 @@ def scale_from_display(self, points, do_offset=True): integer """ offset = self._canvas.offset if do_offset else (0, 0) - retval = np.rint((points - offset) / self._globals.current_frame["scale"]).astype("int32") + retval = np.rint((points - offset) / self._globals.current_frame.scale).astype("int32") logger.trace("Original points: %s, scaled points: %s", points, retval) return retval @@ -532,7 +532,11 @@ def _add_action(self, title, icon, helptext, group=None, hotkey=None): Default: ``None`` """ var = tk.BooleanVar() - action = dict(icon=icon, helptext=helptext, group=group, tk_var=var, hotkey=hotkey) + action = {"icon": icon, + "helptext": helptext, + "group": group, + "tk_var": var, + "hotkey": hotkey} logger.debug("Adding action: %s", action) self._actions[title] = action @@ -567,7 +571,7 @@ def _add_control(self, option, global_control=False): group_key = "none" if group_key == "_master" else group_key annotation_key = option.title.replace(" ", "") self._canvas.control_tk_vars.setdefault( - editor_key, dict()).setdefault(group_key, dict())[annotation_key] = option.tk_var + editor_key, {}).setdefault(group_key, {})[annotation_key] = option.tk_var def _add_annotation_format_controls(self): """ Add the annotation display (color/size) controls to :attr:`_annotation_formats`. @@ -594,7 +598,7 @@ def _add_annotation_format_controls(self): default=self._default_colors[annotation_key], helptext="Set the annotation color") colors.set(self._default_colors[annotation_key]) - self._annotation_formats.setdefault(annotation_key, dict())["color"] = colors + self._annotation_formats.setdefault(annotation_key, {})["color"] = colors self._annotation_formats[annotation_key]["mask_opacity"] = opacity for editor in editors: @@ -627,4 +631,6 @@ def _add_actions(self): """ Add the optional action buttons to the viewer. Current actions are Zoom. """ self._add_action("magnify", "zoom", _("Magnify/Demagnify the View"), group=None, hotkey="M") - self._actions["magnify"]["tk_var"].trace("w", lambda *e: self._globals.tk_update.set(True)) + self._actions["magnify"]["tk_var"].trace_add( + "write", + lambda *e: self._globals.var_full_update.set(True)) diff --git a/tools/manual/frameviewer/editor/bounding_box.py b/tools/manual/frameviewer/editor/bounding_box.py index f3bbd78d00..d546feb172 100644 --- a/tools/manual/frameviewer/editor/bounding_box.py +++ b/tools/manual/frameviewer/editor/bounding_box.py @@ -105,7 +105,7 @@ def update_annotation(self): for idx, face in enumerate(self._face_iterator): box = np.array([(face.left, face.top), (face.right, face.bottom)]) box = self._scale_to_display(box).astype("int32").flatten() - kwargs = dict(outline=color, width=1) + kwargs = {"outline": color, "width": 1} logger.trace("frame_index: %s, face_index: %s, box: %s, kwargs: %s", self._globals.frame_index, idx, box, kwargs) self._object_tracker(key, "rectangle", idx, box, kwargs) @@ -137,10 +137,10 @@ def _update_anchor_annotation(self, face_index, bounding_box, color): (bounding_box[2], bounding_box[3]), (bounding_box[0], bounding_box[3]))) for idx, (anc_dsp, anc_grb) in enumerate(zip(*anchor_points)): - dsp_kwargs = dict(outline=color, fill=fill_color, width=1) - grb_kwargs = dict(outline="", fill="", width=1, activefill=activefill_color) - dsp_key = "bb_anc_dsp_{}".format(idx) - grb_key = "bb_anc_grb_{}".format(idx) + dsp_kwargs = {"outline": color, "fill": fill_color, "width": 1} + grb_kwargs = {"outline": '', "fill": '', "width": 1, "activefill": activefill_color} + dsp_key = f"bb_anc_dsp_{idx}" + grb_key = f"bb_anc_grb_{idx}" self._object_tracker(dsp_key, "oval", face_index, anc_dsp, dsp_kwargs) self._object_tracker(grb_key, "oval", face_index, anc_grb, grb_kwargs) logger.trace("Updated bounding box anchor annotations") @@ -193,8 +193,9 @@ def _check_cursor_anchors(self): corner_idx = int(next(tag for tag in tags if tag.startswith("bb_anc_grb_") and "face_" not in tag).split("_")[-1]) - self._canvas.config(cursor="{}_{}_corner".format(*self._corner_order[corner_idx])) - self._mouse_location = ("anchor", "{}_{}".format(face_idx, corner_idx)) + pos_x, pos_y = self._corner_order[corner_idx] + self._canvas.config(cursor=f"{pos_x}_{pos_y}_corner") + self._mouse_location = ("anchor", f"{face_idx}_{corner_idx}") return True def _check_cursor_bounding_box(self, event): @@ -242,7 +243,7 @@ def _check_cursor_image(self, event): """ if self._globals.frame_index == -1: return False - display_dims = self._globals.current_frame["display_dims"] + display_dims = self._globals.current_frame.display_dims if (self._canvas.offset[0] <= event.x <= display_dims[0] + self._canvas.offset[0] and self._canvas.offset[1] <= event.y <= display_dims[1] + self._canvas.offset[1]): self._canvas.config(cursor="plus") @@ -275,7 +276,7 @@ def _drag_start(self, event): The tkinter mouse event. """ if self._mouse_location is None: - self._drag_data = dict() + self._drag_data = {} self._drag_callback = None return if self._mouse_location[0] == "anchor": @@ -315,7 +316,7 @@ def _create_new_bounding_box(self, event): event: :class:`tkinter.Event` The tkinter mouse event """ - size = min(self._globals.current_frame["display_dims"]) // 8 + size = min(self._globals.current_frame.display_dims) // 8 box = (event.x - size, event.y - size, event.x + size, event.y + size) logger.debug("Creating new bounding box: %s ", box) self._det_faces.update.add(self._globals.frame_index, *self._coords_to_bounding_box(box)) @@ -329,7 +330,7 @@ def _resize(self, event): The tkinter mouse event. """ face_idx = int(self._mouse_location[1].split("_")[0]) - face_tag = "bb_box_face_{}".format(face_idx) + face_tag = f"bb_box_face_{face_idx}" box = self._canvas.coords(face_tag) logger.trace("Face Index: %s, Corner Index: %s. Original ROI: %s", face_idx, self._drag_data["corner"], box) @@ -361,7 +362,7 @@ def _move(self, event): face_idx = int(self._mouse_location[1]) shift = (event.x - self._drag_data["current_location"][0], event.y - self._drag_data["current_location"][1]) - face_tag = "bb_box_face_{}".format(face_idx) + face_tag = f"bb_box_face_{face_idx}" coords = np.array(self._canvas.coords(face_tag)) + (*shift, *shift) logger.trace("face_tag: %s, shift: %s, new co-ords: %s", face_tag, shift, coords) self._det_faces.update.bounding_box(self._globals.frame_index, diff --git a/tools/manual/frameviewer/editor/extract_box.py b/tools/manual/frameviewer/editor/extract_box.py index f49acc055b..ffe8bf4734 100644 --- a/tools/manual/frameviewer/editor/extract_box.py +++ b/tools/manual/frameviewer/editor/extract_box.py @@ -61,9 +61,9 @@ def update_annotation(self): aligned = AlignedFace(face.landmarks_xy, centering="face") box = self._scale_to_display(aligned.original_roi).flatten() top_left = box[:2] - 10 - kwargs = dict(fill=color, font=("Default", 20, "bold"), text=str(idx)) + kwargs = {"fill": color, "font": ('Default', 20, 'bold'), "text": str(idx)} self._object_tracker("eb_text", "text", idx, top_left, kwargs) - kwargs = dict(fill="", outline=color, width=1) + kwargs = {"fill": '', "outline": color, "width": 1} self._object_tracker("eb_box", "polygon", idx, box, kwargs) self._update_anchor_annotation(idx, box, color) logger.trace("Updated extract box annotations") @@ -93,10 +93,10 @@ def _update_anchor_annotation(self, face_index, extract_box, color): extract_box[4:6], extract_box[6:])) for idx, (anc_dsp, anc_grb) in enumerate(zip(*anchor_points)): - dsp_kwargs = dict(outline=color, fill=fill_color, width=1) - grb_kwargs = dict(outline="", fill="", width=1, activefill=activefill_color) - dsp_key = "eb_anc_dsp_{}".format(idx) - grb_key = "eb_anc_grb_{}".format(idx) + dsp_kwargs = {"outline": color, "fill": fill_color, "width": 1} + grb_kwargs = {"outline": '', "fill": '', "width": 1, "activefill": activefill_color} + dsp_key = f"eb_anc_dsp_{idx}" + grb_key = f"eb_anc_grb_{idx}" self._object_tracker(dsp_key, "oval", face_index, anc_dsp, dsp_kwargs) self._object_tracker(grb_key, "oval", face_index, anc_grb, grb_kwargs) logger.trace("Updated extract box anchor annotations") @@ -143,7 +143,8 @@ def _check_cursor_anchors(self): if tag.startswith("eb_anc_grb_") and "face_" not in tag).split("_")[-1]) - self._canvas.config(cursor="{}_{}_corner".format(*self._corner_order[corner_idx])) + pos_x, pos_y = self._corner_order[corner_idx] + self._canvas.config(cursor=f"{pos_x}_{pos_y}_corner") self._mouse_location = ("anchor", face_idx, corner_idx) return True @@ -222,11 +223,11 @@ def _drag_start(self, event): The tkinter mouse event. """ if self._mouse_location is None: - self._drag_data = dict() + self._drag_data = {} self._drag_callback = None return self._drag_data["current_location"] = np.array((event.x, event.y)) - callback = dict(anchor=self._resize, rotate=self._rotate, box=self._move) + callback = {"anchor": self._resize, "rotate": self._rotate, "box": self._move} self._drag_callback = callback[self._mouse_location[0]] def _drag_stop(self, event): # pylint:disable=unused-argument @@ -270,7 +271,7 @@ def _resize(self, event): The tkinter mouse event. """ face_idx = self._mouse_location[1] - face_tag = "eb_box_face_{}".format(face_idx) + face_tag = f"eb_box_face_{face_idx}" position = np.array((event.x, event.y)) box = np.array(self._canvas.coords(face_tag)) center = np.array((sum(box[0::2]) / 4, sum(box[1::2]) / 4)) @@ -365,7 +366,7 @@ def _rotate(self, event): The tkinter mouse event. """ face_idx = self._mouse_location[1] - face_tag = "eb_box_face_{}".format(face_idx) + face_tag = f"eb_box_face_{face_idx}" box = np.array(self._canvas.coords(face_tag)) position = np.array((event.x, event.y)) diff --git a/tools/manual/frameviewer/editor/landmarks.py b/tools/manual/frameviewer/editor/landmarks.py index 452e426ab0..e59517e7b0 100644 --- a/tools/manual/frameviewer/editor/landmarks.py +++ b/tools/manual/frameviewer/editor/landmarks.py @@ -36,7 +36,7 @@ def __init__(self, canvas, detected_faces): super().__init__(canvas, detected_faces, control_text) # Clear selection box on an editor or frame change self._canvas._tk_action_var.trace("w", lambda *e: self._reset_selection()) - self._globals.tk_frame_index.trace("w", lambda *e: self._reset_selection()) + self._globals.var_frame_index.trace_add("write", lambda *e: self._reset_selection()) def _add_actions(self): """ Add the optional action buttons to the viewer. Current actions are Point, Select @@ -55,7 +55,7 @@ def _toggle_zoom(self, *args): # pylint:disable=unused-argument tkinter callback arguments. Required but unused. """ self._reset_selection() - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def _reset_selection(self, event=None): # pylint:disable=unused-argument """ Reset the selection box and the selected landmark annotations. """ diff --git a/tools/manual/frameviewer/editor/mask.py b/tools/manual/frameviewer/editor/mask.py index 66372ce522..fec2c92d13 100644 --- a/tools/manual/frameviewer/editor/mask.py +++ b/tools/manual/frameviewer/editor/mask.py @@ -82,7 +82,9 @@ def _add_actions(self): group=None, hotkey="M") self._add_action("draw", "draw", _("Draw Tool"), group="paint", hotkey="D") self._add_action("erase", "erase", _("Erase Tool"), group="paint", hotkey="E") - self._actions["magnify"]["tk_var"].trace("w", lambda *e: self._globals.tk_update.set(True)) + self._actions["magnify"]["tk_var"].trace( + "w", + lambda *e: self._globals.var_full_update.set(True)) def _add_controls(self): """ Add the mask specific control panel controls. @@ -143,21 +145,21 @@ def _on_mask_type_change(self): mask_type = self._control_vars["display"]["MaskType"].get() if mask_type == self._mask_type: return - self._meta = dict(position=self._globals.frame_index) + self._meta = {"position": self._globals.frame_index} self._mask_type = mask_type - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def hide_annotation(self, tag=None): """ Clear the mask :attr:`_meta` dict when hiding the annotation. """ super().hide_annotation() - self._meta = dict() + self._meta = {} def update_annotation(self): """ Update the mask annotation with the latest mask. """ position = self._globals.frame_index if position != self._meta.get("position", -1): # Reset meta information when moving to a new frame - self._meta = dict(position=position) + self._meta = {"position": position} key = self.__class__.__name__ mask_type = self._control_vars["display"]["MaskType"].get().lower() color = self._control_color[1:] @@ -221,21 +223,21 @@ def _set_full_frame_meta(self, mask, mask_scale): - slices: The (`x`, `y`) slice objects required to extract the mask ROI from the full frame """ - frame_dims = self._globals.current_frame["display_dims"] + frame_dims = self._globals.current_frame.display_dims scaled_mask_roi = np.rint(mask.original_roi * - self._globals.current_frame["scale"]).astype("int32") + self._globals.current_frame.scale).astype("int32") # Scale and clip the ROI to fit within display frame boundaries clipped_roi = scaled_mask_roi.clip(min=(0, 0), max=frame_dims) # Obtain min and max points to get ROI as a rectangle - min_max = dict(min=clipped_roi.min(axis=0), max=clipped_roi.max(axis=0)) + min_max = {"min": clipped_roi.min(axis=0), "max": clipped_roi.max(axis=0)} # Create a bounding box rectangle ROI roi_dims = np.rint((min_max["max"][1] - min_max["min"][1], min_max["max"][0] - min_max["min"][0])).astype("uint16") - roi = dict(mask=np.zeros(roi_dims, dtype="uint8")[..., None], - corners=np.expand_dims(scaled_mask_roi - min_max["min"], axis=0)) + roi = {"mask": np.zeros(roi_dims, dtype="uint8")[..., None], + "corners": np.expand_dims(scaled_mask_roi - min_max["min"], axis=0)} # Block out areas outside of the actual mask ROI polygon cv2.fillPoly(roi["mask"], roi["corners"], 255) logger.trace("Setting Full Frame mask ROI. shape: %s", roi["mask"].shape) @@ -246,8 +248,8 @@ def _set_full_frame_meta(self, mask, mask_scale): # Adjust affine matrix for internal mask size and display dimensions adjustments = (np.array([[mask_scale, 0., 0.], [0., mask_scale, 0.]]), - np.array([[1 / self._globals.current_frame["scale"], 0., 0.], - [0., 1 / self._globals.current_frame["scale"], 0.], + np.array([[1 / self._globals.current_frame.scale, 0., 0.], + [0., 1 / self._globals.current_frame.scale, 0.], [0., 0., 1.]])) in_matrix = np.dot(adjustments[0], np.concatenate((mask.affine_matrix, np.array([[0., 0., 1.]])))) @@ -285,7 +287,7 @@ def _update_mask_image(self, key, face_index, rgb_color, opacity): top_left = self._zoomed_roi[:2] # Hide all masks and only display selected self._canvas.itemconfig("Mask", state="hidden") - self._canvas.itemconfig("Mask_face_{}".format(face_index), state="normal") + self._canvas.itemconfig(f"Mask_face_{face_index}", state="normal") else: display_image = self._update_mask_image_full_frame(mask, rgb_color, face_index) top_left = self._meta["top_left"][face_index] @@ -305,7 +307,7 @@ def _update_mask_image(self, key, face_index, rgb_color, opacity): "image", face_index, top_left, - dict(image=self._tk_faces[face_index], anchor=tk.NW)) + {"image": self._tk_faces[face_index], "anchor": tk.NW}) def _update_mask_image_zoomed(self, mask, rgb_color): """ Update the mask image when zoomed in. @@ -346,7 +348,7 @@ def _update_mask_image_full_frame(self, mask, rgb_color, face_index): :class: `PIL.Image` The full frame mask image formatted for display """ - frame_dims = self._globals.current_frame["display_dims"] + frame_dims = self._globals.current_frame.display_dims frame = np.zeros(frame_dims + (1, ), dtype="uint8") interpolator = self._meta["interpolator"][face_index] slices = self._meta["slices"][face_index] @@ -377,13 +379,13 @@ def _update_roi_box(self, mask, face_index, color): else: box = self._scale_to_display(mask.original_roi).flatten() top_left = box[:2] - 10 - kwargs = dict(fill=color, font=("Default", 20, "bold"), text=str(face_index)) + kwargs = {"fill": color, "font": ("Default", 20, "bold"), "text": str(face_index)} self._object_tracker("mask_text", "text", face_index, top_left, kwargs) - kwargs = dict(fill="", outline=color, width=1) + kwargs = {"fill": "", "outline": color, "width": 1} self._object_tracker("mask_roi", "polygon", face_index, box, kwargs) if self._globals.is_zoomed: # Raise box above zoomed image - self._canvas.tag_raise("mask_roi_face_{}".format(face_index)) + self._canvas.tag_raise(f"mask_roi_face_{face_index}") # << MOUSE HANDLING >> # Mouse cursor display @@ -450,7 +452,7 @@ def _drag_start(self, event, control_click=False): # pylint:disable=arguments-d """ face_idx = self._mouse_location[1] if face_idx is None: - self._drag_data = dict() + self._drag_data = {} self._drag_callback = None else: self._drag_data["starting_location"] = np.array((event.x, event.y)) @@ -532,7 +534,7 @@ def _drag_stop(self, event): if np.array_equal(self._drag_data["starting_location"], location[0]): self._get_cursor_shape_mark(self._meta["mask"][face_idx], location, face_idx) self._mask_to_alignments(face_idx) - self._drag_data = dict() + self._drag_data = {} self._update_cursor(event) def _get_cursor_shape_mark(self, img, location, face_idx): @@ -562,11 +564,10 @@ def _get_cursor_shape_mark(self, img, location, face_idx): else: cv2.circle(img, tuple(points), radius, color, thickness=-1) - def _get_cursor_shape(self, x1=0, y1=0, x2=0, y2=0, outline="black", state="hidden"): + def _get_cursor_shape(self, x_1=0, y_1=0, x_2=0, y_2=0, outline="black", state="hidden"): if self._cursor_shape_name == "Rectangle": - return self._canvas.create_rectangle(x1, y1, x2, y2, outline=outline, state=state) - else: - return self._canvas.create_oval(x1, y1, x2, y2, outline=outline, state=state) + return self._canvas.create_rectangle(x_1, y_1, x_2, y_2, outline=outline, state=state) + return self._canvas.create_oval(x_1, y_1, x_2, y_2, outline=outline, state=state) def _mask_to_alignments(self, face_index): """ Update the annotated mask to alignments. diff --git a/tools/manual/frameviewer/frame.py b/tools/manual/frameviewer/frame.py index 0b761b543b..e83f3492f8 100644 --- a/tools/manual/frameviewer/frame.py +++ b/tools/manual/frameviewer/frame.py @@ -146,41 +146,41 @@ def _add_nav(self): lbl_frame.pack(side=tk.RIGHT) tbox = ttk.Entry(lbl_frame, width=7, - textvariable=self._globals.tk_transport_index, + textvariable=self._globals.var_transport_index, justify=tk.RIGHT) tbox.pack(padx=0, side=tk.LEFT) lbl = ttk.Label(lbl_frame, text=f"/{max_frame}") lbl.pack(side=tk.RIGHT) cmd = partial(set_slider_rounding, - var=self._globals.tk_transport_index, + var=self._globals.var_transport_index, d_type=int, round_to=1, min_max=(0, max_frame)) nav = ttk.Scale(frame, - variable=self._globals.tk_transport_index, + variable=self._globals.var_transport_index, from_=0, to=max_frame, command=cmd) nav.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) - self._globals.tk_transport_index.trace("w", self._set_frame_index) + self._globals.var_transport_index.trace_add("write", self._set_frame_index) return {"entry": tbox, "scale": nav, "label": lbl} def _set_frame_index(self, *args): # pylint:disable=unused-argument """ Set the actual frame index based on current slider position and filter mode. """ try: - slider_position = self._globals.tk_transport_index.get() + slider_position = self._globals.var_transport_index.get() except TclError: # don't update the slider when the entry box has been cleared of any value return frames = self._det_faces.filter.frames_list actual_position = max(0, min(len(frames) - 1, slider_position)) if actual_position != slider_position: - self._globals.tk_transport_index.set(actual_position) + self._globals.var_transport_index.set(actual_position) frame_idx = frames[actual_position] if frames else -1 logger.trace("slider_position: %s, frame_idx: %s", actual_position, frame_idx) - self._globals.tk_frame_index.set(frame_idx) + self._globals.var_frame_index.set(frame_idx) def _add_transport(self): """ Add video transport controls """ @@ -237,14 +237,14 @@ def _add_filter_mode_combo(self, frame): frame: :class:`tkinter.ttk.Frame` The Filter Frame that holds the filter combo box """ - self._globals.tk_filter_mode.set("All Frames") - self._globals.tk_filter_mode.trace("w", self._navigation.nav_scale_callback) + self._globals.var_filter_mode.set("All Frames") + self._globals.var_filter_mode.trace("w", self._navigation.nav_scale_callback) nav_frame = ttk.Frame(frame) lbl = ttk.Label(nav_frame, text="Filter:") lbl.pack(side=tk.LEFT, padx=(0, 5)) combo = ttk.Combobox( nav_frame, - textvariable=self._globals.tk_filter_mode, + textvariable=self._globals.var_filter_mode, state="readonly", values=self._filter_modes) combo.pack(side=tk.RIGHT) @@ -260,7 +260,7 @@ def _add_filter_threshold_slider(self, frame): The Filter Frame that holds the filter threshold slider """ slider_frame = ttk.Frame(frame) - tk_var = self._globals.tk_filter_distance + tk_var = self._globals.var_filter_distance min_max = (5, 20) ctl_frame = ttk.Frame(slider_frame) @@ -284,22 +284,22 @@ def _add_filter_threshold_slider(self, frame): Tooltip(item, text=self._helptext["distance"], wrap_length=200) - tk_var.trace("w", self._navigation.nav_scale_callback) + tk_var.trace_add("write", self._navigation.nav_scale_callback) self._optional_widgets["distance_slider"] = slider_frame def pack_threshold_slider(self): """ Display or hide the threshold slider depending on the current filter mode. For misaligned faces filter, display the slider. Hide for all other filters. """ - if self._globals.tk_filter_mode.get() == "Misaligned Faces": + if self._globals.var_filter_mode.get() == "Misaligned Faces": self._optional_widgets["distance_slider"].pack(side=tk.LEFT) else: self._optional_widgets["distance_slider"].pack_forget() def cycle_filter_mode(self): """ Cycle the navigation mode combo entry """ - current_mode = self._globals.filter_mode + current_mode = self._globals.var_filter_mode.get() idx = (self._filter_modes.index(current_mode) + 1) % len(self._filter_modes) - self._globals.tk_filter_mode.set(self._filter_modes[idx]) + self._globals.var_filter_mode.set(self._filter_modes[idx]) def set_action(self, key): """ Set the current action based on keyboard shortcut @@ -318,7 +318,7 @@ def _resize(self, event): framesize = (event.width, event.height) logger.trace("Resizing video frame. Framesize: %s", framesize) self._globals.set_frame_display_dims(*framesize) - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) # << TRANSPORT >> # def _play(self, *args, frame_count=None): # pylint:disable=unused-argument @@ -475,17 +475,16 @@ def _add_static_buttons(self): sep = ttk.Frame(frame, height=2, relief=tk.RIDGE) sep.pack(fill=tk.X, pady=5, side=tk.TOP) buttons = {} - tk_frame_index = self._globals.tk_frame_index for action in ("copy_prev", "copy_next", "reload"): if action == "reload": icon = "reload3" - cmd = lambda f=tk_frame_index: self._det_faces.revert_to_saved(f.get()) # noqa:E731 # pylint:disable=line-too-long,unnecessary-lambda-assignment + cmd = lambda f=self._globals: self._det_faces.revert_to_saved(f.frame_index) # noqa:E731,E501 # pylint:disable=line-too-long,unnecessary-lambda-assignment helptext = _("Revert to saved Alignments ({})").format(lookup[action][1]) else: icon = action direction = action.replace("copy_", "") - cmd = lambda f=tk_frame_index, d=direction: self._det_faces.update.copy( # noqa:E731 # pylint:disable=line-too-long,unnecessary-lambda-assignment - f.get(), d) + cmd = lambda f=self._globals, d=direction: self._det_faces.update.copy( # noqa:E731,E501 # pylint:disable=line-too-long,unnecessary-lambda-assignment + f.frame_index, d) helptext = _("Copy {} Alignments ({})").format(*lookup[action]) state = ["!disabled"] if action == "copy_next" else ["disabled"] button = ttk.Button(frame, @@ -496,8 +495,8 @@ def _add_static_buttons(self): button.pack() Tooltip(button, text=helptext) buttons[action] = button - self._globals.tk_frame_index.trace("w", self._disable_enable_copy_buttons) - self._globals.tk_update.trace("w", self._disable_enable_reload_button) + self._globals.var_frame_index.trace_add("write", self._disable_enable_copy_buttons) + self._globals.var_full_update.trace_add("write", self._disable_enable_reload_button) return buttons def _disable_enable_copy_buttons(self, *args): # pylint:disable=unused-argument @@ -707,7 +706,7 @@ def editor_display(self): def offset(self): """ tuple: The (`width`, `height`) offset of the canvas based on the size of the currently displayed image """ - frame_dims = self._globals.current_frame["display_dims"] + frame_dims = self._globals.current_frame.display_dims offset_x = (self._globals.frame_display_dims[0] - frame_dims[0]) / 2 offset_y = (self._globals.frame_display_dims[1] - frame_dims[1]) / 2 logger.trace("offset_x: %s, offset_y: %s", offset_x, offset_y) @@ -733,11 +732,11 @@ def _add_callbacks(self): """ Add the callback trace functions to the :class:`tkinter.Variable` s Adds callbacks for: - :attr:`_globals.tk_update` Update the display for the current image + :attr:`_globals.var_full_update` Update the display for the current image :attr:`__tk_action_var` Update the mouse display tracking for current action """ - self._globals.tk_update.trace("w", self._update_display) - self._tk_action_var.trace("w", self._change_active_editor) + self._globals.var_full_update.trace_add("write", self._update_display) + self._tk_action_var.trace_add("write", self._change_active_editor) def _change_active_editor(self, *args): # pylint:disable=unused-argument """ Update the display for the active editor. @@ -757,7 +756,7 @@ def _change_active_editor(self, *args): # pylint:disable=unused-argument self.active_editor.bind_mouse_motion() self.active_editor.set_mouse_click_actions() - self._globals.tk_update.set(True) + self._globals.var_full_update.set(True) def _update_display(self, *args): # pylint:disable=unused-argument """ Update the display on frame cache update @@ -767,7 +766,7 @@ def _update_display(self, *args): # pylint:disable=unused-argument A little hacky, but the editors to display or hide are processed in alphabetical order, so that they are always processed in the same order (for tag lowering and raising) """ - if not self._globals.tk_update.get(): + if not self._globals.var_full_update.get(): return zoomed_centering = self.active_editor.zoomed_centering self._image.refresh(self.active_editor.view_mode) @@ -779,7 +778,7 @@ def _update_display(self, *args): # pylint:disable=unused-argument if zoomed_centering != self.active_editor.zoomed_centering: # Refresh the image if editor annotation has changed the zoom centering of the image self._image.refresh(self.active_editor.view_mode) - self._globals.tk_update.set(False) + self._globals.var_full_update.set(False) self.update_idletasks() def _hide_additional_faces(self): diff --git a/tools/manual/globals.py b/tools/manual/globals.py new file mode 100644 index 0000000000..07843f552e --- /dev/null +++ b/tools/manual/globals.py @@ -0,0 +1,309 @@ +#!/usr/bin/env python3 +""" Holds global tkinter variables and information pertaining to the entire Manual tool """ +from __future__ import annotations + +import logging +import os +import sys +import tkinter as tk + +from dataclasses import dataclass, field + +import cv2 +import numpy as np + +from lib.gui.utils import get_config +from lib.logger import parse_class_init +from lib.utils import VIDEO_EXTENSIONS + +logger = logging.getLogger(__name__) + + +@dataclass +class CurrentFrame: + """ Dataclass for holding information about the currently displayed frame """ + image: np.ndarray = field(default_factory=lambda: np.zeros(1)) + """:class:`numpy.ndarry`: The currently displayed frame in original dimensions """ + scale: float = 1.0 + """float: The scaling factor to use to resize the image to the display window """ + interpolation: int = cv2.INTER_AREA + """int: The opencv interpolator ID to use for resizing the image to the display window """ + display_dims: tuple[int, int] = (0, 0) + """tuple[int, int]`: The size of the currently displayed frame, in the display window """ + filename: str = "" + """str: The filename of the currently displayed frame """ + + def __repr__(self) -> str: + """ Clean string representation showing numpy arrays as shape and dtype + + Returns + ------- + str + Loggable representation of the dataclass + """ + properties = [f"{k}={(v.shape, v.dtype) if isinstance(v, np.ndarray) else v}" + for k, v in self.__dict__.items()] + return f"{self.__class__.__name__} ({', '.join(properties)}" + + +@dataclass +class TKVars: + """ Holds the global TK Variables """ + frame_index: tk.IntVar + """:class:`tkinter.IntVar`: The absolute frame index of the currently displayed frame""" + transport_index: tk.IntVar + """:class:`tkinter.IntVar`: The transport index of the currently displayed frame when filters + have been applied """ + face_index: tk.IntVar + """:class:`tkinter.IntVar`: The face index of the currently selected face""" + filter_distance: tk.IntVar + """:class:`tkinter.IntVar`: The amount to filter by distance""" + + update: tk.BooleanVar + """:class:`tkinter.BooleanVar`: Whether an update has been performed """ + update_active_viewport: tk.BooleanVar + """:class:`tkinter.BooleanVar`: Whether the viewport needs updating """ + is_zoomed: tk.BooleanVar + """:class:`tkinter.BooleanVar`: Whether the main window is zoomed in to a face or out to a + full frame""" + + filter_mode: tk.StringVar + """:class:`tkinter.StringVar`: The currently selected filter mode """ + faces_size: tk.StringVar + """:class:`tkinter.StringVar`: The pixel size of faces in the viewport """ + + def __repr__(self) -> str: + """ Clean string representation showing variable type as well as their value + + Returns + ------- + str + Loggable representation of the dataclass + """ + properties = [f"{k}={v.__class__.__name__}({v.get()})" for k, v in self.__dict__.items()] + return f"{self.__class__.__name__} ({', '.join(properties)}" + + +class TkGlobals(): + """ Holds Tkinter Variables and other frame information that need to be accessible from all + areas of the GUI. + + Parameters + ---------- + input_location: str + The location of the input folder of frames or video file + """ + def __init__(self, input_location: str) -> None: + logger.debug(parse_class_init(locals())) + self._tk_vars = self._get_tk_vars() + + self._is_video = self._check_input(input_location) + self._frame_count = 0 # set by FrameLoader + self._frame_display_dims = (int(round(896 * get_config().scaling_factor)), + int(round(504 * get_config().scaling_factor))) + self._current_frame = CurrentFrame() + logger.debug("Initialized %s", self.__class__.__name__) + + @classmethod + def _get_tk_vars(cls) -> TKVars: + """ Create and initialize the tkinter variables. + + Returns + ------- + :class:`TKVars` + The global tkinter variables + """ + retval = TKVars(frame_index=tk.IntVar(value=0), + transport_index=tk.IntVar(value=0), + face_index=tk.IntVar(value=0), + filter_distance=tk.IntVar(value=10), + update=tk.BooleanVar(value=False), + update_active_viewport=tk.BooleanVar(value=False), + is_zoomed=tk.BooleanVar(value=False), + filter_mode=tk.StringVar(), + faces_size=tk.StringVar()) + logger.debug(retval) + return retval + + @property + def current_frame(self) -> CurrentFrame: + """ :class:`CurrentFrame`: The currently displayed frame in the frame viewer with it's + meta information. """ + return self._current_frame + + @property + def frame_count(self) -> int: + """ int: The total number of frames for the input location """ + return self._frame_count + + @property + def frame_display_dims(self) -> tuple[int, int]: + """ tuple: The (`width`, `height`) of the video display frame in pixels. """ + return self._frame_display_dims + + @property + def is_video(self) -> bool: + """ bool: ``True`` if the input is a video file, ``False`` if it is a folder of images. """ + return self._is_video + + # TK Variables that need to be exposed + @property + def var_full_update(self) -> tk.BooleanVar: + """ :class:`tkinter.BooleanVar`: Flag to indicate that whole GUI should be refreshed """ + return self._tk_vars.update + + @property + def var_transport_index(self) -> tk.IntVar: + """ :class:`tkinter.IntVar`: The current index of the display frame's transport slider. """ + return self._tk_vars.transport_index + + @property + def var_frame_index(self) -> tk.IntVar: + """ :class:`tkinter.IntVar`: The current absolute frame index of the currently + displayed frame. """ + return self._tk_vars.frame_index + + @property + def var_filter_distance(self) -> tk.IntVar: + """ :class:`tkinter.IntVar`: The variable holding the currently selected threshold + distance for misaligned filter mode. """ + return self._tk_vars.filter_distance + + @property + def var_filter_mode(self) -> tk.StringVar: + """ :class:`tkinter.StringVar`: The variable holding the currently selected navigation + filter mode. """ + return self._tk_vars.filter_mode + + @property + def var_faces_size(self) -> tk.StringVar: + """ :class:`tkinter..IntVar`: The variable holding the currently selected Faces Viewer + thumbnail size. """ + return self._tk_vars.faces_size + + @property + def var_update_active_viewport(self) -> tk.BooleanVar: + """ :class:`tkinter.BooleanVar`: Boolean Variable that is traced by the viewport's active + frame to update. """ + return self._tk_vars.update_active_viewport + + # Raw values returned from TK Variables + @property + def face_index(self) -> int: + """ int: The currently displayed face index when in zoomed mode. """ + return self._tk_vars.face_index.get() + + @property + def frame_index(self) -> int: + """ int: The currently displayed frame index. NB This returns -1 if there are no frames + that meet the currently selected filter criteria. """ + return self._tk_vars.frame_index.get() + + @property + def is_zoomed(self) -> bool: + """ bool: ``True`` if the frame viewer is zoomed into a face, ``False`` if the frame viewer + is displaying a full frame. """ + return self._tk_vars.is_zoomed.get() + + @staticmethod + def _check_input(frames_location: str) -> bool: + """ Check whether the input is a video + + Parameters + ---------- + frames_location: str + The input location for video or images + + Returns + ------- + bool: 'True' if input is a video 'False' if it is a folder. + """ + if os.path.isdir(frames_location): + retval = False + elif os.path.splitext(frames_location)[1].lower() in VIDEO_EXTENSIONS: + retval = True + else: + logger.error("The input location '%s' is not valid", frames_location) + sys.exit(1) + logger.debug("Input '%s' is_video: %s", frames_location, retval) + return retval + + def set_face_index(self, index: int) -> None: + """ Set the currently selected face index + + Parameters + ---------- + index: int + The currently selected face index + """ + logger.trace("Setting face index from %s to %s", # type:ignore[attr-defined] + self.face_index, index) + self._tk_vars.face_index.set(index) + + def set_frame_count(self, count: int) -> None: + """ Set the count of total number of frames to :attr:`frame_count` when the + :class:`FramesLoader` has completed loading. + + Parameters + ---------- + count: int + The number of frames that exist for this session + """ + logger.debug("Setting frame_count to : %s", count) + self._frame_count = count + + def set_current_frame(self, image: np.ndarray, filename: str) -> None: + """ Set the frame and meta information for the currently displayed frame. Populates the + attribute :attr:`current_frame` + + Parameters + ---------- + image: :class:`numpy.ndarray` + The image used to display in the Frame Viewer + filename: str + The filename of the current frame + """ + scale = min(self.frame_display_dims[0] / image.shape[1], + self.frame_display_dims[1] / image.shape[0]) + self._current_frame.image = image + self._current_frame.filename = filename + self._current_frame.scale = scale + self._current_frame.interpolation = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA + self._current_frame.display_dims = (int(round(image.shape[1] * scale)), + int(round(image.shape[0] * scale))) + logger.trace(self._current_frame) # type:ignore[attr-defined] + + def set_frame_display_dims(self, width: int, height: int) -> None: + """ Set the size, in pixels, of the video frame display window and resize the displayed + frame. + + Used on a frame resize callback, sets the :attr:frame_display_dims`. + + Parameters + ---------- + width: int + The width of the frame holding the video canvas in pixels + height: int + The height of the frame holding the video canvas in pixels + """ + self._frame_display_dims = (int(width), int(height)) + image = self._current_frame.image + scale = min(self.frame_display_dims[0] / image.shape[1], + self.frame_display_dims[1] / image.shape[0]) + self._current_frame.scale = scale + self._current_frame.interpolation = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA + self._current_frame.display_dims = (int(round(image.shape[1] * scale)), + int(round(image.shape[0] * scale))) + logger.trace(self._current_frame) # type:ignore[attr-defined] + + def set_zoomed(self, state: bool) -> None: + """ Set the current zoom state + + Parameters + ---------- + state: bool + ``True`` for zoomed ``False`` for full frame + """ + logger.trace("Setting zoom state from %s to %s", # type:ignore[attr-defined] + self.is_zoomed, state) + self._tk_vars.is_zoomed.set(state) diff --git a/tools/manual/manual.py b/tools/manual/manual.py index 46685766bc..2516a62b9c 100644 --- a/tools/manual/manual.py +++ b/tools/manual/manual.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -""" The Manual Tool is a tkinter driven GUI app for editing alignments files with visual tools. -This module is the main entry point into the Manual Tool. """ +""" Main entry point for the Manual Tool. A GUI app for editing alignments files """ from __future__ import annotations import logging @@ -9,24 +8,27 @@ import typing as T import tkinter as tk from tkinter import ttk +from dataclasses import dataclass from time import sleep -import cv2 import numpy as np from lib.gui.control_helper import ControlPanel from lib.gui.utils import get_images, get_config, initialize_config, initialize_images from lib.image import SingleFrameLoader, read_image_meta +from lib.logger import parse_class_init from lib.multithreading import MultiThread -from lib.utils import handle_deprecated_cliopts, VIDEO_EXTENSIONS +from lib.utils import handle_deprecated_cliopts from plugins.extract import ExtractMedia, Extractor from .detected_faces import DetectedFaces from .faceviewer.frame import FacesFrame from .frameviewer.frame import DisplayFrame +from .globals import TkGlobals from .thumbnails import ThumbsCreator if T.TYPE_CHECKING: + from argparse import Namespace from lib.align import DetectedFace, Mask from lib.queue_manager import EventQueue @@ -35,6 +37,17 @@ TypeManualExtractor = T.Literal["FAN", "cv2-dnn", "mask"] +@dataclass +class _Containers: + """ Dataclass for holding the main area containers in the GUI """ + main: ttk.PanedWindow + """:class:`tkinter.ttk.PanedWindow`: The main window holding the full GUI """ + top: ttk.Frame + """:class:`tkinter.ttk.Frame: The top part (frame viewer) of the GUI""" + bottom: ttk.Frame + """:class:`tkinter.ttk.Frame: The bottom part (face viewer) of the GUI""" + + class Manual(tk.Tk): """ The main entry point for Faceswap's Manual Editor Tool. This tool is part of the Faceswap Tools suite and should be called from ``python tools.py manual`` command. @@ -48,8 +61,8 @@ class Manual(tk.Tk): The :mod:`argparse` arguments as passed in from :mod:`tools.py` """ - def __init__(self, arguments): - logger.debug("Initializing %s: (arguments: '%s')", self.__class__.__name__, arguments) + def __init__(self, arguments: Namespace) -> None: + logger.debug(parse_class_init(locals())) super().__init__() arguments = handle_deprecated_cliopts(arguments) self._validate_non_faces(arguments.frames) @@ -66,24 +79,27 @@ def __init__(self, arguments): video_meta_data = self._detected_faces.video_meta_data valid_meta = all(val is not None for val in video_meta_data.values()) - loader = FrameLoader(self._globals, arguments.frames, video_meta_data) + loader = FrameLoader(self._globals, + arguments.frames, + video_meta_data, + self._detected_faces.frame_list) + if valid_meta: # Load the faces whilst other threads complete if we have valid meta data self._detected_faces.load_faces() self._containers = self._create_containers() self._wait_for_threads(extractor, loader, valid_meta) - if not valid_meta: - # Load the faces after other threads complete if meta data required updating + if not valid_meta: # If meta data needs updating, load faces after other threads self._detected_faces.load_faces() self._generate_thumbs(arguments.frames, arguments.thumb_regen, arguments.single_process) - self._display = DisplayFrame(self._containers["top"], + self._display = DisplayFrame(self._containers.top, self._globals, self._detected_faces) - _Options(self._containers["top"], self._globals, self._display) + _Options(self._containers.top, self._globals, self._display) - self._faces_frame = FacesFrame(self._containers["bottom"], + self._faces_frame = FacesFrame(self._containers.bottom, self._globals, self._detected_faces, self._display) @@ -94,7 +110,7 @@ def __init__(self, arguments): logger.debug("Initialized %s", self.__class__.__name__) @classmethod - def _validate_non_faces(cls, frames_folder): + def _validate_non_faces(cls, frames_folder: str) -> None: """ Quick check on the input to make sure that a folder of extracted faces is not being passed in. """ if not os.path.isdir(frames_folder): @@ -117,7 +133,7 @@ def _validate_non_faces(cls, frames_folder): sys.exit(1) logger.debug("Test input file '%s' does not contain Faceswap header data", test_file) - def _wait_for_threads(self, extractor, loader, valid_meta): + def _wait_for_threads(self, extractor: Aligner, loader: FrameLoader, valid_meta: bool) -> None: """ The :class:`Aligner` and :class:`FramesLoader` are launched in background threads. Wait for them to be initialized prior to proceeding. @@ -150,9 +166,10 @@ def _wait_for_threads(self, extractor, loader, valid_meta): extractor.link_faces(self._detected_faces) if not valid_meta: logger.debug("Saving video meta data to alignments file") - self._detected_faces.save_video_meta_data(**loader.video_meta_data) + self._detected_faces.save_video_meta_data( + **loader.video_meta_data) # type:ignore[arg-type] - def _generate_thumbs(self, input_location, force, single_process): + def _generate_thumbs(self, input_location: str, force: bool, single_process: bool) -> None: """ Check whether thumbnails are stored in the alignments file and if not generate them. Parameters @@ -173,7 +190,7 @@ def _generate_thumbs(self, input_location, force, single_process): thumbs.generate_cache() logger.debug("Generated thumbnails cache") - def _initialize_tkinter(self): + def _initialize_tkinter(self) -> None: """ Initialize a standalone tkinter instance. """ logger.debug("Initializing tkinter") for widget in ("TButton", "TCheckbutton", "TRadiobutton"): @@ -184,15 +201,16 @@ def _initialize_tkinter(self): self.title("Faceswap.py - Visual Alignments") logger.debug("Initialized tkinter") - def _create_containers(self): + def _create_containers(self) -> _Containers: """ Create the paned window containers for various GUI elements Returns ------- - dict: + :class:`_Containers`: The main containers of the manual tool. """ logger.debug("Creating containers") + main = ttk.PanedWindow(self, orient=tk.VERTICAL, name="pw_main") @@ -203,11 +221,13 @@ def _create_containers(self): bottom = ttk.Frame(main, name="frame_bottom") main.add(bottom) - retval = {"main": main, "top": top, "bottom": bottom} + + retval = _Containers(main=main, top=top, bottom=bottom) + logger.debug("Created containers: %s", retval) return retval - def _handle_key_press(self, event): + def _handle_key_press(self, event: tk.Event) -> None: """ Keyboard shortcuts Parameters @@ -226,7 +246,7 @@ def _handle_key_press(self, event): modifiers = {0x0001: 'shift', 0x0004: 'ctrl'} - tk_pos = self._globals.tk_frame_index + globs = self._globals bindings = { "z": self._display.navigation.decrement_frame, "x": self._display.navigation.increment_frame, @@ -245,21 +265,23 @@ def _handle_key_press(self, event): "f5": lambda k=event.keysym: self._display.set_action(k), "f9": lambda k=event.keysym: self._faces_frame.set_annotation_display(k), "f10": lambda k=event.keysym: self._faces_frame.set_annotation_display(k), - "c": lambda f=tk_pos.get(), d="prev": self._detected_faces.update.copy(f, d), - "v": lambda f=tk_pos.get(), d="next": self._detected_faces.update.copy(f, d), + "c": lambda f=globs.frame_index, d="prev": self._detected_faces.update.copy(f, d), + "v": lambda f=globs.frame_index, d="next": self._detected_faces.update.copy(f, d), "ctrl_s": self._detected_faces.save, - "r": lambda f=tk_pos.get(): self._detected_faces.revert_to_saved(f)} + "r": lambda f=globs.frame_index: self._detected_faces.revert_to_saved(f)} # Allow keypad keys to be used for numbers press = event.keysym.replace("KP_", "") if event.keysym.startswith("KP_") else event.keysym + assert isinstance(event.state, int) modifier = "_".join(val for key, val in modifiers.items() if event.state & key != 0) key_press = "_".join([modifier, press]) if modifier else press if key_press.lower() in bindings: - logger.trace("key press: %s, action: %s", key_press, bindings[key_press.lower()]) + logger.trace("key press: %s, action: %s", # type:ignore[attr-defined] + key_press, bindings[key_press.lower()]) self.focus_set() bindings[key_press.lower()]() - def _set_initial_layout(self): + def _set_initial_layout(self) -> None: """ Set the favicon and the bottom frame position to correct location to display full frame window. @@ -271,12 +293,13 @@ def _set_initial_layout(self): logger.debug("Setting initial layout") self.tk.call("wm", "iconphoto", - self._w, get_images().icons["favicon"]) # pylint:disable=protected-access + self._w, # type:ignore[attr-defined] # pylint:disable=protected-access + get_images().icons["favicon"]) location = int(self.winfo_screenheight() // 1.5) - self._containers["main"].sashpos(0, location) + self._containers.main.sashpos(0, location) self.update_idletasks() - def process(self): + def process(self) -> None: """ The entry point for the Visual Alignments tool from :mod:`lib.tools.manual.cli`. Launch the tkinter Visual Alignments Window and run main loop. @@ -289,6 +312,8 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors """ Control panel options for currently displayed Editor. This is the right hand panel of the GUI that holds editor specific settings and annotation display settings. + Parameters + ---------- parent: :class:`tkinter.ttk.Frame` The parent frame for the control panel options tk_globals: :class:`~tools.manual.manual.TkGlobals` @@ -296,9 +321,11 @@ class _Options(ttk.Frame): # pylint:disable=too-many-ancestors display_frame: :class:`DisplayFrame` The frame that holds the editors """ - def __init__(self, parent, tk_globals, display_frame): - logger.debug("Initializing %s: (parent: %s, tk_globals: %s, display_frame: %s)", - self.__class__.__name__, parent, tk_globals, display_frame) + def __init__(self, + parent: ttk.Frame, + tk_globals: TkGlobals, + display_frame: DisplayFrame) -> None: + logger.debug(parse_class_init(locals())) super().__init__(parent) self._globals = tk_globals @@ -309,7 +336,7 @@ def __init__(self, parent, tk_globals, display_frame): self.pack(side=tk.RIGHT, fill=tk.Y) logger.debug("Initialized %s", self.__class__.__name__) - def _initialize(self): + def _initialize(self) -> dict[str, ControlPanel]: """ Initialize all of the control panels, then display the default panel. Adds the control panel to :attr:`_control_panels` and sets the traceback to update @@ -322,6 +349,11 @@ def _initialize(self): The Traceback must be set after the panel has first been packed as otherwise it interferes with the loading of the faces pane. + + Returns + ------- + dict[str, :class:`~lib.gui.control_helper.ControlPanel`] + The configured control panels """ self._initialize_face_options() frame = ttk.Frame(self) @@ -343,7 +375,7 @@ def _initialize(self): panels[name] = panel return panels - def _initialize_face_options(self): + def _initialize_face_options(self) -> None: """ Set the Face Viewer options panel, beneath the standard control options. """ frame = ttk.Frame(self) frame.pack(side=tk.BOTTOM, fill=tk.X, padx=5, pady=5) @@ -352,13 +384,13 @@ def _initialize_face_options(self): lbl = ttk.Label(size_frame, text="Face Size:") lbl.pack(side=tk.LEFT) cmb = ttk.Combobox(size_frame, - value=["Tiny", "Small", "Medium", "Large", "Extra Large"], + values=["Tiny", "Small", "Medium", "Large", "Extra Large"], state="readonly", - textvariable=self._globals.tk_faces_size) - self._globals.tk_faces_size.set("Medium") + textvariable=self._globals.var_faces_size) + self._globals.var_faces_size.set("Medium") cmb.pack(side=tk.RIGHT, padx=5) - def _set_tk_callbacks(self): + def _set_tk_callbacks(self) -> None: """ Sets the callback to change to the relevant control panel options when the selected editor is changed, and the display update on panel option change.""" self._display_frame.tk_selected_action.trace("w", self._update_options) @@ -372,9 +404,9 @@ def _set_tk_callbacks(self): logger.debug("Adding control update callback: (editor: %s, control: %s)", name, ctl.title) seen_controls.add(ctl) - ctl.tk_var.trace("w", lambda *e: self._globals.tk_update.set(True)) + ctl.tk_var.trace("w", lambda *e: self._globals.var_full_update.set(True)) - def _update_options(self, *args): # pylint:disable=unused-argument + def _update_options(self, *args) -> None: # pylint:disable=unused-argument """ Update the control panel display for the current editor. If the options have not already been set, then adds the control panel to @@ -390,7 +422,7 @@ def _update_options(self, *args): # pylint:disable=unused-argument logger.debug("Displaying control panel for editor: '%s'", editor) self._control_panels[editor].pack(expand=True, fill=tk.BOTH) - def _clear_options_frame(self): + def _clear_options_frame(self) -> None: """ Hides the currently displayed control panel """ for editor, panel in self._control_panels.items(): if panel.winfo_ismapped(): @@ -398,244 +430,6 @@ def _clear_options_frame(self): panel.pack_forget() -class TkGlobals(): - """ Holds Tkinter Variables and other frame information that need to be accessible from all - areas of the GUI. - - Parameters - ---------- - input_location: str - The location of the input folder of frames or video file - """ - def __init__(self, input_location): - logger.debug("Initializing %s: (input_location: %s)", - self.__class__.__name__, input_location) - self._tk_vars = self._get_tk_vars() - - self._is_video = self._check_input(input_location) - self._frame_count = 0 # set by FrameLoader - self._frame_display_dims = (int(round(896 * get_config().scaling_factor)), - int(round(504 * get_config().scaling_factor))) - self._current_frame = {"image": None, - "scale": None, - "interpolation": None, - "display_dims": None, - "filename": None} - logger.debug("Initialized %s", self.__class__.__name__) - - @classmethod - def _get_tk_vars(cls): - """ Create and initialize the tkinter variables. - - Returns - ------- - dict - The variable name as key, the variable as value - """ - retval = {} - for name in ("frame_index", "transport_index", "face_index", "filter_distance"): - var = tk.IntVar() - var.set(10 if name == "filter_distance" else 0) - retval[name] = var - for name in ("update", "update_active_viewport", "is_zoomed"): - var = tk.BooleanVar() - var.set(False) - retval[name] = var - for name in ("filter_mode", "faces_size"): - retval[name] = tk.StringVar() - return retval - - @property - def current_frame(self): - """ dict: The currently displayed frame in the frame viewer with it's meta information. Key - and Values are as follows: - - **image** (:class:`numpy.ndarry`): The currently displayed frame in original dimensions - - **scale** (`float`): The scaling factor to use to resize the image to the display - window - - **interpolation** (`int`): The opencv interpolator ID to use for resizing the image to - the display window - - **display_dims** (`tuple`): The size of the currently displayed frame, sized for the - display window - - **filename** (`str`): The filename of the currently displayed frame - """ - return self._current_frame - - @property - def frame_count(self): - """ int: The total number of frames for the input location """ - return self._frame_count - - @property - def tk_face_index(self): - """ :class:`tkinter.IntVar`: The variable that holds the face index of the selected face - within the current frame when in zoomed mode. """ - return self._tk_vars["face_index"] - - @property - def tk_update_active_viewport(self): - """ :class:`tkinter.BooleanVar`: Boolean Variable that is traced by the viewport's active - frame to update.. """ - return self._tk_vars["update_active_viewport"] - - @property - def face_index(self): - """ int: The currently displayed face index when in zoomed mode. """ - return self._tk_vars["face_index"].get() - - @property - def frame_display_dims(self): - """ tuple: The (`width`, `height`) of the video display frame in pixels. """ - return self._frame_display_dims - - @property - def frame_index(self): - """ int: The currently displayed frame index. NB This returns -1 if there are no frames - that meet the currently selected filter criteria. """ - return self._tk_vars["frame_index"].get() - - @property - def tk_frame_index(self): - """ :class:`tkinter.IntVar`: The variable holding the current frame index. """ - return self._tk_vars["frame_index"] - - @property - def filter_mode(self): - """ str: The currently selected navigation mode. """ - return self._tk_vars["filter_mode"].get() - - @property - def tk_filter_mode(self): - """ :class:`tkinter.StringVar`: The variable holding the currently selected navigation - filter mode. """ - return self._tk_vars["filter_mode"] - - @property - def tk_filter_distance(self): - """ :class:`tkinter.DoubleVar`: The variable holding the currently selected threshold - distance for misaligned filter mode. """ - return self._tk_vars["filter_distance"] - - @property - def tk_faces_size(self): - """ :class:`tkinter.StringVar`: The variable holding the currently selected Faces Viewer - thumbnail size. """ - return self._tk_vars["faces_size"] - - @property - def is_video(self): - """ bool: ``True`` if the input is a video file, ``False`` if it is a folder of images. """ - return self._is_video - - @property - def tk_is_zoomed(self): - """ :class:`tkinter.BooleanVar`: The variable holding the value indicating whether the - frame viewer is zoomed into a face or zoomed out to the full frame. """ - return self._tk_vars["is_zoomed"] - - @property - def is_zoomed(self): - """ bool: ``True`` if the frame viewer is zoomed into a face, ``False`` if the frame viewer - is displaying a full frame. """ - return self._tk_vars["is_zoomed"].get() - - @property - def tk_transport_index(self): - """ :class:`tkinter.IntVar`: The current index of the display frame's transport slider. """ - return self._tk_vars["transport_index"] - - @property - def tk_update(self): - """ :class:`tkinter.BooleanVar`: The variable holding the trigger that indicates that a - full update needs to occur. """ - return self._tk_vars["update"] - - @staticmethod - def _check_input(frames_location): - """ Check whether the input is a video - - Parameters - ---------- - frames_location: str - The input location for video or images - - Returns - ------- - bool: 'True' if input is a video 'False' if it is a folder. - """ - if os.path.isdir(frames_location): - retval = False - elif os.path.splitext(frames_location)[1].lower() in VIDEO_EXTENSIONS: - retval = True - else: - logger.error("The input location '%s' is not valid", frames_location) - sys.exit(1) - logger.debug("Input '%s' is_video: %s", frames_location, retval) - return retval - - def set_frame_count(self, count): - """ Set the count of total number of frames to :attr:`frame_count` when the - :class:`FramesLoader` has completed loading. - - Parameters - ---------- - count: int - The number of frames that exist for this session - """ - logger.debug("Setting frame_count to : %s", count) - self._frame_count = count - - def set_current_frame(self, image, filename): - """ Set the frame and meta information for the currently displayed frame. Populates the - attribute :attr:`current_frame` - - Parameters - ---------- - image: :class:`numpy.ndarray` - The image used to display in the Frame Viewer - filename: str - The filename of the current frame - """ - scale = min(self.frame_display_dims[0] / image.shape[1], - self.frame_display_dims[1] / image.shape[0]) - self._current_frame["image"] = image - self._current_frame["filename"] = filename - self._current_frame["scale"] = scale - self._current_frame["interpolation"] = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA - self._current_frame["display_dims"] = (int(round(image.shape[1] * scale)), - int(round(image.shape[0] * scale))) - logger.trace({k: v.shape if isinstance(v, np.ndarray) else v - for k, v in self._current_frame.items()}) - - def set_frame_display_dims(self, width, height): - """ Set the size, in pixels, of the video frame display window and resize the displayed - frame. - - Used on a frame resize callback, sets the :attr:frame_display_dims`. - - Parameters - ---------- - width: int - The width of the frame holding the video canvas in pixels - height: int - The height of the frame holding the video canvas in pixels - """ - self._frame_display_dims = (int(width), int(height)) - image = self._current_frame["image"] - scale = min(self.frame_display_dims[0] / image.shape[1], - self.frame_display_dims[1] / image.shape[0]) - self._current_frame["scale"] = scale - self._current_frame["interpolation"] = cv2.INTER_CUBIC if scale > 1.0 else cv2.INTER_AREA - self._current_frame["display_dims"] = (int(round(image.shape[1] * scale)), - int(round(image.shape[0] * scale))) - logger.trace({k: v.shape if isinstance(v, np.ndarray) else v - for k, v in self._current_frame.items()}) - - class Aligner(): """ The :class:`Aligner` class sets up an extraction pipeline for each of the current Faceswap Aligners, along with the Landmarks based Maskers. When new landmarks are required, the bounding @@ -684,8 +478,8 @@ def _feed_face(self) -> ExtractMedia: assert self._detected_faces is not None face = self._detected_faces.current_faces[self._frame_index][self._face_index] return ExtractMedia( - self._globals.current_frame["filename"], - self._globals.current_frame["image"], + self._globals.current_frame.filename, + self._globals.current_frame.image, detected_faces=[face]) @property @@ -864,53 +658,93 @@ class FrameLoader(): The path to the input frames video_meta_data: dict The meta data held within the alignments file, if it exists and the input is a video + file_list: list[str] + The list of filenames that exist within the alignments file """ - def __init__(self, tk_globals, frames_location, video_meta_data): - logger.debug("Initializing %s: (tk_globals: %s, frames_location: '%s', " - "video_meta_data: %s)", self.__class__.__name__, tk_globals, frames_location, - video_meta_data) + def __init__(self, + tk_globals: TkGlobals, + frames_location: str, + video_meta_data: dict[str, list[int] | list[float] | None], + file_list: list[str]) -> None: + logger.debug(parse_class_init(locals())) self._globals = tk_globals - self._loader = None + self._loader: SingleFrameLoader | None = None self._current_idx = 0 - self._init_thread = self._background_init_frames(frames_location, video_meta_data) - self._globals.tk_frame_index.trace("w", self._set_frame) + self._init_thread = self._background_init_frames(frames_location, + video_meta_data, + file_list) + self._globals.var_frame_index.trace_add("write", self._set_frame) logger.debug("Initialized %s", self.__class__.__name__) @property - def is_initialized(self): - """ bool: ``True`` if the Frame Loader has completed initialization otherwise - ``False``. """ + def is_initialized(self) -> bool: + """ bool: ``True`` if the Frame Loader has completed initialization. """ thread_is_alive = self._init_thread.is_alive() if thread_is_alive: self._init_thread.check_and_raise_error() else: self._init_thread.join() - # Setting the initial frame cannot be done in the thread, so set when queried from main - self._set_frame(initialize=True) + self._set_frame(initialize=True) # Setting initial frame must be done from main thread return not thread_is_alive @property - def video_meta_data(self): + def video_meta_data(self) -> dict[str, list[int] | list[float] | None]: """ dict: The pts_time and key frames for the loader. """ + assert self._loader is not None return self._loader.video_meta_data - def _background_init_frames(self, frames_location, video_meta_data): + def _background_init_frames(self, + frames_location: str, + video_meta_data: dict[str, list[int] | list[float] | None], + frame_list: list[str]) -> MultiThread: """ Launch the images loader in a background thread so we can run other tasks whilst - waiting for initialization. """ + waiting for initialization. + + Parameters + ---------- + frame_location: str + The location of the source video file/frames folder + video_meta_data: dict + The meta data for video file sources + frame_list: list[str] + The list of frames that exist in the alignments file + """ thread = MultiThread(self._load_images, frames_location, video_meta_data, + frame_list, thread_count=1, name=f"{self.__class__.__name__}.init_frames") thread.start() return thread - def _load_images(self, frames_location, video_meta_data): - """ Load the images in a background thread. """ - self._loader = SingleFrameLoader(frames_location, video_meta_data=video_meta_data) - self._globals.set_frame_count(self._loader.count) + def _load_images(self, + frames_location: str, + video_meta_data: dict[str, list[int] | list[float] | None], + frame_list: list[str]) -> None: + """ Load the images in a background thread. - def _set_frame(self, *args, initialize=False): # pylint:disable=unused-argument + Parameters + ---------- + frame_location: str + The location of the source video file/frames folder + video_meta_data: dict + The meta data for video file sources + frame_list: list[str] + The list of frames that exist in the alignments file + """ + self._loader = SingleFrameLoader(frames_location, video_meta_data=video_meta_data) + if not self._loader.is_video and len(frame_list) < self._loader.count: + files = [os.path.basename(f) for f in self._loader.file_list] + skip_list = [idx for idx, fname in enumerate(files) if fname not in frame_list] + logger.debug("Adding %s entries to skip list for images not in alignments file", + len(skip_list)) + self._loader.add_skip_list(skip_list) + self._globals.set_frame_count(self._loader.process_count) + + def _set_frame(self, # pylint:disable=unused-argument + *args, + initialize: bool = False) -> None: """ Set the currently loaded frame to :attr:`_current_frame` and trigger a full GUI update. If the loader has not been initialized, or the navigation position is the same as the @@ -926,17 +760,19 @@ def _set_frame(self, *args, initialize=False): # pylint:disable=unused-argument """ position = self._globals.frame_index if not initialize and (position == self._current_idx and not self._globals.is_zoomed): - logger.trace("Update criteria not met. Not updating: (initialize: %s, position: %s, " - "current_idx: %s, is_zoomed: %s)", initialize, position, - self._current_idx, self._globals.is_zoomed) + logger.trace("Update criteria not met. Not updating: " # type:ignore[attr-defined] + "(initialize: %s, position: %s, current_idx: %s, is_zoomed: %s)", + initialize, position, self._current_idx, self._globals.is_zoomed) return if position == -1: filename = "No Frame" frame = np.ones(self._globals.frame_display_dims + (3, ), dtype="uint8") else: + assert self._loader is not None filename, frame = self._loader.image_from_index(position) - logger.trace("filename: %s, frame: %s, position: %s", filename, frame.shape, position) + logger.trace("filename: %s, frame: %s, position: %s", # type:ignore[attr-defined] + filename, frame.shape, position) self._globals.set_current_frame(frame, filename) self._current_idx = position - self._globals.tk_update.set(True) - self._globals.tk_update_active_viewport.set(True) + self._globals.var_full_update.set(True) + self._globals.var_update_active_viewport.set(True)