├── 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 | --------------------------------------------------------------------------------