├── ICEAutomation.bat
├── .gitignore
├── src
├── ICEAutomation.csproj
├── App.config
├── Options.cs
├── Program.cs
└── ComposeAppService.cs
├── .vscode
├── tasks.json
└── launch.json
├── ICEAutomation.sln
└── README.md
/ICEAutomation.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | start "" "C:\Projects\ICEAutomation\ICEAutomation.exe" %*
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | #Ignore thumbnails created by Windows
3 | Thumbs.db
4 | #Ignore files built by Visual Studio
5 | *.obj
6 | *.exe
7 | *.pdb
8 | *.user
9 | *.aps
10 | *.pch
11 | *.vspscc
12 | *_i.c
13 | *_p.c
14 | *.ncb
15 | *.suo
16 | *.tlb
17 | *.tlh
18 | *.bak
19 | *.cache
20 | *.ilk
21 | *.log
22 | [Bb]in
23 | [Dd]ebug*/
24 | *.lib
25 | *.sbr
26 | obj/
27 | [Rr]elease*/
28 | _ReSharper*/
29 | [Tt]est[Rr]esult*
30 | .vs/
31 | #Nuget packages folder
32 | packages/
33 |
--------------------------------------------------------------------------------
/src/ICEAutomation.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | netcoreapp3.1
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "build",
8 | "command": "dotnet",
9 | "type": "shell",
10 | "args": [
11 | "build",
12 | // Ask dotnet build to generate full paths for file names.
13 | "/property:GenerateFullPaths=true",
14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel
15 | "/consoleloggerparameters:NoSummary"
16 | ],
17 | "group": "build",
18 | "presentation": {
19 | "reveal": "silent"
20 | },
21 | "problemMatcher": "$msCompile"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (console)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | "program": "${workspaceFolder}/src/bin/Debug/netcoreapp3.1/ICEAutomation.exe",
13 | "args": ["process", "6"],
14 | "cwd": "${workspaceFolder}",
15 | "console": "internalConsole",
16 | "stopAtEntry": false
17 | },
18 | {
19 | "name": ".NET Core Attach",
20 | "type": "coreclr",
21 | "request": "attach",
22 | "processId": "${command:pickProcess}"
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/ICEAutomation.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.27703.2042
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ICEAutomation", "src\ICEAutomation.csproj", "{4CD879EB-28B9-4511-B997-34C07288D813}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {4CD879EB-28B9-4511-B997-34C07288D813}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {4CD879EB-28B9-4511-B997-34C07288D813}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {4CD879EB-28B9-4511-B997-34C07288D813}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {4CD879EB-28B9-4511-B997-34C07288D813}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {FDA5FE7F-D35F-4224-8867-43215D49EACF}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ICE Automation
2 |
3 | A command line application to bach process image stitching using the marvellous [Image Compose Editor (ICE)](https://www.microsoft.com/en-us/research/product/computational-photography-applications/image-composite-editor).
4 |
5 | # Build
6 | Now the project has moved to netcoreapp3.1. I recommend you to use VS Code to work with it, but only .net core 3.1 SDK is required. Follow this:
7 | 1) install [dot.net core 3.1 SDK](https://dotnet.microsoft.com/download/dotnet-core/3.1)
8 | 2) open a cmd, move to your folder and execute:
9 | ```
10 | > git clone https://github.com/danice/ICEAutomation.git
11 | > cd ICEAutomation
12 | > dotnet build
13 | ```
14 | You will found the compiled files in \ICEAutomation\src\bin\Debug\netcoreapp3.1
15 | Next adjust the ICEAutomation.bat to point to this folder. Then copy the batch file to c:\Windows or some folder in system Path so you can execute the application from any folder.
16 |
17 |
18 | # Instructions
19 |
20 | 1) open a command line and move to the folder where your images are
21 | 2) execute
22 | - "ICEAutomation compose [file1] [file2] [file3...]" to stitch those files
23 | - "ICEAutomation process" to process all *.JPG files in current folder in groups of 3
24 | - "ICEAutomation process [num]" to process all *.JPG files in current folder in groups of [num]
25 | - "ICEAutomation process [num] [ext]" to process all files with extension [ext] in current folder in groups of [num]
26 | - "ICEAutomation process [num] [ext] [folder]" to process all files with extension [ext] in [folder] in groups of [num]
27 | - "ICEAutomation structure [num] [ext] [folder]" process as before but using structure panorama
28 |
29 | Options:
30 | - --motion: to specify Camera motion type. Default: autoDetect. Possible values: autoDetect , planarMotion, planarMotionWithSkew, planarMotionWithPerspective, rotatingMotion]
31 | - --save: saves stich processing file
32 |
33 | Structure panorama options:
34 | - --initial-corner: topLef (default), topRight, bottomLeft, bottomRight
35 | - --rows: Number of rows. If defined the direction will be down (if intial corner is top) or up (if initial corner is bottom)
36 | - --cols: Number of columns. If defined the direction will be right (if intial corner is left) or left (if initial corner is right)
37 | - --order: serpentine, zigzag
38 | - --angular-range: less360, horiz, vert (pending)
39 | - --horizontal-overlap
40 | - --vertical-overlap
41 | - --search-radious (pending)
42 | - --auto-overlap (pending)
43 |
44 | # Warning
45 |
46 | The application uses button labels to automate ICE. Depending of your environment this names can change (for example "Save" button).
47 | You can configure the button labels in your ICE in app.config.
48 |
49 | The processed files will be copied in the last folder used by ICE. So I recommend firt executing manually a stich to select the destination folder.
50 |
51 |
--------------------------------------------------------------------------------
/src/Options.cs:
--------------------------------------------------------------------------------
1 | using CommandLine;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.ComponentModel;
5 |
6 | namespace ImageComposeEditorAutomation
7 | {
8 | public enum CameraMotion
9 | {
10 | [Description("Auto-detect")]
11 | autoDetect,
12 | [Description("Planar motion")]
13 | planarMotion,
14 | [Description("Planar motion with skew")]
15 | planarMotionWithSkew,
16 | [Description("Planar motion with perspective")]
17 | planarMotionWithPerspective,
18 | [Description("Rotating motion")]
19 | rotatingMotion
20 | }
21 |
22 | public enum Corner
23 | {
24 | [Description("Top Left")]
25 | topLeft,
26 | [Description("Top Right")]
27 | topRight,
28 | [Description("Bottom Left")]
29 | bottomLeft,
30 | [Description("Bottom Right")]
31 | bottomRight
32 | }
33 |
34 | public enum Direction
35 | {
36 | [Description("Left")]
37 | left,
38 | [Description("Right")]
39 | right,
40 | [Description("Bottom")]
41 | bottom,
42 | [Description("Top")]
43 | top
44 | }
45 |
46 | public enum ImageOrder
47 | {
48 | [Description("Serpentine")]
49 | serpentine,
50 | [Description("Zigzag")]
51 | zigzag
52 | }
53 |
54 | public enum AngularRange
55 | {
56 | [Description("Less than 360")]
57 | less360,
58 | [Description("360 horiz")]
59 | horiz,
60 | [Description("360 vert")]
61 | vert
62 |
63 | }
64 |
65 | public class BaseOptions
66 | {
67 | [Option('v', "verbose", Required = false, HelpText = "Set output to verbose messages.")]
68 | public bool Verbose { get; set; }
69 |
70 | [Option('s', "save", Required = false, HelpText = "Save project file.")]
71 | public bool? Save { get; set; }
72 |
73 | [Option('m', "motion", Required = false, HelpText = "Set camera motion.")]
74 | public CameraMotion Motion { get; set; }
75 |
76 | }
77 |
78 | [Verb("compose", HelpText = "compose .... Stich file1 fil2,... ")]
79 | public class ComposeOptions : BaseOptions
80 | {
81 | [Value(0)]
82 | public IEnumerable Images { get; set; }
83 |
84 | }
85 |
86 | public class ProcessBaseOptions : BaseOptions
87 | {
88 | [Value(0)]
89 | public int Num { get; set; }
90 |
91 | [Value(1)]
92 | public string Extension { get; set; }
93 | [Value(2)]
94 | public string Folder { get; set; }
95 |
96 | }
97 |
98 | [Verb("process", HelpText = "process . Process all files in in groups of ")]
99 | public class ProcessOptions : ProcessBaseOptions
100 | {
101 |
102 |
103 | }
104 |
105 | [Verb("structure", HelpText = "process . Process all files in in groups of ")]
106 | public class StructurePanoramaOptions : ProcessBaseOptions
107 | {
108 | [Option('i', "initial-corner", Required = false, HelpText = "Initial corner: topLeft (default), topRight, bottomLeft, bottomRight", Default = Corner.topLeft)]
109 | public Corner InitialCorner { get; set; }
110 |
111 | [Option('r', "rows", Required = false, HelpText = "Number of rows. If defined the direction will be down (if intial corner is top) or up (if initial corner is bottom)", Default = null)]
112 | public int? Rows { get; set; }
113 |
114 | [Option('c', "columns", Required = false, HelpText = "Number of columns. If defined the direction will be right (if intial corner is left) or left (if initial corner is right)", Default = null)]
115 | public int? Columns { get; set; }
116 |
117 | [Option('o', "order", Required = false, HelpText = "serpentine, zigzag", Default = ImageOrder.serpentine)]
118 | public ImageOrder Order { get; set; }
119 |
120 | [Option('g', "angular-range", Required = false, HelpText = "Angular range: less360, horiz, vert", Default = AngularRange.less360)]
121 | public AngularRange AngularRange { get; set; }
122 |
123 | [Option('h', "horizontal-overlap", Required = false, HelpText = "Horizontal overlap", Default = null)]
124 | public int? HorizontalOverlap { get; set; }
125 |
126 | [Option('v', "vertical-overlap", Required = false, HelpText = "Vertical overlap.", Default = null)]
127 | public int? VerticalOverlap { get; set; }
128 |
129 | [Option('s', "search-radious", Required = false, HelpText = "Search Radious.", Default = 10)]
130 | public int SearchRadious { get; set; }
131 |
132 | [Option('a', "auto-overlap", Required = false, HelpText = "Set camera motion.", Default = true)]
133 | public bool AutoOverlap { get; set; }
134 | }
135 | }
--------------------------------------------------------------------------------
/src/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using System.Linq;
6 | using System.Threading;
7 | using CommandLine;
8 | using FlaUI.Core.Definitions;
9 | using FlaUI.UIA3;
10 |
11 | namespace ImageComposeEditorAutomation
12 | {
13 | class Program
14 | {
15 |
16 | static void Main(string[] args)
17 | {
18 | var parser = new Parser(config => config.HelpWriter = Console.Out);
19 | var options = parser.ParseArguments(args)
20 | .WithParsed(options => Compose(options))
21 | .WithParsed(options => Process(options))
22 | .WithParsed(options => Process(options))
23 | .WithNotParsed(errors => { }); // errors is a sequence of type IEnumerable
24 | Console.ReadLine();
25 | }
26 |
27 | private static void Compose(ComposeOptions options)
28 | {
29 | Console.WriteLine("composing...");
30 | var composeApp = new ComposeAppService();
31 | var saveProject = options.Save.HasValue ? options.Save.Value : false;
32 | composeApp.Compose(options.Images.ToArray(), options, m => Console.WriteLine(m), i => drawTextProgressBar(1, 100), saveProject);
33 |
34 | }
35 |
36 | private static void Process(ProcessBaseOptions options)
37 | {
38 | var composeApp = new ComposeAppService();
39 |
40 | Console.WriteLine("process...");
41 | if (string.IsNullOrEmpty(options.Extension))
42 | options.Extension = "*.JPG";
43 |
44 | if (!string.IsNullOrEmpty(options.Folder))
45 | Directory.SetCurrentDirectory(options.Folder);
46 | var files = GroupFiles(options.Extension, options.Num, ignoreStichInName: true);
47 | int total = files.Count;
48 | int count = 0;
49 | foreach (var item in files)
50 | {
51 | count++;
52 | Console.WriteLine(string.Format("composing {0} of {1}....", count, total));
53 | var saveProject = options.Save.HasValue ? options.Save.Value : false;
54 | composeApp.Compose(item, options, m => Console.WriteLine(m), i => drawTextProgressBar(i, 100), saveProject: saveProject);
55 | }
56 | Console.WriteLine("Finished.");
57 | }
58 |
59 | private static List GroupFiles(string extension, int groupNum, bool ignoreStichInName = false)
60 | {
61 | string[] filePaths = Directory.GetFiles(Directory.GetCurrentDirectory(), extension, SearchOption.TopDirectoryOnly);
62 | string[] stichFilePaths = null;
63 |
64 | if (ignoreStichInName)
65 | {
66 | stichFilePaths = filePaths.Where(f => IsStitchResult(Path.GetFileName(f))).Select(s => Path.GetFileName(s).ToLower()).ToArray();
67 | filePaths = filePaths.Where(f => !IsStitchResult(Path.GetFileName(f))).ToArray();
68 | }
69 |
70 | var grouped = filePaths.Select((value, index) => new { value, index })
71 | .GroupBy(x => x.index / groupNum, x => Path.GetFileName(x.value)).Select(g => g.ToArray()).ToList();
72 |
73 | if (ignoreStichInName)
74 | {
75 | grouped = grouped.Where(g => !FileAlreadyInStich(g[0], stichFilePaths)).ToList();
76 | }
77 |
78 | return grouped;
79 | }
80 |
81 | private static bool FileAlreadyInStich(string fileName, string[] stichFilePaths)
82 | {
83 |
84 | var stichName = Path.GetFileNameWithoutExtension(fileName) + "_stitch" + Path.GetExtension(fileName);
85 | return stichFilePaths.Contains(stichName.ToLower());
86 | }
87 |
88 | private static bool IsStitchResult(string fileName)
89 | {
90 | return fileName.Contains("_stitch");
91 | }
92 |
93 | private static void drawTextProgressBar(int progress, int total)
94 | {
95 | ////draw empty progress bar
96 | //Console.CursorLeft = 0;
97 | //Console.Write("["); //start
98 | //Console.CursorLeft = 32;
99 | //Console.Write("]"); //end
100 | //Console.CursorLeft = 1;
101 | //float onechunk = 30.0f / total;
102 |
103 | ////draw filled part
104 | //int position = 1;
105 | //for (int i = 0; i < onechunk * progress; i++)
106 | //{
107 | // Console.BackgroundColor = ConsoleColor.Gray;
108 | // Console.CursorLeft = position++;
109 | // Console.Write(" ");
110 | //}
111 |
112 | ////draw unfilled part
113 | //for (int i = position; i <= 31; i++)
114 | //{
115 | // Console.BackgroundColor = ConsoleColor.Green;
116 | // Console.CursorLeft = position++;
117 | // Console.Write(" ");
118 | //}
119 |
120 | ////draw totals
121 | //Console.CursorLeft = 35;
122 | //Console.BackgroundColor = ConsoleColor.Black;
123 | try
124 | {
125 | Console.CursorLeft = 15;
126 | }
127 | catch (System.Exception)
128 | {
129 |
130 | }
131 | Console.Write(progress.ToString() + " of " + total.ToString() + " "); //blanks at the end remove any excess
132 |
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/ComposeAppService.cs:
--------------------------------------------------------------------------------
1 | using FlaUI.Core;
2 | using FlaUI.Core.AutomationElements;
3 | using FlaUI.Core.AutomationElements.Infrastructure;
4 | using FlaUI.Core.Conditions;
5 | using FlaUI.Core.Definitions;
6 | using FlaUI.Core.Identifiers;
7 | using FlaUI.UIA3;
8 | using System;
9 | using System.Collections.Generic;
10 | using System.ComponentModel;
11 | using System.Configuration;
12 | using System.Diagnostics;
13 | using System.IO;
14 | using System.Linq;
15 | using System.Text;
16 | using System.Threading;
17 | using System.Threading.Tasks;
18 |
19 | namespace ImageComposeEditorAutomation
20 | {
21 |
22 | public class ComposeAppService
23 | {
24 | Action onEvent;
25 | Action onProgress;
26 | Application app;
27 |
28 | public void Compose(string[] images, BaseOptions options, Action onEvent = null, Action onProgress = null, bool saveProject = false)
29 | {
30 | var cameraMotion = options.Motion;
31 | this.onEvent = onEvent;
32 | this.onProgress = onProgress;
33 | var appStr = ConfigurationManager.AppSettings["ICE-app-path"];
34 | var appName = Path.GetFileName(appStr);
35 | var exportBtnLabel = ConfigurationManager.AppSettings["Export-btn-label"];
36 | var exportToDiskBtnLabel = ConfigurationManager.AppSettings["ExportToDisk-btn-label"];
37 | var cameraMotionLabel = ConfigurationManager.AppSettings["CameraMotion-btn-label"];
38 | var exportPanoramaBtnLabel = ConfigurationManager.AppSettings["ExportPanorama-btn-label"];
39 | var saveBtnLabel = ConfigurationManager.AppSettings["Save-btn-label"];
40 | var saveProjectLabel = ConfigurationManager.AppSettings["Save-project-label"];
41 | int saveWait = int.Parse(ConfigurationManager.AppSettings["Save-wait"]);
42 |
43 | var imgStr = string.Join(" ", images);
44 | var processStartInfo = new ProcessStartInfo(fileName: appStr, arguments: imgStr);
45 | this.app = FlaUI.Core.Application.Launch(processStartInfo);
46 |
47 |
48 | using (var automation = new UIA3Automation())
49 | {
50 | string title = null;
51 | Window window = null;
52 | do
53 | {
54 | try
55 | {
56 | app = FlaUI.Core.Application.Attach(appName);
57 | window = app.GetMainWindow(automation);
58 | title = window.Title;
59 | OnEvent("Opened :" + title);
60 | }
61 | catch (Exception)
62 | {
63 | title = null;
64 | }
65 |
66 | } while (string.IsNullOrWhiteSpace(title));
67 |
68 | OnEvent("files :" + imgStr);
69 | if (options is StructurePanoramaOptions)
70 | SetStructurePanorama(automation);
71 |
72 | SetCameraMotion(automation, options.Motion);
73 |
74 | if (options is StructurePanoramaOptions)
75 | SetStructurePanoramaOptions(automation, (StructurePanoramaOptions)options);
76 |
77 | try
78 | {
79 | AutomationElement button1 = null;
80 | do
81 | {
82 | button1 = window.FindFirstDescendant(cf => cf.ByText(exportBtnLabel));
83 | if (button1 == null)
84 | {
85 | OnEvent(".");
86 | }
87 | } while (button1 == null);
88 |
89 | if (button1.ControlType != ControlType.Button)
90 | button1 = button1.AsButton().Parent;
91 |
92 | button1?.AsButton().Invoke();
93 | bool finished = false;
94 | OnEvent("composing.");
95 | do
96 | {
97 | var window2 = app.GetMainWindow(automation);
98 | var button2 = window.FindFirstDescendant(cf => cf.ByText(exportToDiskBtnLabel));
99 |
100 | title = window2.Title;
101 | finished = button2 != null && title.StartsWith("U");
102 | int percent = 0;
103 | if (!finished)
104 | {
105 | var percentStr = title.Substring(0, 2);
106 | var numStr = percentStr[1] == '%' ? percentStr.Substring(0, 1) : percentStr;
107 | if (int.TryParse(numStr, out percent))
108 | onProgress?.Invoke(percent);
109 | }
110 | } while (!finished);
111 |
112 | }
113 | catch (Exception ex)
114 | {
115 | OnEvent(ex.Message);
116 | }
117 |
118 | try
119 | {
120 | var button2 = window.FindFirstDescendant(cf => cf.ByText(exportToDiskBtnLabel));
121 | if (button2 != null && button2.ControlType != ControlType.Button)
122 | button2 = button2.AsButton().Parent;
123 |
124 | button2?.AsButton().Invoke();
125 | OnEvent("exporting to disk...");
126 | }
127 | catch (Exception ex)
128 | {
129 | OnEvent(ex.Message);
130 | }
131 | try
132 | {
133 | Thread.Sleep(1000);
134 | var saveDlg = window.ModalWindows.Length == 1
135 | ? window.ModalWindows[0]
136 | : window.ModalWindows.FirstOrDefault(w => w.Name == exportPanoramaBtnLabel);
137 | var buttonSave = saveDlg.FindFirstDescendant(cf => cf.ByText(saveBtnLabel)).AsButton();
138 | if (buttonSave == null) {
139 | OnEvent("Save button not found: "+saveBtnLabel);
140 | } else
141 | buttonSave?.Invoke();
142 |
143 | Thread.Sleep(saveWait);
144 |
145 | if (saveProject)
146 | {
147 | window.Close();
148 | var onCloseDlg = window.ModalWindows[0];
149 | var buttonOnCloseSave = onCloseDlg.FindFirstDescendant(cf => cf.ByText(saveBtnLabel)).AsButton();
150 | buttonOnCloseSave?.Invoke();
151 |
152 | var saveProjectDlg = window.ModalWindows[0];
153 |
154 | var projectName = saveProjectDlg.FindFirstDescendant(cf => cf.ByControlType(ControlType.ComboBox)).AsComboBox();
155 | projectName.EditableText = Path.GetFileNameWithoutExtension(images[0]);
156 | var buttonSaveProjectSave = saveProjectDlg.FindFirstDescendant(cf => cf.ByText(saveBtnLabel)).AsButton();
157 | buttonSaveProjectSave?.Invoke();
158 |
159 |
160 | //var buttonSaveProj = window.FindFirstDescendant(cf => cf.ByText(saveProjectLabel));
161 | //if (buttonSaveProj != null && buttonSaveProj.ControlType != ControlType.Button)
162 | // buttonSaveProj = buttonSaveProj.AsButton().Parent;
163 | //var saveProj = buttonSaveProj?.AsButton();
164 | //if (saveProj.IsEnabled)
165 | // buttonSaveProj?.AsButton().Invoke();
166 | OnEvent("saving project...");
167 | }
168 |
169 | }
170 | catch (Exception ex)
171 | {
172 | OnEvent(ex.Message);
173 | }
174 |
175 | }
176 | app.Kill();
177 | app = null;
178 | }
179 |
180 | void SetCameraMotion(UIA3Automation automation, CameraMotion cameraMotion)
181 | {
182 | if (cameraMotion == CameraMotion.autoDetect)
183 | return;
184 |
185 | var window2 = app.GetMainWindow(automation);
186 | var comboBoxes = window2.FindAllDescendants(cf => cf.ByControlType(ControlType.ComboBox)).Select(x => x.AsComboBox());
187 | var cameraActionSelect = comboBoxes.FirstOrDefault(x => x.Items.Length > 0 && x.Items[0].Name == "Auto-detect");
188 | if (cameraActionSelect != null)
189 | {
190 | var descAttr = GetAttribute(cameraMotion);
191 | cameraActionSelect.Select(descAttr.Description);
192 | }
193 |
194 |
195 | }
196 |
197 | void SetStructurePanorama(UIA3Automation automation)
198 | {
199 | var window2 = app.GetMainWindow(automation);
200 | AutomationElement button2 = null;
201 |
202 | do
203 | {
204 | button2 = window2.FindFirstDescendant(cf => cf.ByText("Structured panorama"));
205 | } while (button2 == null);
206 | if (button2 != null && button2.ControlType != ControlType.Button)
207 | button2 = button2.AsButton().Parent;
208 | var list = button2.AsListBox().Select(1);
209 |
210 |
211 | //button2.Click();
212 | }
213 |
214 |
215 | void SetStructurePanoramaOptions(UIA3Automation automation, StructurePanoramaOptions options)
216 | {
217 | var window = app.GetMainWindow(automation);
218 |
219 | try
220 | {
221 | // Initial corner and direction - top left
222 | var pos1 = GetPosition1(options.InitialCorner);
223 | var pos2 = GetPosition2(options.InitialCorner, options.Rows);
224 | var layout = window.FindFirstDescendant(cf => cf.ByName("Layout"));
225 | var b1 = layout.FindChildAt(pos1);
226 | //var str = layout.FindAllDescendants().Select(d => string.Format("id: {0} {1} [{2}]", d.AutomationId, d.HelpText, d.Name)).ToList();
227 | //Console.WriteLine(string.Join(",", str));
228 | b1.AsRadioButton().IsChecked = true;
229 |
230 | var b2 = layout.FindChildAt(pos2);
231 | b2.AsRadioButton().IsChecked = true;
232 | }
233 | catch (Exception ex)
234 | {
235 | Console.WriteLine(ex.Message);
236 | }
237 |
238 | try
239 | {
240 | // Number of columns - 3x3
241 | var numOfRowsOrColumns = options.Rows.HasValue ? options.Rows.Value : options.Columns.Value;
242 | var b3 = options.Rows.HasValue
243 | ? window.FindFirstDescendant(cf => cf.ByAutomationId("rowCountTextBox"))
244 | : window.FindFirstDescendant(cf => cf.ByAutomationId("primaryDirectionImageCountTextBox"));
245 | b3.AsTextBox().Enter(numOfRowsOrColumns.ToString());
246 |
247 | //Serpentine
248 | var radiobutton = window.FindFirstDescendant(cf => cf.ByAutomationId("serpentineRadioButton")).AsRadioButton();
249 | radiobutton.IsChecked = true;
250 |
251 | //Angular range
252 | if (!options.AngularRange.Equals("less360"))
253 | {
254 | var radioButton = window.FindAllDescendants(cf => cf.ByControlType(ControlType.RadioButton).And(cf.ByName( GetAngularName(options.AngularRange.ToString()) ))).FirstOrDefault().AsRadioButton();
255 | radioButton.IsChecked = true;
256 | }
257 |
258 | }
259 | catch (Exception ex)
260 | {
261 | Console.WriteLine(ex.Message);
262 | }
263 |
264 | try
265 | {
266 | //Overlap percentage
267 | var overlap = window.FindFirstDescendant(cf => cf.ByName("Overlap"));
268 | // var str2 = overlap.FindAllDescendants().Select(d => string.Format("<{2}> id: {0} {1} [{3}]", d.AutomationId, d.HelpText, d.ControlType.ToString(), d.Name)).ToList();
269 | // Console.WriteLine(string.Join("\n\r", str2));
270 |
271 | var overlapH = options.HorizontalOverlap ?? 10;
272 | var overlapV = options.VerticalOverlap ?? 10;
273 | window.FindFirstDescendant(cf => cf.ByAutomationId("horizontalOverlapTextBox")).AsTextBox().Enter(overlapH.ToString());
274 | window.FindFirstDescendant(cf => cf.ByAutomationId("verticalOverlapTextBox")).AsTextBox().Enter(overlapV.ToString());
275 |
276 | }
277 | catch (Exception ex)
278 | {
279 | Console.WriteLine(ex.Message);
280 | }
281 |
282 | }
283 | int GetPosition1(Corner corner)
284 | {
285 | if (corner == Corner.topLeft)
286 | return 5;
287 | if (corner == Corner.topRight)
288 | return 6;
289 | if (corner == Corner.bottomLeft)
290 | return 7;
291 | return 8; //"Start in bottom right corner"
292 | }
293 |
294 | int GetPosition2(Corner corner, int? rows)
295 | {
296 | if (corner == Corner.topLeft)
297 | return rows.HasValue
298 | ? 11 //Start moving down
299 | : 9; //Start moving right
300 | if (corner == Corner.topRight)
301 | return rows.HasValue
302 | ? 12 //Start moving down
303 | : 10; //Start moving left
304 | if (corner == Corner.bottomLeft)
305 | return rows.HasValue
306 | ? 13 //Start moving up
307 | : 15; //Start moving right
308 | return rows.HasValue
309 | ? 14 //Start moving up
310 | : 16; //Start moving left
311 | }
312 |
313 | string GetAngularName(String angular)
314 | {
315 | if (angular.Equals("horiz"))
316 | return "360° horizontally";
317 | if (angular.Equals("vert"))
318 | return "360° vertically";
319 | return "Less than 360°";
320 |
321 | }
322 |
323 | public static T GetAttribute(Enum enumeration) where T : Attribute
324 | {
325 | var type = enumeration.GetType();
326 |
327 | var memberInfo = type.GetMember(enumeration.ToString());
328 |
329 | if (!memberInfo.Any())
330 | throw new ArgumentException($"No public members for the argument '{enumeration}'.");
331 |
332 | var attributes = memberInfo[0].GetCustomAttributes(typeof(T), false);
333 |
334 | if (attributes == null || attributes.Length != 1)
335 | throw new ArgumentException($"Can't find an attribute matching '{typeof(T).Name}' for the argument '{enumeration}'");
336 |
337 | return attributes.Single() as T;
338 | }
339 |
340 | private void OnEvent(string message)
341 | {
342 | this.onEvent?.Invoke(message);
343 | }
344 | }
345 | }
346 |
--------------------------------------------------------------------------------