-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgui.pyw
3722 lines (2980 loc) · 166 KB
/
gui.pyw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
#region Information
############################################################
## Target Analysis ##
############################################################
## Copyright (c) 2022 Sigmond Kukla, All Rights Reserved ##
############################################################
## Author: Sigmond Kukla ##
## Copyright: Copyright 2022, Sigmond Kukla ##
## The Target Analysis system does not include a license. ##
## This means that this work is under exclusive ##
## copyright by the developer (Sigmond Kukla) alone. ##
## Therefore, you are not permitted to copy, distribute, ##
## or modify this work and claim it is your own. ##
## Maintainer: Sigmond Kukla ##
## Contact: [email protected] (business) ##
## [email protected] (school) ##
## +1 (412)-287-0463 (mobile phone) ##
## Status: Released, active development ##
############################################################
#endregion
#region Import libraries
# Tkinter for the GUI
from tkinter.constants import BOTH, BOTTOM, DISABLED, HORIZONTAL, LEFT, NORMAL, NSEW, RIDGE, RIGHT, TOP, X
from tkinter.font import BOLD
import tkinter as tk
from tkinter import ttk, filedialog
# OpenCV and numpy primarily used for image processing
import cv2
import numpy as np
from PIL import ImageTk,Image # PIL for image resizing and images in the GUI
import os # OS for file management
import csv # CSV for reading and writing data files
import datetime # Datetime for date-string manipulation
# Matplotlib for plotting trends of scores
import matplotlib.pyplot as plt
import matplotlib
from configparser import ConfigParser # Configparser for storing settings in .ini files
from enum import Enum # Enum for the different target types
from pathlib import Path # Pathlib for path manipulation and formatting
import subprocess # Subprocess for running other programs
import pygsheets # Pygsheets for Google Sheets integration
#import pyzbar # for barcode reading and positioning
#import traceback # For debugging - Usage: traceback.print_stack()
#endregion
# --------------------------- Load image functions --------------------------- #
def load_image(target_type, image_selector="ask"):
"""Prompts the user to select an image from their computer, adds it to the preview, and calls the appropriate function to crop the image
Args:
target_type (TargetType): The type of target to load the image for
image_selector (str): The image to load. If "ask" is passed, the user will be prompted to select an image.
"""
#region Type-specific changes
if target_type == TargetTypes.NRA_LEFT:
canvas = left_canvas
if target_type == TargetTypes.NRA_RIGHT:
canvas = right_canvas
if target_type == TargetTypes.ORION_USAS_50 or target_type == TargetTypes.ORION_USAS_50_NRA_SCORING:
canvas = orion_single_canvas
if target_type == TargetTypes.ORION_50FT_CONVENTIONAL:
canvas = orion_50ft_conventional_canvas
#endregion
update_main_label("Loading image...")
canvas.delete("all") # Clear the left canvas in case it already has an image
if image_selector == "ask": image_file = filedialog.askopenfilename() # Open a tkinter file dialog to select an image
else: image_file = image_selector # Use the image passed in
if image_file == "": # If the user didn't select an image, return
update_main_label("No image selected", "warning")
return
image = cv2.imread(image_file) # Load the image for OpenCV image
# If the user wants to use information from the file name, do so
if use_file_info_var.get():
try: set_info_from_file(image_file)
except ValueError as e: print(e)
if (target_type == TargetTypes.NRA_LEFT or
target_type == TargetTypes.ORION_USAS_50 or
target_type == TargetTypes.ORION_USAS_50_NRA_SCORING or
target_type == TargetTypes.ORION_50FT_CONVENTIONAL):
canvas.grid(row = 0, column = 0) # Refresh the canvas
if target_type == TargetTypes.NRA_RIGHT:
canvas.grid(row = 0, column = 1) # Refresh the canvas, placing it in the correct column
global target_preview # Images must be stored globally to be show on the canvas
target_preview = ImageTk.PhotoImage(Image.open(image_file).resize((230, 350), Image.Resampling.LANCZOS)) # Store the image as a tkinter photo image and resize it
canvas.create_image(0, 0, anchor="nw", image=target_preview) # Place the image on the canvas
update_main_label("Image loaded")
root.minsize(550,540) # Increase the window size to accomodate the image
crop_image(image, target_type) # Crop the image to prepare for analysis
# --------------------------- Crop image functions --------------------------- #
def crop_image(image, target_type):
"""Crops the given image based on the given target_type and saves the bulls to images/output
Args:
image (cv2 image): The image to crop
target_type (TargetTypes): The type of target to crop
"""
update_main_label("Cropping image...") # Update main label
ensure_path_exists('images/output')
#_, barcode_rect = get_barcode(image) # Get the barcode rectangle
# Pixel measurements were taken from 300dpi targets, so use the same ratio where necessary
ratio_height = 3507
ratio_width = 2550
# Crop left side of NRA target
if target_type == TargetTypes.NRA_LEFT:
# Flip the image vertically and horizontally before cropping
verticalFlippedImage = cv2.flip(image, -1)
cv2.imwrite("images/output/vertical-flipped.jpg", verticalFlippedImage)
# Set the bull size for NRA A-17 targets and calculate the height and width of the cropped images
bull_size = 580
h=int((bull_size/ratio_height)*image.shape[0])
w=int((bull_size/ratio_width)*image.shape[1])
leftX = 185
y=int((240/ratio_height)*image.shape[0])
x=int((leftX/ratio_width)*image.shape[1])
crop2 = verticalFlippedImage[y:y+h, x:x+w]
y=int((1040/ratio_height)*image.shape[0])
x=int((leftX/ratio_width)*image.shape[1])
crop3 = verticalFlippedImage[y:y+h, x:x+w]
y=int((1840/ratio_height)*image.shape[0])
x=int((leftX/ratio_width)*image.shape[1])
crop4 = verticalFlippedImage[y:y+h, x:x+w]
y=int((2645/ratio_height)*image.shape[0])
x=int((leftX/ratio_width)*image.shape[1])
crop5 = verticalFlippedImage[y:y+h, x:x+w]
# Save the cropped sections
cv2.imwrite("images/output/top-left.jpg", crop2)
cv2.imwrite("images/output/upper-left.jpg", crop3)
cv2.imwrite("images/output/lower-left.jpg", crop4)
cv2.imwrite("images/output/bottom-left.jpg", crop5)
# Crop right side of NRA target
if target_type == TargetTypes.NRA_RIGHT:
# Set the bull size for NRA A-17 targets and calculate the height and width of the cropped images
bull_size = 580
h=int((bull_size/ratio_height)*image.shape[0])
w=int((bull_size/ratio_width)*image.shape[1])
midX = 720
rightX = 1760
topY = 275
upperY = 1070
lowerY = 1880
bottomY = 2680
y=int((topY/ratio_height)*image.shape[0])
x=int((midX/ratio_width)*image.shape[1])
crop1 = image[y:y+h, x:x+w]
y=int((topY/ratio_height)*image.shape[0])
x=int((rightX/ratio_width)*image.shape[1])
crop2 = image[y:y+h, x:x+w]
y=int((upperY/ratio_height)*image.shape[0])
x=int((rightX/ratio_width)*image.shape[1])
crop3 = image[y:y+h, x:x+w]
y=int((lowerY/ratio_height)*image.shape[0])
x=int((rightX/ratio_width)*image.shape[1])
crop4 = image[y:y+h, x:x+w]
y=int((bottomY/ratio_height)*image.shape[0])
x=int((rightX/ratio_width)*image.shape[1])
crop5 = image[y:y+h, x:x+w]
y=int((bottomY/ratio_height)*image.shape[0])
x=int((midX/ratio_width)*image.shape[1])
crop6 = image[y:y+h, x:x+w]
# Save the cropped sections
cv2.imwrite("images/output/top-mid.jpg", crop1)
cv2.imwrite("images/output/top-right.jpg", crop2)
cv2.imwrite("images/output/upper-right.jpg", crop3)
cv2.imwrite("images/output/lower-right.jpg", crop4)
cv2.imwrite("images/output/bottom-right.jpg", crop5)
cv2.imwrite("images/output/bottom-mid.jpg", crop6)
# Crop Orion target
if target_type == TargetTypes.ORION_USAS_50 or target_type == TargetTypes.ORION_USAS_50_NRA_SCORING or target_type == TargetTypes.ORION_50FT_CONVENTIONAL:
# Pixel measurements were taken from 300dpi targets, so use the same ratio where necessary
ratio_height = 3299
ratio_width = 2544
height_to_width_ratio = ratio_height / ratio_width
bottom_removed = image[0:int(image.shape[1]*height_to_width_ratio), 0:image.shape[1]] # Remove the bottom of the image, keeping proportional height
cv2.imwrite("images/output/bottom-removed.jpg", bottom_removed) # for debugging
#region Crop image to only include the bubble zones
h=int((250/ratio_height)*bottom_removed.shape[0])
w=int((1135/ratio_width)*bottom_removed.shape[1])
y=0
x=int((1415/ratio_width)*image.shape[1])
bubbles_crop = image[y:y+h, x:x+w]
def image_resize(image, width=None, height=None, inter=cv2.INTER_AREA):
"""
image (cv2 image): image to be resized
width (int): width of resized image, None for automatic
height (int): height of resized image, None for automatic
inter (cv2 interpolation): interpolation method
"""
dim = None
(h, w) = image.shape[:2]
if width is None and height is None:
return image
if width is None:
r = height / float(h)
dim = (int(w * r), height)
else:
r = width / float(w)
dim = (width, int(h * r))
resized = cv2.resize(image, dim, interpolation=inter)
return resized
bubbles_crop = image_resize(bubbles_crop, width=1135, height=250) # Resize image to fit the coordinate system that the algorithm uses later
#endregion
# Set positions for Orion NRA/USAS-50 targets
if target_type == TargetTypes.ORION_USAS_50 or target_type == TargetTypes.ORION_USAS_50_NRA_SCORING:
bull_size = 400
# (LeftX, TopY) (MidX, TopY) (RightX, TopY)
# (LeftX, UpperY) (RightX, UpperY)
# (LeftX, LowerY) (RightX, LowerY)
# (LeftX, BottomY) (MidX, BottomY) (RightX, BottomY)
leftX = 225
midX = 1070
rightX = 1920
topY = 425
upperY = 1175
lowerY = 1925
bottomY = 2680
# Set positions for Orion 50ft conventional targets
if target_type == TargetTypes.ORION_50FT_CONVENTIONAL:
bull_size = 600
# (LeftX, TopY) (MidX, TopY) (RightX, TopY)
# (LeftX, UpperY) (RightX, UpperY)
# (LeftX, LowerY) (RightX, LowerY)
# (LeftX, BottomY) (MidX, BottomY) (RightX, BottomY)
leftX = 115
midX = 965
rightX = 1805
topY = 355
upperY = 1090
lowerY = 1850
bottomY = 2600
# Set the same height and width for all cropped images
h=int((bull_size/ratio_height)*bottom_removed.shape[0])
w=int((bull_size/ratio_width)*bottom_removed.shape[1])
y=int((topY/ratio_height)*bottom_removed.shape[0])
x=int((midX/ratio_width)*bottom_removed.shape[1])
crop1 = bottom_removed[y:y+h, x:x+w]
y=int((topY/ratio_height)*bottom_removed.shape[0])
x=int((rightX/ratio_width)*bottom_removed.shape[1])
crop2 = bottom_removed[y:y+h, x:x+w]
y=int((upperY/ratio_height)*bottom_removed.shape[0])
x=int((rightX/ratio_width)*bottom_removed.shape[1])
crop3 = bottom_removed[y:y+h, x:x+w]
y=int((lowerY/ratio_height)*bottom_removed.shape[0])
x=int((rightX/ratio_width)*bottom_removed.shape[1])
crop4 = bottom_removed[y:y+h, x:x+w]
y=int((bottomY/ratio_height)*bottom_removed.shape[0])
x=int((rightX/ratio_width)*bottom_removed.shape[1])
crop5 = bottom_removed[y:y+h, x:x+w]
y=int((bottomY/ratio_height)*bottom_removed.shape[0])
x=int((midX/ratio_width)*bottom_removed.shape[1])
crop6 = bottom_removed[y:y+h, x:x+w]
# NOTE: STUFF BELOW IS STRANGE
# crop7 represents the top-left corner of the bull
# and the following go DOWN the left side
# now this is illogical, but apparently I do it
# throughout the program and is a leftover from
# the left/right cropping of NRA targets
# Therefore, I'm just going to leave it as is
y=int((topY/ratio_height)*bottom_removed.shape[0])
x=int((leftX/ratio_width)*bottom_removed.shape[1])
crop7 = bottom_removed[y:y+h, x:x+w]
y=int((upperY/ratio_height)*bottom_removed.shape[0])
x=int((leftX/ratio_width)*bottom_removed.shape[1])
crop8 = bottom_removed[y:y+h, x:x+w]
y=int((lowerY/ratio_height)*bottom_removed.shape[0])
x=int((leftX/ratio_width)*bottom_removed.shape[1])
crop9 = bottom_removed[y:y+h, x:x+w]
y=int((bottomY/ratio_height)*bottom_removed.shape[0])
x=int((leftX/ratio_width)*bottom_removed.shape[1])
crop10 = bottom_removed[y:y+h, x:x+w]
# Save the cropped images
cv2.imwrite("images/output/bubbles.jpg", bubbles_crop)
cv2.imwrite("images/output/top-mid.jpg", crop1)
cv2.imwrite("images/output/top-right.jpg", crop2)
cv2.imwrite("images/output/upper-right.jpg", crop3)
cv2.imwrite("images/output/lower-right.jpg", crop4)
cv2.imwrite("images/output/bottom-right.jpg", crop5)
cv2.imwrite("images/output/bottom-mid.jpg", crop6)
cv2.imwrite("images/output/top-left.jpg", crop7)
cv2.imwrite("images/output/upper-left.jpg", crop8)
cv2.imwrite("images/output/lower-left.jpg", crop9)
cv2.imwrite("images/output/bottom-left.jpg", crop10)
# Get name from bubbles if enabled
if use_bubbles_var.get() and (tab_control.index("current") == 1 or tab_control.index("current") == 2):
update_main_label("Setting name from bubbles...")
set_name_from_bubbles(target_type)
update_main_label("Cropped image")
def get_barcode(image):
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # Make the image grayscale
detected_barcodes = pyzbar.pyzbar.decode(gray) # Detect the barcodes
if len(detected_barcodes) == 0:
raise Exception("No barcode detected")
for barcode in detected_barcodes:
barcode_data = barcode.data.decode("utf-8")
barcode_rect = barcode.rect
return barcode_data, barcode_rect
# --------------------------- Scan image functions --------------------------- #
def scan_image():
"""Uses wia-cmd-scanner to scan an image and save it to the images folder"""
def create_image_name():
'''Generates a file name compatible with Target Analysis based on the current options'''
month = shorten_month(month_var.get()) # Get the first 3 letters of the month
return day_var.get() + month + year_var.get() + name_var.get() + target_num_var.get() + ".jpg" # Create the image name
image_name = create_image_name() # Create the image name
# Scanning uses WINDOWS ONLY wia-cmd-scanner.exe from https://github.com/nagimov/wia-cmd-scanner
subprocess.run([
Path('assets\wia-cmd-scanner\wia-cmd-scanner.exe'),
'/w', '0',
'/h', '0',
'/dpi', '300',
'/color', 'RGB',
'/format', 'JPG',
'/output', Path('images',image_name)
])
update_main_label(f"Image scanned as {image_name}")
return image_name # Return the image name to call the load function on it
def scan_process(target_type):
"""Scans an image, loads and crops it, and analyzes the target
Args:
target_type (TargetTypes): What target type the scanned image is
"""
update_main_label("Scanning image...")
image_name = scan_image() # Scan and save an image, getting the image name
path = Path("images", image_name) # Get the path to the image
load_image(target_type, path) # Load the image
# Again, really dumb that I haven't combined the enums yet. So I have to do a hacky thing to convert to the scoring type
if target_type == TargetTypes.ORION_USAS_50:
scoring_type = ScoringTypes.ORION_USAS_50
elif target_type == TargetTypes.ORION_USAS_50_NRA_SCORING:
scoring_type = ScoringTypes.ORION_USAS_50_NRA_SCORING
elif target_type == TargetTypes.ORION_50FT_CONVENTIONAL:
scoring_type = ScoringTypes.ORION_50FT_CONVENTIONAL
analyze_target(scoring_type) # Analyze the image
# -------------------- Target processing control functions ------------------- #
def analyze_target(target_type):
"""Runs the appropriate analyze_image function for every image that has been cropped and saved.
Args:
target_type (TargetType): The type of target to analyze
"""
update_main_label("Analyzing target...") # Update main label
global current_target_type
current_target_type = target_type # Set the current target type
# Create a folder (if necessary) for the current date's targets
global data_folder
data_folder = Path("data", f"{day_var.get()}{shorten_month(month_var.get())}{year_var.get()}")
ensure_path_exists(data_folder)
# If a global data CSV doesn't exist, create it
GLOBAL_CSV_PATH = Path('data/data.csv')
if not GLOBAL_CSV_PATH.exists(): create_csv(GLOBAL_CSV_PATH)
# If today's overview CSV doesn't exist, create it
overview_csv_name = f"overview-{day_var.get()}{shorten_month(month_var.get())}{year_var.get()}.csv"
overview_csv_path = Path(data_folder, overview_csv_name)
if not overview_csv_path.exists(): create_csv(overview_csv_path)
# If there is a duplicate name on this day, increase the target number
with open(overview_csv_path) as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',')
for index,row in enumerate(csv_reader):
if index != 0:
if row[0] == name_var.get():
target_num_var.set(int(row[2]) + 1)
print(f"Name already in today's data. Increased target number to {target_num_var.get()}")
# Create and store a name for the target output file
target_metadata = f"{name_var.get()}{day_var.get()}{shorten_month(month_var.get())}{year_var.get()}{target_num_var.get()}"
csv_name = f"data-{target_metadata}.csv"
# If the CSV file already exists, delete it
global csv_path
csv_path = Path(data_folder, csv_name)
if csv_path.exists():
print("CSV already exists. Removing old version")
os.remove(csv_path)
# Create the CSV file template
with open(csv_path, 'x', newline="") as csv_file:
filewriter = csv.writer(csv_file, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL)
# label it differently if its a decimal target
if target_type == ScoringTypes.ORION_USAS_50:
filewriter.writerow(["Image", "Score", "Decimal", "hole_x", "hole_y", "Distance", "hole_ratio_x", "hole_ratio_y"])
else:
filewriter.writerow(["Image", "Dropped", "X", "hole_x", "hole_y", "Distance", "hole_ratio_x", "hole_ratio_y"])
csv_file.close()
# Analyze each cropped image
if target_type == ScoringTypes.NRA:
analyze_image("images/output/top-mid.jpg")
analyze_image("images/output/top-right.jpg")
analyze_image("images/output/upper-right.jpg")
analyze_image("images/output/lower-right.jpg")
analyze_image("images/output/bottom-right.jpg")
analyze_image("images/output/bottom-mid.jpg")
analyze_image("images/output/bottom-left.jpg")
analyze_image("images/output/lower-left.jpg")
analyze_image("images/output/upper-left.jpg")
analyze_image("images/output/top-left.jpg")
elif target_type == ScoringTypes.ORION_USAS_50:
analyze_orion_image("images/output/top-mid.jpg")
analyze_orion_image("images/output/top-right.jpg")
analyze_orion_image("images/output/upper-right.jpg")
analyze_orion_image("images/output/lower-right.jpg")
analyze_orion_image("images/output/bottom-right.jpg")
analyze_orion_image("images/output/bottom-mid.jpg")
analyze_orion_image("images/output/bottom-left.jpg")
analyze_orion_image("images/output/lower-left.jpg")
analyze_orion_image("images/output/upper-left.jpg")
analyze_orion_image("images/output/top-left.jpg")
elif target_type == ScoringTypes.ORION_USAS_50_NRA_SCORING:
analyze_orion_image_nra_scoring("images/output/top-mid.jpg")
analyze_orion_image_nra_scoring("images/output/top-right.jpg")
analyze_orion_image_nra_scoring("images/output/upper-right.jpg")
analyze_orion_image_nra_scoring("images/output/lower-right.jpg")
analyze_orion_image_nra_scoring("images/output/bottom-right.jpg")
analyze_orion_image_nra_scoring("images/output/bottom-mid.jpg")
analyze_orion_image_nra_scoring("images/output/bottom-left.jpg")
analyze_orion_image_nra_scoring("images/output/lower-left.jpg")
analyze_orion_image_nra_scoring("images/output/upper-left.jpg")
analyze_orion_image_nra_scoring("images/output/top-left.jpg")
elif target_type == ScoringTypes.ORION_50FT_CONVENTIONAL:
analyze_50ft_conventional("images/output/top-mid.jpg")
analyze_50ft_conventional("images/output/top-right.jpg")
analyze_50ft_conventional("images/output/upper-right.jpg")
analyze_50ft_conventional("images/output/lower-right.jpg")
analyze_50ft_conventional("images/output/bottom-right.jpg")
analyze_50ft_conventional("images/output/bottom-mid.jpg")
analyze_50ft_conventional("images/output/bottom-left.jpg")
analyze_50ft_conventional("images/output/lower-left.jpg")
analyze_50ft_conventional("images/output/upper-left.jpg")
analyze_50ft_conventional("images/output/top-left.jpg")
# Create variables to store the score and x count
global score, x_count
score = 100
x_count = 0
if target_type == ScoringTypes.ORION_USAS_50: score = 0
# Update the score and x count from the saved target CSV file
with open(csv_path) as csv_file:
csv_reader = csv.reader(csv_file, delimiter=',')
line_count = 0
for row in csv_reader:
if line_count != 0:
if target_type == ScoringTypes.ORION_USAS_50:
score += int(row[1])
score += int(row[2]) / 10
else:
score -= int(row[1])
x_count += int(row[2])
line_count += 1
# Round the score to the nearest decimal place to avoid floating point error
score = round(score, 1)
def write_target_to_csv(csv_file):
"""Write the target's score and x count to the CSV file
Args:
csv_file (str or path): The CSV file to write to
"""
filewriter = csv.writer(csv_file, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL)
date = datetime.datetime.strptime(f"{day_var.get()} {month_var.get()} {year_var.get()}", r"%d %B %Y")
date_str = date.strftime(r"%m/%d/%Y")
filewriter.writerow([name_var.get(), date_str, target_num_var.get(), score, x_count])
csv_file.close()
# Save the target's basic info to the global data CSV
with open(GLOBAL_CSV_PATH, 'a', newline="") as csv_file:
write_target_to_csv(csv_file)
# Save the target's basic info to today's CSV
with open(overview_csv_path, 'a', newline="") as csv_file:
write_target_to_csv(csv_file)
if enable_teams_var.get():
teams_csv_path = Path(f"data/{active_team_var.get()}.csv")
with open(teams_csv_path, 'a', newline="") as csv_file:
filewriter = csv.writer(csv_file, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL)
filewriter.writerow([name_var.get(), day_var.get() + " " + month_var.get() + " " + year_var.get(), target_num_var.get(), score, x_count])
csv_file.close()
# Enable the "Show Output" menu item
# If any menu items have been added above this, make sure to recount them to get the correct index
# Counting starts at zero.
filemenu.entryconfigure(1, state=NORMAL)
save_name = f"archive-{target_metadata}.jpg"
save_path = Path(data_folder, save_name)
combine_output(score, x_count, save_path)
# If scanning a single target, show the analysis window
if not is_opening_folder:
if individual_output_type_var.get() == "tkinter":
# If the user uses the new analysis window, open it
# There is no need to show the output here, instead, if it is needed,
# it will be shown when the Finish button is pressed in the analysis window
open_analysis_window()
elif show_output_when_finished_var.get():
show_output() # Otherwise, show the output now that analysis has finished
def show_output():
"""Shows the most recently saved results of the analysis in a new window."""
update_main_label("Showing output window") # Update main label
#region Create window
show_output_window = tk.Toplevel(root)
show_output_window.minsize(525,750)
show_output_window.geometry("525x750")
show_output_window.tk.call('wm', 'iconphoto', show_output_window._w, tk.PhotoImage(file='assets/icon.png'))
show_output_window.title("Target Analysis")
#endregion
#region Create frames
# Only buttons and labels go in the top frame
output_top_frame = ttk.Frame(show_output_window)
output_top_frame.pack(side=TOP, fill=X, expand=True, pady=10)
# Target images are shown in the bottom frame
output_bottom_frame = ttk.Frame(show_output_window)
output_bottom_frame.pack(side=TOP, fill=X)
#endregion
#region Create buttons and info at the top
# Create a button to open the target CSV file
open_target_csv_button = ttk.Button(output_top_frame, text="Open target CSV", command=lambda: open_file(csv_path))
open_target_csv_button.grid(row=0, column=0)
output_top_frame.grid_columnconfigure(0, weight=1)
# Create a label for the score
global current_target_type
if current_target_type == ScoringTypes.ORION_USAS_50: score_label_text = str(score)
else: score_label_text = str(score) + "-" + str(x_count) + "X"
score_label = ttk.Label(output_top_frame, text=score_label_text, font='bold')
score_label.grid(row=0, column=1)
output_top_frame.grid_columnconfigure(1, weight=1)
# Create a button to open the global data CSV file
open_data_csv_button = ttk.Button(output_top_frame, text="Open data CSV", command=lambda: open_file(Path('data', 'data.csv')))
open_data_csv_button.grid(row=0, column=2)
output_top_frame.grid_columnconfigure(2, weight=1)
#endregion
# Create canvases and images for each bull
top_left_canvas = tk.Canvas(output_bottom_frame, width=170,height=170)
top_left_canvas.grid(row = 0, column = 0)
global top_left_output
top_left_output = ImageTk.PhotoImage(Image.open("images/output/top-left.jpg-output.jpg").resize((170, 170), Image.Resampling.LANCZOS))
top_left_canvas.create_image(0, 0, anchor="nw", image=top_left_output)
upper_left_canvas = tk.Canvas(output_bottom_frame, width=170,height=170)
upper_left_canvas.grid(row = 1, column = 0)
global upper_left_output
upper_left_output = ImageTk.PhotoImage(Image.open("images/output/upper-left.jpg-output.jpg").resize((170, 170), Image.Resampling.LANCZOS))
upper_left_canvas.create_image(0, 0, anchor="nw", image=upper_left_output)
lower_left_canvas = tk.Canvas(output_bottom_frame, width=170,height=170)
lower_left_canvas.grid(row = 2, column = 0)
global lower_left_output
lower_left_output = ImageTk.PhotoImage(Image.open("images/output/lower-left.jpg-output.jpg").resize((170, 170), Image.Resampling.LANCZOS))
lower_left_canvas.create_image(0, 0, anchor="nw", image=lower_left_output)
bottom_left_canvas = tk.Canvas(output_bottom_frame, width=170,height=170)
bottom_left_canvas.grid(row = 3, column = 0)
global bottom_left_output
bottom_left_output = ImageTk.PhotoImage(Image.open("images/output/bottom-left.jpg-output.jpg").resize((170, 170), Image.Resampling.LANCZOS))
bottom_left_canvas.create_image(0, 0, anchor="nw", image=bottom_left_output)
top_mid_canvas = tk.Canvas(output_bottom_frame, width=170,height=170)
top_mid_canvas.grid(row = 0, column = 1)
global top_mid_output
top_mid_output = ImageTk.PhotoImage(Image.open("images/output/top-mid.jpg-output.jpg").resize((170, 170), Image.Resampling.LANCZOS))
top_mid_canvas.create_image(0, 0, anchor="nw", image=top_mid_output)
bottom_mid_canvas = tk.Canvas(output_bottom_frame, width=170,height=170)
bottom_mid_canvas.grid(row = 3, column = 1)
global bottom_mid_output
bottom_mid_output = ImageTk.PhotoImage(Image.open("images/output/bottom-mid.jpg-output.jpg").resize((170, 170), Image.Resampling.LANCZOS))
bottom_mid_canvas.create_image(0, 0, anchor="nw", image=bottom_mid_output)
top_right_canvas = tk.Canvas(output_bottom_frame, width=170,height=170)
top_right_canvas.grid(row = 0, column = 2)
global top_right_output
top_right_output = ImageTk.PhotoImage(Image.open("images/output/top-right.jpg-output.jpg").resize((170, 170), Image.Resampling.LANCZOS))
top_right_canvas.create_image(0, 0, anchor="nw", image=top_right_output)
upper_right_canvas = tk.Canvas(output_bottom_frame, width=170,height=170)
upper_right_canvas.grid(row = 1, column = 2)
global upper_right_output
upper_right_output = ImageTk.PhotoImage(Image.open("images/output/upper-right.jpg-output.jpg").resize((170, 170), Image.Resampling.LANCZOS))
upper_right_canvas.create_image(0, 0, anchor="nw", image=upper_right_output)
lower_right_canvas = tk.Canvas(output_bottom_frame, width=170,height=170)
lower_right_canvas.grid(row = 2, column = 2)
global lower_right_output
lower_right_output = ImageTk.PhotoImage(Image.open("images/output/lower-right.jpg-output.jpg").resize((170, 170), Image.Resampling.LANCZOS))
lower_right_canvas.create_image(0, 0, anchor="nw", image=lower_right_output)
bottom_right_canvas = tk.Canvas(output_bottom_frame, width=170,height=170)
bottom_right_canvas.grid(row = 3, column = 2)
global bottom_right_output
bottom_right_output = ImageTk.PhotoImage(Image.open("images/output/bottom-right.jpg-output.jpg").resize((170, 170), Image.Resampling.LANCZOS))
bottom_right_canvas.create_image(0, 0, anchor="nw", image=bottom_right_output)
def show_folder(path):
"""Open Windows Explorer to the path specified
Args:
path (str or Path): The path to open
"""
print(f"Opening folder: {str(path)}")
update_main_label("Opening folder (Windows only)")
subprocess.run(["explorer", Path(path)]) # Run a system command to open the folder using Explorer (Windows only)
update_main_label(f"{path} opened in explorer")
def open_file(path):
"""Opens the file specified by path with the default viewer
Args:
path (str or Path): path to the file to open
"""
update_main_label(f"Opening file {path}")
subprocess.run([Path(path)], shell=True) # Run a system command to open the file using the default viewer (should work on any operating system)
def ensure_path_exists(path):
"""Checks if the given path exists, and creates it if it doesn't
Args:
path (str or Path): The path to check"""
path = Path(path)
if not path.exists(): os.mkdir(path)
def open_analysis_window():
"""Opens a tkinter-based image viewer for the analysis review"""
# Load all of the images that have been saved from analysis
def load_images():
# Create a list of images
global output_images
global output_image_names
output_images = []
output_image_names = []
# os.listdir returns a list of the files in the directory
for file in Path("images/output").iterdir():
# Output images are saved as such: <original image name>-output.png
if file.name.endswith("output.jpg"):
output_images.append(ImageTk.PhotoImage(Image.open(file).resize((600, 600), Image.Resampling.LANCZOS))) # Load the image as a tkinter photo image and add it to the list
output_image_names.append(file.name) # Add the image name to the list
# Prepare image names lists for use by ordering them in a clockwise fashion, starting with the top middle target image.
# Define the correct order for the list
clockwise_order = {"top-mid.jpg-output.jpg" : 0,
"top-right.jpg-output.jpg" : 1,
"upper-right.jpg-output.jpg" : 2,
"lower-right.jpg-output.jpg" : 3,
"bottom-right.jpg-output.jpg" : 4,
"bottom-mid.jpg-output.jpg" : 5,
"bottom-left.jpg-output.jpg" : 6,
"lower-left.jpg-output.jpg" : 7,
"upper-left.jpg-output.jpg" : 8,
"top-left.jpg-output.jpg" : 9}
# Sort the images and image names list by the image names according to the clockwise order
sorted_zipped = sorted(zip(output_images, output_image_names), key=lambda d: clockwise_order[d[1]])
# Unzip the sorted list into images and image names
output_images = [x for x, y in sorted_zipped]
output_image_names = [y for x, y in sorted_zipped]
# Create friendly names for use in the GUI by removing the file extension and "-output" from the image name,
# replacing the hyphens with spaces and capitalizing the first letter of each word.
global output_friendly_names
output_friendly_names = [(y.split(".jpg-output.jpg")[0]).replace("-", " ").capitalize() for x, y in sorted_zipped]
# Delete everything on the analysis canvas
def clear_canvas():
analysis_canvas.delete("all")
# Shows the indexth image in the output_images list
def show_image(index):
analysis_canvas.create_image(0, 0, anchor="nw", image=output_images[index]) # Create the image on the canvas
analysis_top_label.config(text=output_friendly_names[index]) # Update the top label with the friendly name of the image
# Advance to the next image in the output_images list if allowed
def on_next_button_pressed():
global image_index
if image_index < len(output_images) - 1:
image_index += 1
clear_canvas()
show_image(image_index)
update_buttons()
# Move back to the previous image in the output_images list if allowed
def on_back_button_pressed():
global image_index
if image_index > 0:
image_index -= 1
clear_canvas()
show_image(image_index)
update_buttons()
# Close the analysis window and show the output window if enabled
def on_finish_button_pressed():
analysis_window.destroy()
if show_output_when_finished_var.get():
show_output()
# Update the buttons to show the correct state based on the current image index
def update_buttons():
if image_index == 0:
analysis_back_button.config(state=DISABLED) # Disable the back button if the first image is showing
else:
analysis_back_button.config(state=NORMAL) # Enable the back button if the first image is not showing
if image_index == len(output_images)-1:
analysis_next_button.config(text="Finish", style="Accent.TButton", command=on_finish_button_pressed) # If the last image is showing, change the next button to say "Finish" and make it an accent button (blue) for emphasis
else:
analysis_next_button.config(state=NORMAL, text="Next", style="Button.TButton") # If the last image is not showing, change the next button to say "Next" and make it a normal button
#region Create the analysis window
analysis_window = tk.Toplevel(root)
analysis_window.title("Target Analysis")
analysis_window.minsize(width=600, height=690)
analysis_window.geometry("600x690")
analysis_window.tk.call('wm', 'iconphoto', analysis_window._w, tk.PhotoImage(file='assets/icon.png'))
#endregion
#region Create frames
# Top frame shows the image name
analysis_top_frame = ttk.Frame(analysis_window)
analysis_top_frame.pack(side=TOP, fill=X)
# Images frame holds the canvas with the images
analysis_images_frame = ttk.Frame(analysis_window)
analysis_images_frame.pack(side=TOP, fill=X)
# Bottom frame holds the buttons
analysis_bottom_frame = ttk.Frame(analysis_window)
analysis_bottom_frame.pack(side=BOTTOM, fill=X)
#endregion
#region Create top label
analysis_top_label = ttk.Label(analysis_top_frame, text="Analysis", font="bold")
analysis_top_label.pack(pady=10)
#endregion
#region Create canvas
analysis_canvas = tk.Canvas(analysis_images_frame, width=600, height=600)
analysis_canvas.pack()
#endregion
#region Create buttons
analysis_next_button = ttk.Button(analysis_bottom_frame, text="Next", command=on_next_button_pressed)#, style="Accent.TButton")
analysis_next_button.pack(side=RIGHT, padx=5, pady=5)
analysis_back_button = ttk.Button(analysis_bottom_frame, text="Back", command=on_back_button_pressed)#, style="Accent.TButton")
analysis_back_button.pack(side=LEFT, padx=5, pady=5)
#endregion
#Show first image
global image_index
image_index = 0
load_images()
clear_canvas()
show_image(image_index)
update_buttons()
def create_csv(path):
"""Creates a data.csv file to store the data from all targets.
Args:
path (str or Path): The path of the CSV file to create."""
# Open the CSV file
with open(Path(path), 'x', newline="") as csv_file:
filewriter = csv.writer(csv_file, delimiter=',', quotechar='|', quoting=csv.QUOTE_MINIMAL) # Create a filewriter
filewriter.writerow(['Name', 'Date', 'Target Number', 'Score','X']) # Write the header row
csv_file.close() # Close the file
update_main_label(f"Created CSV file at {path}")
def combine_output(score, x_count, path):
"""Saves an image with all of the target data after scoring
Args:
score (float): The score of the target
x_count (float): The number of Xs on the target
path (str or Path): The path to the target image
"""
# Create a new image with the correct size
new_image = Image.new('RGB', (600, 800))
# Open each image and paste it into the new image
top_left = Image.open("images/output/top-left.jpg-output.jpg").resize((200, 200), Image.Resampling.LANCZOS)
new_image.paste(top_left, (0, 0))
upper_left = Image.open("images/output/upper-left.jpg-output.jpg").resize((200, 200), Image.Resampling.LANCZOS)
new_image.paste(upper_left, (0, 200))
lower_left = Image.open("images/output/lower-left.jpg-output.jpg").resize((200, 200), Image.Resampling.LANCZOS)
new_image.paste(lower_left, (0, 400))
bottom_left = Image.open("images/output/bottom-left.jpg-output.jpg").resize((200, 200), Image.Resampling.LANCZOS)
new_image.paste(bottom_left, (0, 600))
top_mid = Image.open("images/output/top-mid.jpg-output.jpg").resize((200, 200), Image.Resampling.LANCZOS)
new_image.paste(top_mid, (200, 0))
bottom_mid = Image.open("images/output/bottom-mid.jpg-output.jpg").resize((200, 200), Image.Resampling.LANCZOS)
new_image.paste(bottom_mid, (200, 600))
top_right = Image.open("images/output/top-right.jpg-output.jpg").resize((200, 200), Image.Resampling.LANCZOS)
new_image.paste(top_right, (400, 0))
upper_right = Image.open("images/output/upper-right.jpg-output.jpg").resize((200, 200), Image.Resampling.LANCZOS)
new_image.paste(upper_right, (400, 200))
lower_right = Image.open("images/output/lower-right.jpg-output.jpg").resize((200, 200), Image.Resampling.LANCZOS)
new_image.paste(lower_right, (400, 400))
bottom_right = Image.open("images/output/bottom-right.jpg-output.jpg").resize((200, 200), Image.Resampling.LANCZOS)
new_image.paste(bottom_right, (400, 600))
#new_image.save("images/output/combined.jpg") # Save the image
cv2_image = cv2.cvtColor(np.array(new_image), cv2.COLOR_RGB2BGR) # Convert to cv2 image for text
# Add the score and x count to the image
cv2.putText(cv2_image, f"{str(score)}-", (225, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
cv2.putText(cv2_image, f"{str(x_count)}X", (315, 250), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
# Add the name to the image
cv2.putText(cv2_image, str(name_var.get()), (225, 400), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
# Date
cv2.putText(cv2_image, str(month_var.get()), (225, 450), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
cv2.putText(cv2_image, f"{str(day_var.get())},", (295, 450), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
cv2.putText(cv2_image, str(year_var.get()), (335, 450), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
# Target number
cv2.putText(cv2_image, f"Target num: {str(target_num_var.get())}", (225, 500), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2)
# cv2.imwrite("images/output/combined.jpg", cv2_image)
cv2.imwrite(str(path), cv2_image)
# --------------------------- Open folder functions -------------------------- #
def open_folder(scoring_type):
"""Loads, crops, and analyzes all images in a user-selected folder
Args:
scoring_type (ScoringTypes): The type of target that is in the folder
Note:
This function uses ScoringTypes because it automatically distinguishes between left and right NRA targets
"""
def cleanup():
"""Returns opening folder variables to original state"""
show_output_when_finished_var.set(show_output_when_finished_backup) # Revert the show_output_when_finished_var to its original value
is_opening_folder = False # Keep track of whether or not the folder is being opened
global is_opening_folder
is_opening_folder = True # Keep track of whether or not the folder is being opened
show_output_when_finished_backup = show_output_when_finished_var.get()
show_output_when_finished_var.set(False)
update_main_label("Analyzing folder, this could take a while...")
folder = filedialog.askdirectory() # Get the folder to open
if folder == "": # If the user didn't select a folder
update_main_label("No folder selected", "warning")
cleanup()
return
else: folder = Path(folder) # Convert the folder to a Path object
# os.listdir() returns a list of all files in the folder
for file in folder.iterdir():
# Ignore files that are not images
if file.suffix == ".jpeg" or file.suffix == ".jpg":
try:
set_info_from_file(file) # Set the file info automatically (needs proper naming)
needs_renamed = False # If set_info_from_file() doesn't throw an exception, the file doesn't need to be renamed
except ValueError:
needs_renamed = True # Otherwise it does need to be renamed
file_image = cv2.imread(str(file)) # Open the image
if scoring_type == ScoringTypes.NRA:
# Check if the image is a left or right image
if "left" in file.name:
crop_image(file_image, TargetTypes.NRA_LEFT)
elif "right" in file.name:
crop_image(file_image, TargetTypes.NRA_RIGHT)
file_num += 1 # Increment the file number
# For every two files opened, analyze the target
# Again, it is imperative that the naming convention is correct
# See the README for more information
if file_num == 2:
analyze_target(ScoringTypes.NRA)
file_num = 0 # Reset the file number and continue
else:
# For orion targets that only have one image, just crop that single image
if scoring_type == ScoringTypes.ORION_USAS_50:
crop_image(file_image, TargetTypes.ORION_USAS_50)