-
Notifications
You must be signed in to change notification settings - Fork 4
/
EvoCamoGame.h
2381 lines (2166 loc) · 99.3 KB
/
EvoCamoGame.h
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
//
// EvoCamoGame.h
// TexSyn
//
// Created by Craig Reynolds on 1/20/21.
// Copyright © 2021 Craig Reynolds. All rights reserved.
//
//------------------------------------------------------------------------------
//
// "Interactive Evolution of Camouflage" implemented on top of TexSyn and
// LazyPredator. Slightly updated version of the approach used in:
//
// Craig Reynolds. 2011. Interactive Evolution of Camouflage.
// Artificial Life 17(2). https://doi.org/10.1162/artl_a_00023
//
// This class is the top level "main()" for unix-style command evo_camo_game.
// See doc: https://cwreynolds.github.io/TexSyn/evo_camo_game_doc.html
//
// The code in this file became the paper:
//
// Craig Reynolds. 2023. Coevolution of Camouflage. 2023 Artificial Life
// Conference. MIT Press. https://doi.org/10.1162/isal_a_00583
// Preprint with supplemental materials and high resolution images:
// https://arxiv.org/abs/2304.11793
//
// Prototyping inside TexSyn, maybe should be its own library/git repository.
//
//------------------------------------------------------------------------------
#pragma once
#include "GP.h"
#include "Texture.h"
#include "Utilities.h"
#include "RandomSequence.h"
class EvoCamoGame
{
public:
// Constructor to get parameters from pre-parsed "unix style" command line.
EvoCamoGame(const CommandLine& cmd)
: run_name_(runNameDefault(cmd)),
background_image_directory_(cmd.positionalArgument(1)),
output_directory_(cmd.positionalArgument(2, ".")),
output_directory_this_run_(runOutputDirectory()),
background_scale_(cmd.positionalArgument(3, float(0.5))),
random_seed_(cmd.positionalArgument(4, int(LPRS().defaultSeed()))),
gui_size_(cmd.positionalArgument(5, 1200),
cmd.positionalArgument(6, 800)),
gui_(gui_size_, Vec2(), run_name_),
individuals_(cmd.positionalArgument(7, 120)),
subpops_(cmd.positionalArgument(8, 6)),
max_steps_(cmd.positionalArgument(9, 12000)),
max_init_tree_size_(cmd.positionalArgument(10, 100)),
min_crossover_tree_size_
(cmd.positionalArgument(11, max_init_tree_size_ * 0.5f)),
max_crossover_tree_size_
(cmd.positionalArgument(12, max_init_tree_size_ * 1.5f))
{
if ((backgroundScale() > 10000) || (backgroundScale() < 0.0001))
{
// Better warning for confusing case: the output_directory arg was
// accidentally omitted, scale read a value (20210529) which was WAY
// too large and caused a confusing assert fail deep inside OpenCV.
debugPrint(backgroundScale());
assert(!"backgroundScale() seems out of range");
}
if (background_image_directory_.empty())
{
// Exit with failure after listing command arguments.
std::cout << cmd.commandNameWithoutPath();
std::cout << " requires at least one pathname parameter,";
std::cout << " others may be omitted from the end:" << std::endl;
std::cout << " background_image_directory (required)"<<std::endl;
// TODO this should say something about creating a time-stamped
// directory under this for output from this run.
std::cout << " output_directory (defaults to .)" << std::endl;
std::cout << " background_scale (defaults to 0.5)" << std::endl;
std::cout << " random_seed (else: default seed)" << std::endl;
std::cout << " window width (defaults to 1200)" << std::endl;
std::cout << " window height (defaults to 800)" << std::endl;
std::cout << " individuals (defaults to 120)" << std::endl;
std::cout << " subpopulations (defaults to 6)" << std::endl;
std::cout << " max_steps (defaults to 12000)" << std::endl;
std::cout << " max_init_tree_size (defaults to 100)"<< std::endl;
std::cout << " min_crossover_tree_size "
"(default max_init_tree_size_ * 0.5)" << std::endl;
std::cout << " max_crossover_tree_size "
"(default max_init_tree_size_ * 1.5)" << std::endl;
std::cout << std::endl;
}
else
{
std::cout << "Interactive evolution of camouflage:" << std::endl;
debugPrint(run_name_);
debugPrint(background_image_directory_);
debugPrint(output_directory_);
debugPrint(output_directory_this_run_);
debugPrint(background_scale_);
debugPrint(random_seed_);
debugPrint(gui_.getSize());
debugPrint(individuals_);
debugPrint(subpops_);
debugPrint(max_steps_);
debugPrint(max_init_tree_size_);
debugPrint(min_crossover_tree_size_);
debugPrint(max_crossover_tree_size_);
}
}
// Read specified background image files, scale, and save as cv::Mats.
void collectBackgroundImages()
{
// Names of all files in backgroundImageDirectory() (expect image files)
const std::vector<std::string> background_image_filenames =
directory_filenames(backgroundImageDirectory());
std::cout << "Reading background images:" << std::endl;
int min_x = std::numeric_limits<int>::max();
int min_y = std::numeric_limits<int>::max();
for (auto& filename : background_image_filenames)
{
// Compose absolute pathname for this background image file.
std::string pathname = backgroundImageDirectory() + "/" + filename;
// Read the image file into an OpenCV image.
cv::Mat bg = cv::imread(pathname);
// When valid image file. (To ignore README.txt, .DS_Store, etc.)
if (cv::haveImageReader(pathname))
{
std::cout << " " << pathname << std::endl;
// Keep track of smallest image dimensions.
if (min_x > bg.cols) { min_x = bg.cols; }
if (min_y > bg.rows) { min_y = bg.rows; }
// Adjust the size/resolution by "background_scale" parameter.
cv::resize(bg, bg,
cv::Size(), backgroundScale(), backgroundScale(),
cv::INTER_AREA);
// Add to collection of background images.
addBackgroundImage(bg);
}
}
std::cout << "Found " << backgroundImages().size();
std::cout << " background images." << std::endl;
assert(!backgroundImages().empty());
checkBackgroundImageSizes(min_x, min_y);
}
// Verify all background images (as scaled) are larger than GUI window.
void checkBackgroundImageSizes(int min_x, int min_y)
{
float s = backgroundScale();
int s_min_x = s * min_x;
int s_min_y = s * min_y;
std::cout << std::endl;
std::cout <<"Width of GUI: "<<guiSize().x()<<", of scaled bg images: "
<< s_min_x << ", ratio: " << s_min_x / guiSize().x() << std::endl;
std::cout <<"Height of GUI: "<<guiSize().y()<<", of scaled bg images: "
<< s_min_y << ", ratio: " << s_min_y / guiSize().y() << std::endl;
std::cout << std::endl;
if ((s_min_x < guiSize().x()) || (s_min_y < guiSize().y()))
{
std::cout << std::endl
<< "ERROR: BACKGROUND IMAGE IS TOO SMALL FOR WINDOW."
<< std::endl << "The smallest of " << backgroundImages().size()
<< " background images is " << min_x << "×" << min_y
<< " pixels." << std::endl << "Background scale " << s
<< " produces " << s_min_x << "×" << s_min_y << " image, "
<< "smaller than " << guiSize().x() << "×" << guiSize().y()
<< " window size." << std::endl
<< "Increase scale, reduce window size, or choose other images."
<< std::endl;
exit(EXIT_FAILURE);
}
}
// Randomly select one of the given backgrounds, then randomly select a
// window-sized rectangle within it.
cv::Mat selectRandomBackgroundForWindow()
{
// Pick one of the given background images at random.
const cv::Mat& bg = LPRS().randomSelectElement(backgroundImages());
// Find how much bigger (than GUI window) the original background is.
int dx = std::max(0, int(bg.cols - guiSize().x()));
int dy = std::max(0, int(bg.rows - guiSize().y()));
// Randomly select an offset within that size difference.
Vec2 random_position(LPRS().randomN(dx), LPRS().randomN(dy));
// Return a "submat" reference into the random rectangle inside "bg".
return Texture::getCvMatRect(random_position, guiSize(), bg);
}
// Run the evolution simulation.
void run()
{
// Cannot run without background image directory.
if (backgroundImageDirectory().empty()) return;
LPRS().setSeed(random_seed_);
std::cout << "Create initial population." << std::endl;
setPopulation(std::make_shared<Population>(individuals_,
subpops_,
max_init_tree_size_,
min_crossover_tree_size_,
max_crossover_tree_size_,
GP::fs()));
// Read specified background image files, save as cv::Mats.
collectBackgroundImages();
fs::path out = outputDirectoryThisRun();
std::cout << "Create output directory for this run: ";
std::cout << out << std::endl;
fs::create_directory(out);
// Init GUI window.
gui().setWindowName(run_name_);
gui().refresh();
// Loop of interactive evolution steps, until "Q" command or forced exit.
setRunningState(true);
while (getRunningState())
{
// Display step count in GUI title bar.
std::string step_string = " (step " + getStepAsString() + ")";
gui().setWindowTitle(run_name_ + step_string);
logFunctionUsageCounts(out);
// Evolution step with wrapped EvoCamoGame::tournamentFunction().
getPopulation()->evolutionStep([&]
(TournamentGroup tg)
{ return tournamentFunction(tg); });
setRunningState(getMaxSteps() >= getStepCount());
}
// Delete Population instance.
setPopulation(nullptr);
}
// TODO temporary utility for debugging random non-overlapping placement
// TODO to be removed eventually
void testdraw(const TournamentGroup& tg,
const std::vector<Disk>& disks,
Vec2 rect_min,
Vec2 rect_max,
float min_center_to_center)
{
int p = 0;
gui().clear();
gui().drawRectangle(rect_max - rect_min, rect_min, Color(0.6));
for (auto& tgm : tg.members())
{
Texture* texture = GP::textureFromIndividual(tgm.individual);
texture->rasterizeToImageCache(textureSize(), true);
Vec2 center_to_ul = Vec2(1, 1) * textureSize() / 2;
Vec2 position = disks.at(p++).position - center_to_ul;
int size = textureSize();
Vec2 size2(size, size);
gui().drawCircle(min_center_to_center - size / 2,
disks.at(p-1).position,
Color(1));
cv::Mat target = gui().getCvMatRect(position, size2);
texture->matteImageCacheDiskOverBG(size, target);
}
gui().refresh();
Texture::waitKey(10);
}
// TournamentFunction for "Interactive Evolution of Camouflage".
TournamentGroup tournamentFunction(TournamentGroup tg)
{
// Initialize "global variables" used by mouse callback handler.
tournament_group_ = tg;
background_image_ = selectRandomBackgroundForWindow();
// Generate and store random non-overlapping prey disks in gui window.
generatePreyPlacement();
// Draw the randomly selected background, then the 3 textures on top.
gui().drawMat(background_image_, Vec2());
drawTournamentGroupOverBackground(tg);
// Update the onscreen image. Wait for user to click on one texture.
TimePoint time_start_waiting = TimeClock::now();
gui().refresh();
setMouseCallbackForTournamentFunction();
waitForUserInput();
// Note time user took to respond. Ignored in logging of time per frame.
getPopulation()->setIdleTime(TimeClock::now() - time_start_waiting);
// Designate selected Texture's Individual as worst of TournamentGroup.
Individual* worst = selectIndividualFromMouseClick(getLastMouseClick());
tg.designateWorstIndividual(worst);
// Mark returned TournamentGroup as invalid if predator failed to locate
// a prey. That is, if either: the user's mouse click or the xy position
// returned from the "predator vision" neural net--is not inside any of
// the three disks.
if (worst == nullptr)
{
tg.setValid(false);
tournament_group_ = tg;
invalidTournamentGroupHook();
incrementPredatorFails();
std::cout << " Predator fooled: no prey selected." << std::endl;
}
// Clear GUI, return updated TournamentGroup.
gui().clear();
gui().refresh();
return tg;
}
// For customizations by derived classed (below).
virtual void invalidTournamentGroupHook() {}
// TODO 20230929 Keep or move elsewhere? This was added to allow calling
// it from here but implementing it in derived classes. But then I added
// invalidTournamentGroupHook() as a cleaner way to do that. saveThisStep()
// is no longer used in EvoCamoGame.
virtual bool saveThisStep() { return false; }
// Generate and store random non-overlapping prey disks in gui window.
virtual void generatePreyPlacement()
{
// Restrict Texture disks to be completely inside a rectangle inset
// from the window edge a Texture's radius. Rectangle defined by two
// diagonally opposite corners.
float radius = textureSize() / 2;
Vec2 rect_min = Vec2(radius + 1, radius + 1);
Vec2 rect_max = guiSize() - rect_min;
// Find non-overlapping positions for the Textures in TournamentGroup.
float margin = radius;
// TODO dummy function, should be cleaned up (removed).
auto overlap_viz = [&](const std::vector<Disk>& disks)
{ /*testdraw(tg,disks,rect_min,rect_max,textureSize()+margin);*/ };
setPreyDisks(Disk::randomNonOverlappingDisksInRectangle(3, radius,
radius, margin, rect_min, rect_max, LPRS(), overlap_viz));
}
// Count "invalid tournaments" -- aka "predator fails"
int getPredatorFails() const { return predator_fails_; }
void incrementPredatorFails() { predator_fails_++; }
// Draw Textures of TournamentGroup over current background in GUI.
void drawTournamentGroupOverBackground(const TournamentGroup& tg)
{
int p = 0;
std::vector<Vec2> prey_texture_positions;
for (auto& tgm : tg.members())
{
int size = textureSize();
Texture* texture = GP::textureFromIndividual(tgm.individual);
texture->rasterizeToImageCache(size, true);
Vec2 center_to_ul = Vec2(1, 1) * size / 2;
Vec2 position = getPreyCenter(p++) - center_to_ul;
prey_texture_positions.push_back(position);
cv::Mat target = gui().getCvMatRect(position, Vec2(size, size));
texture->matteImageCacheDiskOverBG(size, target);
}
// TODO 20211010 turn this off. If needed later add global enable flag.
// writeTrainingSetData(prey_texture_positions);
}
// Ad hoc idle loop, waiting for user input. Exits on left mouse click, the
// user's selection of the "worst" camouflage. This also "listens" for and
// executes single character commands: "t" and "Q".
// (20211230 make virtual so can be overridden, eg by EvoCamoVsStaticFCD)
virtual void waitForUserInput()
{
waitForMouseUp(); // In case mouse button still down from last click.
setWaitForMouseClick(true);
int previous_key = cv::waitKeyEx(1);
// Loop until mouse is clicked in window.
while (getWaitForMouseClick())
{
// Wait for 1/4 second, and read any key typed during that time.
int key = cv::waitKey(250); // 1/4 second (250/1000)
// When newly-pressed (key down) event.
if ((key > 0) && (key != previous_key))
{
// For "t" command: write whole window tournament image to file.
if (key == 't') { writeTournamentImageToFile(); }
// For "Q" command: exit run() loop.
if (key == 'Q')
{
setRunningState(false);
setWaitForMouseClick(false);
}
}
previous_key = key;
}
}
// Waits for mouse left button to be released (via mouse_left_button_down_).
void waitForMouseUp()
{
// Wait (1/4 second at a time) until mouse_left_button_down_ is false.
while (mouse_left_button_down_) { cv::waitKey(250); }
}
// Controls mouse behavior during a tournament.
void setMouseCallbackForTournamentFunction()
{
auto mouse_callback =
[](int event, int x, int y, int flags, void* userdata)
{
// Cast void* to EvoCamoGame*
auto c = static_cast<EvoCamoGame*>(userdata);
if (event == cv::EVENT_LBUTTONDOWN) // Left button down.
{
c->mouse_left_button_down_ = true;
Vec2 click(x, y);
if (flags & cv::EVENT_FLAG_SHIFTKEY)
{
Individual* i = c->selectIndividualFromMouseClick(click);
c->writeThumbnailImageToFile(i);
}
else
{
c->setLastMouseClick(click);
c->setWaitForMouseClick(false);
}
}
if (event == cv::EVENT_LBUTTONUP) // Left button up.
{
c->mouse_left_button_down_ = false;
}
};
cv::setMouseCallback(gui().getWindowName(), mouse_callback, this);
}
// See if click was within a Texture disk, set "worst" Individual if so.
Individual* selectIndividualFromMouseClick(Vec2 mouse_click_position)
{
int p = 0;
Individual* selected = nullptr;
for (auto& tgm : tournament_group_.members())
{
Vec2 offset = getPreyCenter(p++) - mouse_click_position;
float distance = offset.length();
if (distance <= (textureSize() / 2)) { selected = tgm.individual; }
}
return selected;
}
// Write current "tournament" image (3 textures and background) to file.
void writeTournamentImageToFile()
{
fs::path path = outputDirectoryThisRun();
path /= "step_" + getStepAsString() + ".png";
writeTournamentImageToFile(path, gui().getCvMat());
}
// Write given "tournament" image to given pathname. Also note on log.
void writeTournamentImageToFile(const fs::path& pathname,
const cv::Mat& image)
{
std::cout << "Writing tournament image to file ";
std::cout << pathname.string() << std::endl;
cv::imwrite(pathname, image);
}
// Write a "thumbnail" image with Texture and its background neighborhood.
void writeThumbnailImageToFile(Individual* individual)
{
// Get index of this Individual within current TournamentGroup.
int index = individualToTournamentIndex(individual);
// Construct reference to thumbnail-sized square of current background.
Vec2 size2(textureSize(), textureSize());
Vec2 center = getPreyCenter(index);
cv::Mat cropped_bg =
Texture::getCvMatRect(center - size2, size2 * 2, background_image_);
// Construct image with Texture matted over cloned (cropped) background.
cv::Mat image = cropped_bg.clone();
Texture* texture = GP::textureFromIndividual(individual);
cv::Mat image_middle = Texture::getCvMatRect(size2/2, size2, image);
texture->matteImageCacheDiskOverBG(textureSize(), image_middle);
// Construct a name for the thumbnail image file.
std::vector<std::string> suffixes = {"_a", "_b", "_c"};
std::string suffix = suffixes.at(index);
std::string step = getStepAsString();
// Construct pathname for file.
fs::path path = outputDirectoryThisRun();
path /= ("thumbnail_" + step + suffix + ".png");
// Write image file.
std::cout << "Writing thumbnail image to file " << path << std::endl;
cv::imwrite(path, image);
// Write source code file.
writeSourceCodeToFile(individual, "source_" + step + suffix + ".txt");
}
// Write text source code for an Individual: indented "C"-style notation.
void writeSourceCodeToFile(Individual* individual, std::string filename)
{
// Construct pathname for file.
fs::path path = outputDirectoryThisRun();
path /= filename;
std::cout << "Writing source code to file " << path << std::endl;
// Open stream to file.
std::ofstream output_file_stream(path);
// Generate indented functional notation for given Individual's GpTree.
output_file_stream << individual->tree().to_string(true);
output_file_stream.close();
}
// Given an Individual, find its index within the "current" TournamentGroup.
int individualToTournamentIndex(Individual* individual) const
{
int i = 0;
int k = 0;
for (auto& tgm : tournament_group_.members())
{
if (tgm.individual == individual) i = k;
k++;
}
return i;
}
// The size of background images is adjusted by this value. It is expected
// to be less than 1, indicating that the input photographic images are
// generally larger than the screen resolution. Assumes only one scale is
// needed, that the user has curated the background images to be at the
// same scale. (If there is need for the scale to be bigger than 1 some
// adjustments may be needed.)
float backgroundScale() const { return background_scale_; }
// Pathname of directory containing raw background image files.
const std::string backgroundImageDirectory() const
{ return background_image_directory_; }
// Collection of cv::Mat to be used as background image source material.
const std::vector<cv::Mat>& backgroundImages() const
{ return background_images_; }
// Add cv::Mat to collection of background images.
void addBackgroundImage(cv::Mat& bg) { background_images_.push_back(bg); }
// GUI size: drawable area in pixels.
Vec2 guiSize() const { return gui_size_; }
// Reference to GUI.
GUI& gui() { return gui_; }
const GUI& gui() const { return gui_; }
// TODO very temp
int texture_size_ = 201;
// TODO 20211231 for backward compatibility / testing
int textureSize() const { return getTextureSize(); }
int getTextureSize() const { return texture_size_; }
void setTextureSize(int size) { texture_size_ = size; }
// Get default run name from background_image_directory_.
// (Provides consistent behavior with and without trailing "/".)
// ((Provides "no_name" as ultimate fall back for empty command line.))
std::string runNameDefault(const CommandLine& command_line)
{
fs::path path = command_line.positionalArgument(1);
std::string fn = path.filename();
if (fn.empty()) { fn = std::string(path.parent_path().filename()); }
if (fn.empty()) { fn = "no_name"; }
return fn;
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// TODO 20221013 actually write log to file.
// // A subdirectory under output_directory_ for results from this run.
// std::string runOutputDirectory()
// {
// if (!fs::exists(output_directory_))
// {
// debugPrint(output_directory_);
// assert(!"output_directory_ does not exist.");
// }
// fs::path run_output_dir = output_directory_;
// run_output_dir /= (run_name_ + "_" + date_hours_minutes());
// return run_output_dir;
// }
// A subdirectory under output_directory_ for results from this run.
std::string runOutputDirectory()
{
if (!fs::exists(output_directory_))
{
debugPrint(output_directory_);
assert(!"output_directory_ does not exist.");
}
fs::path run_output_dir = output_directory_;
run_id_ = run_name_ + "_" + date_hours_minutes();
// run_output_dir /= (run_name_ + "_" + date_hours_minutes());
run_output_dir /= run_id_;
return run_output_dir;
}
std::string getRunID() const { return run_id_; }
std::string run_id_;
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Returns, as a string, current Population evolution "step" number.
std::string getStepAsString() const
{
return getStepAsString(0);
}
// Returns, as a string, current Population "step" number plus signed offset.
std::string getStepAsString(int offset) const
{
return std::to_string(offset + getStepCount());
}
// Get/set a shared_ptr to this run's current Population.
std::shared_ptr<Population> getPopulation() { return population_; };
std::shared_ptr<Population> getPopulation() const { return population_; };
void setPopulation(std::shared_ptr<Population> p) { population_ = p; };
void logFunctionUsageCounts(const fs::path& out)
{
int step = getStepCount();
if ((step % 10) == 0)
{
// Preserve each named counter, but set its count to zero.
cfu_.zeroEachCounter();
// Count total GpFunction usage over entire Population of GpTrees.
cfu_.count(*getPopulation());
// Open output stream to file in append mode.
std::ofstream outfile;
outfile.open(out / "function_counts.txt", std::ios::app);
if (step == 0)
{
std::string names = "steps,ave_tree_size,ave_fitness,";
auto func = [&](std::string s, int c) { names += s + ","; };
cfu_.applyToAllCounts(func);
std::cout << names << std::endl;
outfile << names << std::endl;
}
std::string counts;
auto add_count = [&](int c){ counts += std::to_string(c) + ","; };
add_count(step);
add_count(getPopulation()->averageTreeSize());
add_count(getPopulation()->averageFitness());
cfu_.applyToAllCounts([&](std::string s, int c){ add_count(c); });
std::cout << counts << std::endl;
outfile << counts << std::endl;
}
}
// Every n frames save a JPEG image of the "tournament" (whole window) and
// append line: step number, pixel xy bounding box of all three prey.
void writeTrainingSetData(const std::vector<Vec2>& prey_texture_positions)
{
int n = 10;
if ((getStepCount() % n) == 0)
{
// Construct path for training set directory, create if needed.
fs::path directory = outputDirectoryThisRun();
directory /= "training_set";
fs::create_directory(directory);
// Construct pathname image, write to file.
fs::path image_path = directory;
image_path /= "step_" + getStepAsString() + ".jpeg";
cv::imwrite(image_path, gui().getCvMat());
// Open output stream to bounding_boxes.txt file in append mode.
std::ofstream outfile;
outfile.open(directory / "bounding_boxes.txt", std::ios::app);
// Construct line of text with step number and bounding boxes.
// Note: uses OpenCV coordinates, origin in upper left.
// Each bounding box is "(min_x min_y max_x max_y)"
std::string bboxes = getStepAsString();
for (auto p : prey_texture_positions)
{
bboxes += " (";
bboxes += std::to_string(int(p.x())) + " ";
bboxes += std::to_string(int(p.y())) + " ";
bboxes += std::to_string(int(p.x() + textureSize())) + " ";
bboxes += std::to_string(int(p.y() + textureSize())) + ")";
}
// Write to console log and to bounding_boxes.txt file.
std::cout << "prey texture bounding boxes: " << bboxes << std::endl;
outfile << bboxes << std::endl;
}
}
// Get/set position of most recent mouse (left) click in GUI.
Vec2 getLastMouseClick() const { return last_mouse_click_; }
void setLastMouseClick(Vec2 mouse_pos) { last_mouse_click_ = mouse_pos; }
// Get/set whether we are currently waiting for a mouse click in GUI.
bool getWaitForMouseClick() const { return wait_for_mouse_click_; }
void setWaitForMouseClick(bool wait) { wait_for_mouse_click_ = wait; }
// Get/set whether we are currently running the step loop in run().
bool getRunningState() const { return running_; }
void setRunningState(bool running_state) { running_ = running_state; }
// Accessor for command line argument used by derived classes.
std::string outputDirectoryThisRun() const
{ return output_directory_this_run_; }
// Get/set collection of prey Disks within GUI window.
std::vector<Disk> getPreyDisks() const { return disks_; }
void setPreyDisks(const std::vector<Disk>& disks) { disks_ = disks; }
// Get center position of i-th prey disk.
Vec2 getPreyCenter(int i) const { return disks_.at(i).position; }
// Read-only access to most recently saved (copied) TournamentGroup().
const TournamentGroup& getTournamentGroup() const {return tournament_group_;}
// Current simulation step, held by Population object.
int getStepCount() const { return getPopulation()->getStepCount(); }
// Exit simulation after this many steps.
int getMaxSteps() const { return max_steps_; }
private:
// Name of run. (Defaults to directory holding background image files.)
const std::string run_name_;
// Pathname of directory containing raw background image files.
const std::string background_image_directory_;
// Pathname of directory into which we can create a run log directory.
const std::string output_directory_;
// A subdirectory under output_directory_ for results from this run.
const std::string output_directory_this_run_;
// Collection of cv::Mat to be used as background image source material.
std::vector<cv::Mat> background_images_;
// The size of background images is adjusted by this value (usually < 1).
const float background_scale_ = 1;
// Seed for RandomSequence LPRS() to be used during this run
int random_seed_ = LPRS().defaultSeed();
// GUI size: drawable area in pixels.
Vec2 gui_size_;
// GUI object
GUI gui_;
// Default parameters for Population
int individuals_ = 120;
int subpops_ = 6;
int max_init_tree_size_ = 100;
int min_crossover_tree_size_ = 50;
int max_crossover_tree_size_ = 150;
// For logging GpFunction usage over evolutionary time.
CountFunctionUsage cfu_;
// Count "invalid tournaments" -- aka "predator fails"
int predator_fails_ = 0;
// Exit simulation after this many steps.
int max_steps_ = 12000;
//-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
// Note: the six variables below communicate "global" state with the mouse
// callback handler. This is not thread safe and would need redesign for
// multithreading. But then, since this connects directly to the GUI for an
// interactive task, multithreading seems unlikely?
//
// Store position of most recent mouse (left) click in GUI.
Vec2 last_mouse_click_;
// True while waiting for user to select one texture on screen.
bool wait_for_mouse_click_;
// Collection of Disks describing layout of Textures in GUI window.
std::vector<Disk> disks_;
// TournamentGroup with pointers to 3 textures of most recent tournament.
TournamentGroup tournament_group_;
// Randomly selected rectagle of randomly selected background image.
cv::Mat background_image_;
// Monitor up/down status of (left) mouse button.
bool mouse_left_button_down_ = false;
// Points to heap-allocated Population instance during run() function.
std::shared_ptr<Population> population_ = nullptr;
// True during the step loop in run(). Set to false by "Q" command.
bool running_ = false;
//-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
};
// TODO this should be in its own file.
// Experimental code for generating training set of images for learning to
// "find conspicuous disks". Generates random image files with a background
// texture and a disk of another texture matted on top. Image files maybe
// 1024 square? Textures a mix of "random" TexSyn textures and some natural
// textures from photos, as in /Users/cwr/Pictures/camouflage_backgrounds
//
// (Maybe filenames like 3485729384_0132_0981.jpg where the first number is
// just a random UID, and the second two numbers are the ground truth center
// location of the disk.)
// command line args:
// fcd how_many
// output_dir
// input_photo_dir
// seed
// bg_scale
// output_size
// disk_size
// tree_size
class GenerateTrainingSetForFindConspicuousDisks
{
typedef fs::directory_iterator di;
typedef fs::path pn;
// TODO 20220127 adjust bg scale by output_size_
const int dos_ = 1024; // Default output size (to adjust background_scale_).
public:
// Constructor to get parameters from pre-parsed "unix style" command line.
GenerateTrainingSetForFindConspicuousDisks(const CommandLine& cmd)
: how_many_(cmd.positionalArgument(1, 1)),
output_directory_(cmd.positionalArgument(2, ".")),
input_photo_dir_(cmd.positionalArgument(3, ".")),
random_seed_(cmd.positionalArgument(4, int(LPRS().defaultSeed()))),
background_scale_(cmd.positionalArgument(5, float(0.5)) *
(cmd.positionalArgument(6, dos_) / float(dos_))),
output_size_(cmd.positionalArgument(6, dos_)),
disk_size_(cmd.positionalArgument(7, 201)),
tree_size_(cmd.positionalArgument(8, 40))
{
std::cout << "GenerateTrainingSetForFindConspicuousDisks:" << std::endl;
std::cout << " "; debugPrint(how_many_);
std::cout << " "; debugPrint(output_directory_);
std::cout << " "; debugPrint(input_photo_dir_);
std::cout << " "; debugPrint(random_seed_);
std::cout << " "; debugPrint(background_scale_);
std::cout << " "; debugPrint(output_size_);
std::cout << " "; debugPrint(disk_size_);
std::cout << " "; debugPrint(tree_size_);
}
// Perform the run generating training data for "find conspicuous disks”.
// TODO current runs forever.
void run()
{
LPRS().setSeed(random_seed_);
readAllInputPhotos();
// while (output_counter_ < how_many_) { generateOneOutputImage(); }
while (outputCounter() < how_many_) { generateOneOutputImage(); }
}
virtual void generateOneOutputImage()
{
cv::Mat output = selectRandomBackgroundImage().clone();
cv::Mat disk = selectRandomDiskImage();
Vec2 diff = outputSize() - diskSize();
Vec2 position(diff.x() * LPRS().frandom01(),
diff.y() * LPRS().frandom01());
Vec2 center = position + diskSize() / 2;
// Matte disk texture over random position in output texture.
cv::Mat target = Texture::getCvMatRect(position, diskSize(), output);
Texture::matteImageCacheDiskOverBG(disk, target);
writeOneOutputImage(output, center);
}
void writeOneOutputImage(const cv::Mat& output, Vec2 center)
{
// Display and save the new training image.
cv::imshow("output", output);
// TODO needs to save to file
std::string path = output_directory_ / outputFileName(center);
// std::cout << "Writing image " << ++output_counter_
// << " to " << path << " ... ";
incrementOutputCounter();
std::cout << "Writing image " << outputCounter()
<< " to " << path << " ... ";
cv::imwrite(path, output);
std::cout << "done." <<std::endl;
}
// Read all input photo files as cv::Mats, adjust size, save when OK.
void readAllInputPhotos()
{
Timer t("Reading background images");
std::cout << "Reading background images:" << std::endl;
collectPhotoPathnames(input_photo_dir_);
std::sort(all_photo_pathnames_.begin(), all_photo_pathnames_.end());
for (auto& pathname : all_photo_pathnames_)
{
// Read photo's image file into an OpenCV image.
cv::Mat photo = cv::imread(pathname);
// For "non small" images, adjust by background_scale_.
// (The buggy special case is for the fungus photos. Revisit?)
int min_dim = std::min(photo.rows, photo.cols);
bool big_enough = min_dim >= (outputSize().x() / background_scale_);
float scale = background_scale_ * (big_enough ? 1 : 2);
cv::resize(photo, photo, cv::Size(), scale, scale, cv::INTER_AREA);
// If the adjusted size is large enough
std::cout << " ";
if (std::min(photo.rows, photo.cols) >= outputSize().x())
{
// Add to collection of background images.
std::cout << "Adding " << pathname << std::endl;
all_photos_.push_back(photo);
}
else
{
// Log and ignore.
std::cout << "Ignore " << pathname << " Scaled input image "
<< "smaller than output image size ("
<< outputSize().x() << ")." << std::endl;
}
}
std::cout << "Found " << all_photos_.size();
std::cout << " background images." << std::endl;
assert(!all_photos_.empty());
}
// From the given input_photo_dir, search the sub-directory tree, collecting
// pathnames of all valid image files into all_photo_pathnames_.
void collectPhotoPathnames(pn directory)
{
// For each item within the given top level directory.
for (const auto& i : di(directory))
{
pn item = i;
if (fs::is_directory(item))
{
// Recurse on sub-directories.
collectPhotoPathnames(item);
}
else
{
// Save the pathnames that look like image files.
if (cv::haveImageReader(item))
{
all_photo_pathnames_.push_back(item);
}
}
}
}
// Randomly select content to be the background of a generated image. This
// may be from one of the given source photographs, or synthesized on the
// fly as a TexSyn texture.
cv::Mat selectRandomBackgroundImage()
{
// Use a photo 60% of the time, otherwise synthesize a texture.
return (LPRS().frandom01() < 0.6 ?
fcdSelectRandomPhoto(outputSize()) :
fcdMakeRandomTexture(outputSize()));
}
// Randomly select content for disk on a generated image. This may be from
// one of the given source photographs, or synthesized on the fly as a
// TexSyn texture.
// cv::Mat fcdSelectRandomDiskImage()
cv::Mat selectRandomDiskImage()
{
// Use a photo 30% of the time, otherwise synthesize a texture.
return (LPRS().frandom01() < 0.3 ?
fcdSelectRandomPhoto(diskSize()) :
fcdMakeRandomTexture(diskSize()));
}
// Randomly select one of the given source photographs, then randomly select
// a "size_in_pixels" rectangle within it.
cv::Mat fcdSelectRandomPhoto(Vec2 size_in_pixels)
{
// Pick one of the given background photos at random.
const cv::Mat& photo = LPRS().randomSelectElement(all_photos_);
// How much bigger (than the output) is the background image.
int dx = std::max(0, int(photo.cols - outputSize().x()));
int dy = std::max(0, int(photo.rows - outputSize().y()));
// Randomly select an offset within that size difference.
Vec2 random_position(LPRS().randomN(dx), LPRS().randomN(dy));
// Return a "submat" reference into the random rectangle inside "bg".
return Texture::getCvMatRect(random_position, size_in_pixels, photo);
}
// Generate a random TexSyn texture from a random LazyPredator GpTree.
// Changed to loop until the texture is not boringly uniform. (20220127)
cv::Mat fcdMakeRandomTexture(Vec2 size_in_pixels)
{
while (true)
{
std::cout << "Making texture (" << size_in_pixels.x() << "x"
<< size_in_pixels.y() << ")..." << std::flush;
Timer t("done,");
int max_init_tree_size = tree_size_;
const FunctionSet& function_set = GP::fs();
GpTree tree;
function_set.makeRandomTree(max_init_tree_size, tree);
Individual individual(tree);
Texture* texture = GP::textureFromIndividual(&individual);
texture->rasterizeToImageCache(size_in_pixels.x(), false);
cv::Mat cv_mat = texture->getCvMat();
if (Texture::matUniformity(cv_mat) < 0.9) { return cv_mat; }
std::cout << "reject uniform texture...";
}
}
// Random UUID-like string plus pixel x and y of disk center.
std::string outputFileName(Vec2 center)
{
return (n_letters(10, LPRS()) +
"_" + std::to_string(int(center.x())) +
"_" + std::to_string(int(center.y())) +
getOutputFilenameExtension());
}
// Get/set image file format for output, specified by pathname extension.
std::string getOutputFilenameExtension() const
{
return output_filename_extension_;
}
void setOutputFilenameExtension(std::string extension)
{
output_filename_extension_ = extension;
}
Vec2 diskSize() const { return Vec2(disk_size_, disk_size_); }
Vec2 outputSize() const { return Vec2(output_size_, output_size_); }
int treeSize() const { return tree_size_; }
// TODO 20220302 add accessor to make visibe from derived classes.
int outputCounter() const { return output_counter_; }
void incrementOutputCounter() { output_counter_++; }