From c2f32096ed43edf641b2239d498d77654634fe9b Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii <144726123+ns-58@users.noreply.github.com> Date: Fri, 26 Sep 2025 18:41:31 +0300 Subject: [PATCH 01/16] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a0f426..5341996 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# conv \ No newline at end of file +Todo: add license From bebe3236c2d3298afcb65220f6c677450d2f0ceb Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii Date: Fri, 26 Sep 2025 21:03:03 +0300 Subject: [PATCH 02/16] feat: implement seq. convolution --- Lib/Conv.fs | 33 ++++++++++++++++++++++++ Lib/Lib.fsproj | 17 ++++++++++++ Program.fs | 21 +++++++++++++++ Tests/PropBased.fs | 64 ++++++++++++++++++++++++++++++++++++++++++++++ Tests/Tests.fsproj | 22 ++++++++++++++++ root.fsproj | 23 +++++++++++++++++ 6 files changed, 180 insertions(+) create mode 100644 Lib/Conv.fs create mode 100644 Lib/Lib.fsproj create mode 100644 Program.fs create mode 100644 Tests/PropBased.fs create mode 100644 Tests/Tests.fsproj create mode 100644 root.fsproj diff --git a/Lib/Conv.fs b/Lib/Conv.fs new file mode 100644 index 0000000..8600658 --- /dev/null +++ b/Lib/Conv.fs @@ -0,0 +1,33 @@ +open OpenCvSharp + +let (@@) = (<|) +let (>>) f a = (fun () -> a) f + +let curry3 f a1 a2 a3 = f (a1, a2, a3) +let (~~~) = curry3 + +let ind (m: Mat) = m.GetGenericIndexer() +let ind_fl (m: Mat) = m.GetGenericIndexer() + +let conv src dest kernel = + + let src_indr, dest_indr, kernel_indr = ind src, ind dest, ind_fl kernel + + let calc1 x y = + byte + @@ min (float32 255) + @@ max (float32 0) + @@ Seq.fold (+) (float32 0) + @@ seq { + for ky in 0 .. kernel.Height - 1 do + for kx in 0 .. kernel.Width - 1 -> + let m, n = (kernel.Height / 2) - ky, (kernel.Width / 2) - kx + let (-) x y upper = max 0 @@ min (upper - 1) @@ (-) x y + + (float32 @@ src_indr.get_Item ((-) y m src.Height, (-) x n src.Width)) + * (kernel_indr.get_Item (ky, kx)) + } + + for y in 0 .. src.Height - 1 do + for x in 0 .. src.Width - 1 do + ~~~ dest_indr.set_Item y x @@ calc1 x y \ No newline at end of file diff --git a/Lib/Lib.fsproj b/Lib/Lib.fsproj new file mode 100644 index 0000000..cc844af --- /dev/null +++ b/Lib/Lib.fsproj @@ -0,0 +1,17 @@ + + + + Exe + net6.0 + + + + + + + + + + + + \ No newline at end of file diff --git a/Program.fs b/Program.fs new file mode 100644 index 0000000..d5315f3 --- /dev/null +++ b/Program.fs @@ -0,0 +1,21 @@ +open System.IO +open OpenCvSharp +open Conv + +[] +let main = + function + | [| imagePath |] -> + use src = new Mat(imagePath, ImreadModes.Grayscale) + use dest = new Mat(src.Height, src.Width, src.Type()) + use kernel = new Mat([| 3; 3 |], MatType.CV_32FC1, Scalar 0.) + ~~~ (ind_fl kernel).set_Item 1 1 @@ float32 1 + + + conv src dest kernel + + >> Cv2.ImWrite(sprintf "%s-%s" "id" @@ Path.GetFileName imagePath, dest) + |> ignore + + 0 + | _ -> failwith "" diff --git a/Tests/PropBased.fs b/Tests/PropBased.fs new file mode 100644 index 0000000..69ee138 --- /dev/null +++ b/Tests/PropBased.fs @@ -0,0 +1,64 @@ +open OpenCvSharp +open FsCheck.FSharp +open Conv + +let (@@) = (<|) + +let eq_to_opencv (m: Mat) (kernel: Mat) = + use dest1 = new Mat(m.Height, m.Width, m.Type()) + use dest2 = new Mat(m.Height, m.Width, m.Type()) + + conv m dest1 kernel + Cv2.Filter2D(m, dest2, -1, kernel, borderType = BorderTypes.Replicate) + + let indr1, indr2 = ind dest1, ind dest2 + + + Seq.fold (&&) true + @@ seq { + for y in 0 .. dest1.Height - 1 do + for x in 0 .. dest1.Width - 1 -> + (int @@ indr1.get_Item (y, x)) >= (int @@ indr2.get_Item (y, x)) - 1 + && (int @@ indr1.get_Item (y, x)) <= (int @@ indr2.get_Item (y, x)) + 1 + } + + +let byte_mat_gen = + Gen.sized (fun s -> + let [| rows; colomns |] = Gen.sampleWithSize s 2 @@ Gen.choose (10, s + 1) + + Gen.map (fun (data: byte[,]) -> Mat.FromArray(data)) + @@ Gen.array2DOfDim rows colomns + @@ Gen.map byte + @@ Gen.choose (0, 255)) + +let gen_float = + Gen.map (fun (m, n) -> float32 m / float32 n) + @@ Gen.two + @@ Gen.choose (System.Int32.MinValue / 3, System.Int32.MaxValue) + + +let float_mat_gen = + Gen.sized (fun s -> + let s = int @@ sqrt @@ float s + let [| rows; colomns |] = Gen.sampleWithSize s 2 @@ Gen.choose (1, s + 1) + + Gen.map (fun (data: float32[,]) -> Mat.FromArray(data)) + @@ Gen.array2DOfDim rows colomns gen_float) + +type MyGenerators = + static member ByteMap() = + { new FsCheck.Arbitrary>() with + override _.Generator = byte_mat_gen + override _.Shrinker _ = Seq.empty } + + static member FloatMap() = + { new FsCheck.Arbitrary>() with + override _.Generator = float_mat_gen + override _.Shrinker _ = Seq.empty } + + +FsCheck.Check.One( + FsCheck.Config.Quick.WithArbitrary([ typeof ]), + fun (m: Mat) (k: Mat) -> eq_to_opencv m k +) diff --git a/Tests/Tests.fsproj b/Tests/Tests.fsproj new file mode 100644 index 0000000..8b7bfbf --- /dev/null +++ b/Tests/Tests.fsproj @@ -0,0 +1,22 @@ + + + + Exe + net6.0 + + + + + + + + + + + + + + + + + diff --git a/root.fsproj b/root.fsproj new file mode 100644 index 0000000..41372a4 --- /dev/null +++ b/root.fsproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + + + + + + + + + + + + + + + + + + From 3290e48b11a9c995ef3f107c0cf51849f07c7340 Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii <144726123+ns-58@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:43:46 +0300 Subject: [PATCH 03/16] Ci (#1) --- .github/workflows/ci.yml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7326964 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: Build master + +on: + pull_request: + branches: + - 'master' + push: + branches: + - "master" + - "ci" + +env: + OPAMROOT: /home/opam/.opam + OPAMYES: true + OPAMCONFIRMLEVEL: unsafe-yes + +jobs: + build: + runs-on: ubuntu-latest + container: + image: ghcr.io/shimat/opencvsharp/ubuntu22-dotnet6sdk-opencv4.7.0:20230114 + + steps: + + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: look_around + run: ls + + - name: build + run: dotnet build + + - name: run_tests + run: | + cd ./Tests + dotnet run From 5f7089d06496b06fd399f89e23c9c17d63f10b02 Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii Date: Sun, 28 Sep 2025 19:17:38 +0300 Subject: [PATCH 04/16] feat: implement parallel conv, set up benchmark --- Bench/Bench.fsproj | 23 +++++++++++ Bench/Partition.fs | 100 +++++++++++++++++++++++++++++++++++++++++++++ Lib/Conv.fs | 74 ++++++++++++++++++++++++++++++--- Program.fs | 10 ++--- Tests/PropBased.fs | 37 +++++++++++++++-- 5 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 Bench/Bench.fsproj create mode 100644 Bench/Partition.fs diff --git a/Bench/Bench.fsproj b/Bench/Bench.fsproj new file mode 100644 index 0000000..90ec958 --- /dev/null +++ b/Bench/Bench.fsproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + + + + + + + + + + + + + + + + + + diff --git a/Bench/Partition.fs b/Bench/Partition.fs new file mode 100644 index 0000000..4aa2121 --- /dev/null +++ b/Bench/Partition.fs @@ -0,0 +1,100 @@ +open Conv +open System.IO +open BenchmarkDotNet.Attributes +open BenchmarkDotNet.Running +open OpenCvSharp +open System.Runtime.CompilerServices +open System.Runtime.InteropServices + +let kernel = new Mat([| 3; 3 |], MatType.CV_32FC1, Scalar -1.) +~~~ (ind_fl kernel).set_Item 1 1 @@ float32 8 + + +[] +type PartitionConvComparision() = + + [] + member val file = "" with get, set + + member self.getPath([] path: string) = + Path.GetDirectoryName path + + member self.Src = + let path = Path.Combine [| self.getPath (); self.file |] + let src = new Mat(path, ImreadModes.Grayscale) + src + + member self.Dest = new Mat(self.Src.Height, self.Src.Width, self.Src.Type()) + + + [] + member self.Seql() = conv self.Src self.Dest kernel Seql + + [] + member self.ByPixel() = conv self.Src self.Dest kernel ByPixel + + [] + member self.ByRow() = conv self.Src self.Dest kernel ByRow + + [] + member self.ByColumn() = conv self.Src self.Dest kernel ByColumn + + [] + member self.ByRect1x1() = + conv self.Src self.Dest kernel @@ ByRect(Limited 1, Limited 1) + + [] + member self.ByRect1ker() = + conv self.Src self.Dest kernel + @@ ByRect(Limited kernel.Width, Limited kernel.Height) + + [] + member self.ByRect5ker() = + conv self.Src self.Dest kernel + @@ ByRect(Limited @@ ( * ) 5 kernel.Width, Limited @@ ( * ) 5 kernel.Height) + + [] + member self.ByRect10ker() = + conv self.Src self.Dest kernel + @@ ByRect(Limited @@ ( * ) 10 kernel.Width, Limited @@ ( * ) 10 kernel.Height) + + [] + member self.ByRect15ker() = + conv self.Src self.Dest kernel + @@ ByRect(Limited @@ ( * ) 15 kernel.Width, Limited @@ ( * ) 15 kernel.Height) + + + [] + member self.By1KerHeightRows() = + conv self.Src self.Dest kernel @@ ByRect(UnLimited, Limited kernel.Height) + + [] + member self.By3KerHeightRows() = + conv self.Src self.Dest kernel + @@ ByRect(UnLimited, Limited @@ ( * ) 3 kernel.Height) + + [] + member self.By10KerHeightRows() = + conv self.Src self.Dest kernel + @@ ByRect(UnLimited, Limited @@ ( * ) 10 kernel.Height) + + [] + member self.By1KerWidthColomns() = + conv self.Src self.Dest kernel @@ ByRect(Limited kernel.Width, UnLimited) + + [] + member self.By3KerWidthColomns() = + conv self.Src self.Dest kernel + @@ ByRect(Limited @@ ( * ) 3 kernel.Width, UnLimited) + + [] + member self.By10KerWidthColomns() = + conv self.Src self.Dest kernel + @@ ByRect(Limited @@ ( * ) 10 kernel.Width, UnLimited) + + + +[] +let main _ = + BenchmarkRunner.Run typeof |> ignore + 0 diff --git a/Lib/Conv.fs b/Lib/Conv.fs index 8600658..8275635 100644 --- a/Lib/Conv.fs +++ b/Lib/Conv.fs @@ -1,7 +1,7 @@ open OpenCvSharp +open System.Threading.Tasks let (@@) = (<|) -let (>>) f a = (fun () -> a) f let curry3 f a1 a2 a3 = f (a1, a2, a3) let (~~~) = curry3 @@ -9,7 +9,18 @@ let (~~~) = curry3 let ind (m: Mat) = m.GetGenericIndexer() let ind_fl (m: Mat) = m.GetGenericIndexer() -let conv src dest kernel = +type rectSideSize = + | UnLimited + | Limited of int + +type paral_method = + | Seql + | ByPixel + | ByRow + | ByColumn + | ByRect of rectSideSize * rectSideSize + +let conv src dest kernel paral_method = let src_indr, dest_indr, kernel_indr = ind src, ind dest, ind_fl kernel @@ -28,6 +39,59 @@ let conv src dest kernel = * (kernel_indr.get_Item (ky, kx)) } - for y in 0 .. src.Height - 1 do - for x in 0 .. src.Width - 1 do - ~~~ dest_indr.set_Item y x @@ calc1 x y \ No newline at end of file + match paral_method with + | Seql -> + for y in 0 .. src.Height - 1 do + for x in 0 .. src.Width - 1 do + ~~~ dest_indr.set_Item y x @@ calc1 x y + | ByPixel -> + for y in 0 .. src.Height - 1 do + Parallel.For( + 0, + src.Width, + fun x -> + + ~~~ dest_indr.set_Item y x @@ calc1 x y + + ) + |> ignore + | ByRow -> + Parallel.For( + 0, + src.Height, + fun y -> + for x in 0 .. src.Width - 1 do + ~~~ dest_indr.set_Item y x @@ calc1 x y + ) + |> ignore + | ByColumn -> + Parallel.For( + 0, + src.Width, + fun x -> + for y in 0 .. src.Height - 1 do + ~~~ dest_indr.set_Item y x @@ calc1 x y + ) + |> ignore + | ByRect(xS, yS) -> + + let xS, yS = + (fun f -> f src.Height xS, f src.Width yS) + @@ fun def -> + function + | Limited n when n > 0 && n < def -> n + | UnLimited -> def + | Limited n when n > 0 -> def + | _ -> failwith "side size shoudn't be less then 1 pixel" + + Parallel.ForEach( + seq { + for starty in 0..yS .. src.Height - 1 do + for startx in 0..xS .. src.Width - 1 -> (starty, startx) + }, + fun (starty, startx) -> + for y in starty .. min (src.Height - 1) (starty + yS) do + for x in startx .. min (src.Width - 1) (startx + xS) do + ~~~ dest_indr.set_Item y x @@ calc1 x y + ) + |> ignore diff --git a/Program.fs b/Program.fs index d5315f3..fd92b89 100644 --- a/Program.fs +++ b/Program.fs @@ -2,18 +2,18 @@ open OpenCvSharp open Conv +let (>>) f a = (fun () -> a) f + [] let main = function | [| imagePath |] -> use src = new Mat(imagePath, ImreadModes.Grayscale) use dest = new Mat(src.Height, src.Width, src.Type()) - use kernel = new Mat([| 3; 3 |], MatType.CV_32FC1, Scalar 0.) - ~~~ (ind_fl kernel).set_Item 1 1 @@ float32 1 - - - conv src dest kernel + use kernel = new Mat([| 3; 3 |], MatType.CV_32FC1, Scalar -1.) + ~~~ (ind_fl kernel).set_Item 1 1 @@ float32 8 + conv src dest kernel ByRow >> Cv2.ImWrite(sprintf "%s-%s" "id" @@ Path.GetFileName imagePath, dest) |> ignore diff --git a/Tests/PropBased.fs b/Tests/PropBased.fs index 69ee138..0378522 100644 --- a/Tests/PropBased.fs +++ b/Tests/PropBased.fs @@ -4,15 +4,25 @@ open Conv let (@@) = (<|) -let eq_to_opencv (m: Mat) (kernel: Mat) = +// let print_byte_matrix m = +// let indr = ind m +// for y in 0 .. m.Height - 1 do +// for x in 0 .. m.Width do +// printf "%d " @@ indr.get_Item(y,x) +// printfn "" +// printfn "" + + +let eq_to_opencv (m: Mat) (kernel: Mat) mode = use dest1 = new Mat(m.Height, m.Width, m.Type()) use dest2 = new Mat(m.Height, m.Width, m.Type()) - conv m dest1 kernel + conv m dest1 kernel mode Cv2.Filter2D(m, dest2, -1, kernel, borderType = BorderTypes.Replicate) let indr1, indr2 = ind dest1, ind dest2 - + // print_byte_matrix dest1 + // print_byte_matrix dest2 Seq.fold (&&) true @@ seq { @@ -57,8 +67,27 @@ type MyGenerators = override _.Generator = float_mat_gen override _.Shrinker _ = Seq.empty } +for mode in [ Seql; ByPixel; ByRow; ByColumn; ByRect(UnLimited, UnLimited) ] do + FsCheck.Check.One( + FsCheck.Config.Quick.WithArbitrary([ typeof ]), + fun (m: Mat) (k: Mat) -> eq_to_opencv m k mode + ) + +FsCheck.Check.One( + FsCheck.Config.Quick.WithArbitrary([ typeof ]), + fun (m: Mat) (k: Mat) (xS: uint16) -> + eq_to_opencv m k @@ ByRect(Limited @@ max 1 @@ int xS, UnLimited) +) + +FsCheck.Check.One( + FsCheck.Config.Quick.WithArbitrary([ typeof ]), + fun (m: Mat) (k: Mat) (xS: uint16) -> + eq_to_opencv m k @@ ByRect(Limited @@ max 1 @@ int xS, UnLimited) +) FsCheck.Check.One( FsCheck.Config.Quick.WithArbitrary([ typeof ]), - fun (m: Mat) (k: Mat) -> eq_to_opencv m k + fun (m: Mat) (k: Mat) (xS: uint16) (yS: uint16) -> + eq_to_opencv m k + @@ ByRect(Limited @@ max 1 @@ int xS, Limited @@ max 1 @@ int yS) ) From 0305a0921b6c3503801cb64769dc063917b0e2c7 Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii Date: Sat, 4 Oct 2025 23:06:27 +0300 Subject: [PATCH 05/16] feat: implement read-conv-write pipeline --- Program.fs | 106 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 93 insertions(+), 13 deletions(-) diff --git a/Program.fs b/Program.fs index fd92b89..185a0e7 100644 --- a/Program.fs +++ b/Program.fs @@ -1,21 +1,101 @@ open System.IO open OpenCvSharp open Conv +open System.Threading.Tasks.Dataflow let (>>) f a = (fun () -> a) f -[] -let main = - function - | [| imagePath |] -> - use src = new Mat(imagePath, ImreadModes.Grayscale) - use dest = new Mat(src.Height, src.Width, src.Type()) - use kernel = new Mat([| 3; 3 |], MatType.CV_32FC1, Scalar -1.) - ~~~ (ind_fl kernel).set_Item 1 1 @@ float32 8 - - conv src dest kernel ByRow - >> Cv2.ImWrite(sprintf "%s-%s" "id" @@ Path.GetFileName imagePath, dest) +open System + +type cnfg = + { mutable srcdir: string + mutable outdir: string + mutable prefix: string + mutable max_readers: int option + mutable max_writers: int option + mutable max_conv_performers: int option + mutable max_queue_size: int option + mutable conv_mode: paral_method } + +let full_cycle_paral_conv cnfg kernel = + + let load name = + let src = new Mat(name, ImreadModes.Grayscale) + (src, name) + + let applyFilter (m: Mat, name) = + let dest = new Mat(m.Height, m.Width, m.Type()) + conv m dest kernel cnfg.conv_mode + m.Dispose() + (dest, name) + + let save (m: Mat, name: string) = + Cv2.ImWrite(sprintf "./%s/%s-%s" cnfg.outdir cnfg.prefix @@ Path.GetFileName name, m) |> ignore - 0 - | _ -> failwith "" + m.Dispose() + + let opts bounded_queue p_deg = + ExecutionDataflowBlockOptions( + MaxDegreeOfParallelism = Option.defaultValue DataflowBlockOptions.Unbounded p_deg, + BoundedCapacity = + match cnfg.max_queue_size with + | Some n when bounded_queue -> n + | _ -> DataflowBlockOptions.Unbounded + ) + + let link_opts = DataflowLinkOptions(PropagateCompletion = true) + + + let loadBlock = + TransformBlock( + load, + opts true cnfg.max_readers + + ) + + let processBlock = + TransformBlock(applyFilter, opts true cnfg.max_conv_performers) + + let saveBlock = ActionBlock(save, opts true cnfg.max_writers) + + let waitingBlock = BufferBlock() + + use _ = waitingBlock.LinkTo(loadBlock, link_opts) + use _ = loadBlock.LinkTo(processBlock, link_opts) + use _ = processBlock.LinkTo(saveBlock, link_opts) + System.IO.Directory.CreateDirectory cnfg.outdir |> ignore + + Directory.GetFiles(cnfg.srcdir, "*.jpg", SearchOption.TopDirectoryOnly) + |> Array.iter @@ fun name -> waitingBlock.Post name |> ignore + + waitingBlock.Complete() + + try + saveBlock.Completion.Wait() + with :? AggregateException as ex -> + ex.InnerExceptions + |> Seq.iter (fun e -> + match e.Message with + | "!_img.empty()" -> () + | _ -> Printf.eprintfn "Error: %s" e.Message) + + +let cnfg = + { srcdir = "./" + outdir = "./Out" + prefix = "Conv" + max_readers = Some 1 + max_writers = None + max_conv_performers = Some 4 + max_queue_size = Some 4 + conv_mode = ByRow } + +[] +let main _ = + + let kernel = new Mat([| 3; 3 |], MatType.CV_32FC1, Scalar -1.) + ~~~ (ind_fl kernel).set_Item 1 1 @@ float32 8 + full_cycle_paral_conv cnfg kernel + kernel.Dispose() + 0 From a3a3b936c8cfa5c32b036dd4afff28678be7a21a Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii Date: Fri, 10 Oct 2025 15:26:48 +0300 Subject: [PATCH 06/16] ref: rename project --- root.fsproj => conv.fsproj | 1 - 1 file changed, 1 deletion(-) rename root.fsproj => conv.fsproj (86%) diff --git a/root.fsproj b/conv.fsproj similarity index 86% rename from root.fsproj rename to conv.fsproj index 41372a4..6743bd9 100644 --- a/root.fsproj +++ b/conv.fsproj @@ -14,7 +14,6 @@ - From cf448500f0cfa8867e221e6f23c2698294cba219 Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii Date: Fri, 10 Oct 2025 15:34:20 +0300 Subject: [PATCH 07/16] feat: cli, kernels --- Lib/Conv.fs | 2 + Lib/Kernels.fs | 104 ++++++++++++++++++++++ Lib/Lib.fsproj | 5 +- Lib/ReadConvWrite.fs | 93 ++++++++++++++++++++ Program.fs | 199 +++++++++++++++++++++++++------------------ 5 files changed, 319 insertions(+), 84 deletions(-) create mode 100644 Lib/Kernels.fs create mode 100644 Lib/ReadConvWrite.fs diff --git a/Lib/Conv.fs b/Lib/Conv.fs index 8275635..c3a6262 100644 --- a/Lib/Conv.fs +++ b/Lib/Conv.fs @@ -1,3 +1,5 @@ +module Conv + open OpenCvSharp open System.Threading.Tasks diff --git a/Lib/Kernels.fs b/Lib/Kernels.fs new file mode 100644 index 0000000..1d78d00 --- /dev/null +++ b/Lib/Kernels.fs @@ -0,0 +1,104 @@ +module Kernels + +let (@@) = (<|) + +type kernel = + | Id + | Box3x3 + | Box5x5 + | Box7x7 + | Gaussian + | DoG // Difference of Gaussians + | Lapl8Neigh //Laplacian with 8 (eq-t) neighbours + | Lapl4Neigh + | Lapl5x5 + | Lapl5x5LoG //Laplacian Of Gaussians + | SobelN // Directional: from North to South + | SobelS + | SobelW + | SobelE + +let to_2d_array: kernel -> float32[,] = + + let distance_based_3x3 z o t = + Array2D.init 3 3 + @@ fun y x -> + float32 + @@ let dx, dy = abs (1 - x), abs (1 - y) in + + match dx + dy with + | 0 -> z + | 1 -> o + | 2 -> t + + let distance_based_5x5 z o t_eq t th f = + Array2D.init 5 5 + @@ fun y x -> + float32 + @@ let dx, dy = abs (2 - x), abs (2 - y) in + + match dx + dy with + | 0 -> z + | 1 -> o + | 2 when dx = dy -> t_eq + | 2 -> t + | 3 -> th + | 4 -> f + + function + | Id -> + let arr = Array2D.create 3 3 @@ float32 0 + arr[1, 1] <- float32 1 + arr + | Box3x3 -> Array2D.create 3 3 @@ float32 (1. / 9.) + | Box5x5 -> Array2D.create 5 5 @@ float32 (1. / 25.) + | Box7x7 -> Array2D.create 7 7 @@ float32 (1. / 49.) + | Gaussian -> distance_based_5x5 0.150342 0.094907 0.059912 0.023792 0.015019 0.003765 + | DoG -> distance_based_3x3 1. -0.155615 -0.0943852 + | Lapl8Neigh -> + let arr = Array2D.create 3 3 @@ float32 -1 + arr[1, 1] <- float32 8 + arr + | Lapl4Neigh -> distance_based_3x3 4. -1. 0. + | Lapl5x5 -> distance_based_5x5 4. 3. 2. 0. -1. -4. + | Lapl5x5LoG -> distance_based_5x5 16. -2. -1. -1. 0. 0. + | SobelN -> + Array2D.init 3 3 + @@ fun y x -> + float32 + @@ match y, x with + | 1, _ -> 0 + | 0, 1 -> 2 + | 2, 1 -> -2 + | 0, _ -> 1 + | 2, _ -> -1 + | SobelS -> + Array2D.init 3 3 + @@ fun y x -> + float32 + @@ match y, x with + | 1, _ -> 0 + | 0, 1 -> -2 + | 2, 1 -> 2 + | 0, _ -> -1 + | 2, _ -> 1 + | SobelW -> + Array2D.init 3 3 + @@ fun y x -> + float32 + @@ match x, y with + | 1, _ -> 0 + | 0, 1 -> 2 + | 2, 1 -> -2 + | 0, _ -> 1 + | 2, _ -> -1 + | SobelE -> + Array2D.init 3 3 + @@ fun y x -> + float32 + @@ match x, y with + | 1, _ -> 0 + | 0, 1 -> -2 + | 2, 1 -> 2 + | 0, _ -> -1 + | 2, _ -> 1 diff --git a/Lib/Lib.fsproj b/Lib/Lib.fsproj index cc844af..7ad0173 100644 --- a/Lib/Lib.fsproj +++ b/Lib/Lib.fsproj @@ -4,12 +4,15 @@ Exe net6.0 - + + + + diff --git a/Lib/ReadConvWrite.fs b/Lib/ReadConvWrite.fs new file mode 100644 index 0000000..386dd0c --- /dev/null +++ b/Lib/ReadConvWrite.fs @@ -0,0 +1,93 @@ +open System.IO +open OpenCvSharp +open Conv +open System.Threading.Tasks.Dataflow + +type source = string list * SearchOption + + +type cnfg = + { source: source + outdir: string + prefix: string + max_readers: int option + max_writers: int option + max_conv_performers: int option + max_queue_size: int option + conv_mode: paral_method } + +let (~~~) = curry3 + + +let load name = + let src = new Mat(name, ImreadModes.Grayscale) + (src, name) + +let applyFilter conv_mode kernel (m: Mat, name) = + let dest = new Mat(m.Height, m.Width, m.Type()) + conv m dest kernel conv_mode + m.Dispose() + (dest, name) + +let save outdir prefix (m: Mat, name: string) = + Cv2.ImWrite(sprintf "./%s/%s-%s" outdir prefix @@ Path.GetFileName name, m) + |> ignore + + m.Dispose() + +let full_cycle_paral_conv cnfg kernel = + + let opts bounded_queue p_deg = + ExecutionDataflowBlockOptions( + MaxDegreeOfParallelism = Option.defaultValue DataflowBlockOptions.Unbounded p_deg, + BoundedCapacity = + match cnfg.max_queue_size with + | Some n when bounded_queue -> n + | _ -> DataflowBlockOptions.Unbounded + ) + + let link_opts = DataflowLinkOptions(PropagateCompletion = true) + + + let loadBlock = + TransformBlock( + load, + opts true cnfg.max_readers + + ) + + let processBlock = + TransformBlock( + applyFilter cnfg.conv_mode kernel, + opts true cnfg.max_conv_performers + ) + + let saveBlock = + ActionBlock(save cnfg.outdir cnfg.prefix, opts true cnfg.max_writers) + + let waitingBlock = BufferBlock() + + use _ = waitingBlock.LinkTo(loadBlock, link_opts) + use _ = loadBlock.LinkTo(processBlock, link_opts) + use _ = processBlock.LinkTo(saveBlock, link_opts) + Directory.CreateDirectory cnfg.outdir |> ignore + + let pathes, opts = cnfg.source + + List.iter (fun name -> waitingBlock.Post name |> ignore) + @@ List.fold + (fun files path -> + if Directory.Exists path then + List.append (List.ofArray @@ Directory.GetFiles(path, "*.jpg", opts)) files + elif File.Exists path then + path :: files + else + eprintfn "file %s wasn't found!" path + files) + [] + pathes + + waitingBlock.Complete() + + + saveBlock.Completion.Wait() diff --git a/Program.fs b/Program.fs index 185a0e7..0cb263f 100644 --- a/Program.fs +++ b/Program.fs @@ -1,88 +1,11 @@ open System.IO open OpenCvSharp open Conv -open System.Threading.Tasks.Dataflow - -let (>>) f a = (fun () -> a) f - -open System - -type cnfg = - { mutable srcdir: string - mutable outdir: string - mutable prefix: string - mutable max_readers: int option - mutable max_writers: int option - mutable max_conv_performers: int option - mutable max_queue_size: int option - mutable conv_mode: paral_method } - -let full_cycle_paral_conv cnfg kernel = - - let load name = - let src = new Mat(name, ImreadModes.Grayscale) - (src, name) - - let applyFilter (m: Mat, name) = - let dest = new Mat(m.Height, m.Width, m.Type()) - conv m dest kernel cnfg.conv_mode - m.Dispose() - (dest, name) - - let save (m: Mat, name: string) = - Cv2.ImWrite(sprintf "./%s/%s-%s" cnfg.outdir cnfg.prefix @@ Path.GetFileName name, m) - |> ignore - - m.Dispose() - - let opts bounded_queue p_deg = - ExecutionDataflowBlockOptions( - MaxDegreeOfParallelism = Option.defaultValue DataflowBlockOptions.Unbounded p_deg, - BoundedCapacity = - match cnfg.max_queue_size with - | Some n when bounded_queue -> n - | _ -> DataflowBlockOptions.Unbounded - ) - - let link_opts = DataflowLinkOptions(PropagateCompletion = true) - - - let loadBlock = - TransformBlock( - load, - opts true cnfg.max_readers - - ) - - let processBlock = - TransformBlock(applyFilter, opts true cnfg.max_conv_performers) - - let saveBlock = ActionBlock(save, opts true cnfg.max_writers) - - let waitingBlock = BufferBlock() - - use _ = waitingBlock.LinkTo(loadBlock, link_opts) - use _ = loadBlock.LinkTo(processBlock, link_opts) - use _ = processBlock.LinkTo(saveBlock, link_opts) - System.IO.Directory.CreateDirectory cnfg.outdir |> ignore - - Directory.GetFiles(cnfg.srcdir, "*.jpg", SearchOption.TopDirectoryOnly) - |> Array.iter @@ fun name -> waitingBlock.Post name |> ignore - - waitingBlock.Complete() - - try - saveBlock.Completion.Wait() - with :? AggregateException as ex -> - ex.InnerExceptions - |> Seq.iter (fun e -> - match e.Message with - | "!_img.empty()" -> () - | _ -> Printf.eprintfn "Error: %s" e.Message) - +open ReadConvWrite +open Argu let cnfg = - { srcdir = "./" + { source = ([ "./" ], SearchOption.TopDirectoryOnly) outdir = "./Out" prefix = "Conv" max_readers = Some 1 @@ -91,11 +14,121 @@ let cnfg = max_queue_size = Some 4 conv_mode = ByRow } +type conv_mode = + | Seql + | ByPixel + | ByRow + | ByColumn + | ByRect + +type CliArgument = + | [] Source of pathes: string list + | [] Recursive + | [] Kernel of Kernels.kernel + | [] Out_Dir of path: string + | [] Prefix of str: string + | [] Conv_Mode of conv_mode + | [] Rect_Height of num: int option + | [] Rect_Width of num: int option + | Max_Readers of num: int + | Max_Conv_Workers of num: int + | Max_Writers of num: int + | Max_Queue_size of num: int + + interface IArgParserTemplate with + member s.Usage = + match s with + | Source _ -> "specify (list of) image/directory pathes (Default: ./ )" + | Kernel _ -> "specify kernel to be applied (Default: id)" + | Recursive -> "search in nested directories too" + | Out_Dir _ -> "specify directory for output images (Default: ./Out )" + | Prefix _ -> "specify prefix for output image names (Default: Conv)" + | Conv_Mode _ -> "specify the rule for (inter-threading) data partition (Default: ByRow)" + | Rect_Height _ -> "specify rectangle height for ByRect partition (Default: 9). -1 stands for image height" + | Rect_Width _ -> "specify rectangle width for ByRect partition (Default: 9). -1 stands for image width" + | Max_Readers _ -> + "specify upper bound for image reading threads number (Default: 1). -1 stands for unbound" + | Max_Conv_Workers _ -> + "specify upper bound for image conv. threads number (Default: 4). -1 stands for unbound" + | Max_Writers _ -> + "specify upper bound for output image saving threads number (Default: -1). -1 stands for unbound" + | Max_Queue_size _ -> "specify upper bound for images in queue (Default: 4). -1 stands for unbound" + + [] -let main _ = +let main args = + let parser = + ArgumentParser.Create( + errorHandler = + ProcessExiter( + colorizer = + function + | ErrorCode.HelpText -> None + | _ -> Some System.ConsoleColor.Red + ) + ) + + let args = parser.Parse args + + let int_hndl def = + function + | n when n >= 0 -> Some n + | -1 -> None + | _ -> Some def + + let rec_flag_hndl = + function + | true -> SearchOption.AllDirectories + | false -> SearchOption.TopDirectoryOnly + + + let mode_hndl def mode rect_width rect_height = + match mode with + | ByRect -> + let size_hndl = + function + | Some -1 -> UnLimited + | Some n when n >= 0 -> Limited n + | _ -> Limited def + + paral_method.ByRect(size_hndl rect_width, size_hndl rect_height) + + | Seql -> paral_method.Seql + | ByPixel -> paral_method.ByPixel + | ByRow -> paral_method.ByRow + | ByColumn -> paral_method.ByColumn + + + let cnfg = + { source = args.GetResult(Source, defaultValue = [ "./" ]), args.Contains Recursive |> rec_flag_hndl + outdir = args.GetResult(Out_Dir, defaultValue = "./Out") + prefix = args.GetResult(Prefix, defaultValue = "Conv") + max_readers = + int_hndl 1 + @@ args.GetResult(Max_Readers, defaultValue = 1) + max_conv_performers = + int_hndl 4 + @@ args.GetResult(Max_Conv_Workers, defaultValue = 4) + max_writers = + int_hndl (-1) + @@ args.GetResult(Max_Writers, defaultValue = -1) + max_queue_size = + int_hndl 4 + @@ args.GetResult(Max_Queue_size, defaultValue = 4) + conv_mode = + mode_hndl + 9 + (args.GetResult(Conv_Mode, defaultValue = ByRow)) + (args.GetResult(Rect_Width, defaultValue = None)) + (args.GetResult(Rect_Height, defaultValue = None)) } + + + let kernel = + Mat.FromArray( + Kernels.to_2d_array + @@ args.GetResult(Kernel, defaultValue = Kernels.Id) + ) - let kernel = new Mat([| 3; 3 |], MatType.CV_32FC1, Scalar -1.) - ~~~ (ind_fl kernel).set_Item 1 1 @@ float32 8 full_cycle_paral_conv cnfg kernel kernel.Dispose() 0 From bef1b6ee1ad697d99b6d4b2529375cc4768a00fc Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii <144726123+ns-58@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:37:31 +0300 Subject: [PATCH 08/16] chore: LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d6abdbc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Nikita Shchutskii + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From e0908b3ce8ac642b69e7752f7f95ba5e1d00d09e Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii <144726123+ns-58@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:59:52 +0300 Subject: [PATCH 09/16] chore: readme --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5341996..8a4acd6 100644 --- a/README.md +++ b/README.md @@ -1 +1,52 @@ -Todo: add license +# The tool for image convolution +## Usage: +Clone repo: + ```bash + $ git clone https://github.com/ns-58/conv + $ cd conv + ``` +Build app: + ```bash + $ dotnet publish -c release + $ ln -s ./bin/release/net6.0/publish/conv conv + ``` +Use --help for app usage guide: + ```bash + $ ./conv --help +USAGE: conv [--help] [--source [...]] [--recursive] + [--kernel ] + [--out-dir ] [--prefix ] [--conv-mode ] + [--rect-height []] [--rect-width []] [--max-readers ] [--max-conv-workers ] + [--max-writers ] [--max-queue-size ] + +OPTIONS: + + --source, -s [...] + specify (list of) image/directory pathes (Default: ./ ) + --recursive, -r search in nested directories too + --kernel, -k + specify kernel to be applied (Default: id) + --out-dir, -o specify directory for output images (Default: ./Out ) + --prefix, -p specify prefix for output image names (Default: Conv) + --conv-mode, -m + specify the rule for (inter-threading) data partition (Default: ByRow) + --rect-height, -rh [] + specify rectangle height for ByRect partition (Default: 9). -1 stands for image + height + --rect-width, -rw [] + specify rectangle width for ByRect partition (Default: 9). -1 stands for image + width + --max-readers specify upper bound for image reading threads number (Default: 1). -1 stands for + unbound + --max-conv-workers + specify upper bound for image conv. threads number (Default: 4). -1 stands for + unbound + --max-writers specify upper bound for output image saving threads number (Default: -1). -1 + stands for unbound + --max-queue-size + specify upper bound for images in queue (Default: 4). -1 stands for unbound + --help display this list of options. + ``` +## Additional requirements: + opencv4.7+ + consider use of container https://github.com/users/shimat/packages/container/package/opencvsharp%2Fubuntu22-dotnet6sdk-opencv4.7.0 From b0fe2ca1848abc66fdf1f49ce6d065d1eb809826 Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii Date: Sun, 12 Oct 2025 18:27:59 +0300 Subject: [PATCH 10/16] ref: remove dead code --- Program.fs | 250 +++++++++++++++++++++++++---------------------------- 1 file changed, 116 insertions(+), 134 deletions(-) diff --git a/Program.fs b/Program.fs index 0cb263f..94d7eb7 100644 --- a/Program.fs +++ b/Program.fs @@ -1,134 +1,116 @@ -open System.IO -open OpenCvSharp -open Conv -open ReadConvWrite -open Argu - -let cnfg = - { source = ([ "./" ], SearchOption.TopDirectoryOnly) - outdir = "./Out" - prefix = "Conv" - max_readers = Some 1 - max_writers = None - max_conv_performers = Some 4 - max_queue_size = Some 4 - conv_mode = ByRow } - -type conv_mode = - | Seql - | ByPixel - | ByRow - | ByColumn - | ByRect - -type CliArgument = - | [] Source of pathes: string list - | [] Recursive - | [] Kernel of Kernels.kernel - | [] Out_Dir of path: string - | [] Prefix of str: string - | [] Conv_Mode of conv_mode - | [] Rect_Height of num: int option - | [] Rect_Width of num: int option - | Max_Readers of num: int - | Max_Conv_Workers of num: int - | Max_Writers of num: int - | Max_Queue_size of num: int - - interface IArgParserTemplate with - member s.Usage = - match s with - | Source _ -> "specify (list of) image/directory pathes (Default: ./ )" - | Kernel _ -> "specify kernel to be applied (Default: id)" - | Recursive -> "search in nested directories too" - | Out_Dir _ -> "specify directory for output images (Default: ./Out )" - | Prefix _ -> "specify prefix for output image names (Default: Conv)" - | Conv_Mode _ -> "specify the rule for (inter-threading) data partition (Default: ByRow)" - | Rect_Height _ -> "specify rectangle height for ByRect partition (Default: 9). -1 stands for image height" - | Rect_Width _ -> "specify rectangle width for ByRect partition (Default: 9). -1 stands for image width" - | Max_Readers _ -> - "specify upper bound for image reading threads number (Default: 1). -1 stands for unbound" - | Max_Conv_Workers _ -> - "specify upper bound for image conv. threads number (Default: 4). -1 stands for unbound" - | Max_Writers _ -> - "specify upper bound for output image saving threads number (Default: -1). -1 stands for unbound" - | Max_Queue_size _ -> "specify upper bound for images in queue (Default: 4). -1 stands for unbound" - - -[] -let main args = - let parser = - ArgumentParser.Create( - errorHandler = - ProcessExiter( - colorizer = - function - | ErrorCode.HelpText -> None - | _ -> Some System.ConsoleColor.Red - ) - ) - - let args = parser.Parse args - - let int_hndl def = - function - | n when n >= 0 -> Some n - | -1 -> None - | _ -> Some def - - let rec_flag_hndl = - function - | true -> SearchOption.AllDirectories - | false -> SearchOption.TopDirectoryOnly - - - let mode_hndl def mode rect_width rect_height = - match mode with - | ByRect -> - let size_hndl = - function - | Some -1 -> UnLimited - | Some n when n >= 0 -> Limited n - | _ -> Limited def - - paral_method.ByRect(size_hndl rect_width, size_hndl rect_height) - - | Seql -> paral_method.Seql - | ByPixel -> paral_method.ByPixel - | ByRow -> paral_method.ByRow - | ByColumn -> paral_method.ByColumn - - - let cnfg = - { source = args.GetResult(Source, defaultValue = [ "./" ]), args.Contains Recursive |> rec_flag_hndl - outdir = args.GetResult(Out_Dir, defaultValue = "./Out") - prefix = args.GetResult(Prefix, defaultValue = "Conv") - max_readers = - int_hndl 1 - @@ args.GetResult(Max_Readers, defaultValue = 1) - max_conv_performers = - int_hndl 4 - @@ args.GetResult(Max_Conv_Workers, defaultValue = 4) - max_writers = - int_hndl (-1) - @@ args.GetResult(Max_Writers, defaultValue = -1) - max_queue_size = - int_hndl 4 - @@ args.GetResult(Max_Queue_size, defaultValue = 4) - conv_mode = - mode_hndl - 9 - (args.GetResult(Conv_Mode, defaultValue = ByRow)) - (args.GetResult(Rect_Width, defaultValue = None)) - (args.GetResult(Rect_Height, defaultValue = None)) } - - - let kernel = - Mat.FromArray( - Kernels.to_2d_array - @@ args.GetResult(Kernel, defaultValue = Kernels.Id) - ) - - full_cycle_paral_conv cnfg kernel - kernel.Dispose() - 0 +open System.IO +open OpenCvSharp +open Conv +open ReadConvWrite +open Argu +open System + + +type conv_mode = + | Seql + | ByPixel + | ByRow + | ByColumn + | ByRect + +type CliArgument = + | [] Source of pathes: string list + | [] Recursive + | [] Kernel of Kernels.kernel + | [] Out_Dir of path: string + | [] Prefix of str: string + | [] Conv_Mode of conv_mode + | [] Rect_Height of num: int option + | [] Rect_Width of num: int option + | Max_Readers of num: int + | Max_Conv_Workers of num: int + | Max_Writers of num: int + | Max_Queue_size of num: int + + interface IArgParserTemplate with + member s.Usage = + match s with + | Source _ -> "specify (list of) image/directory absolute pathes (Default: ./ )" + | Kernel _ -> "specify kernel to be applied (Default: id)" + | Recursive -> "search in nested directories too" + | Out_Dir _ -> "specify directory for output images (Default: ./Out )" + | Prefix _ -> "specify prefix for output image names (Default: Conv)" + | Conv_Mode _ -> "specify the rule for (inter-threading) data partition (Default: ByRow)" + | Rect_Height _ -> "specify rectangle height for ByRect partition (Default: 9). -1 stands for image height" + | Rect_Width _ -> "specify rectangle width for ByRect partition (Default: 9). -1 stands for image width" + | Max_Readers _ -> + "specify upper bound for image reading threads number (Default: 1). -1 stands for unbound" + | Max_Conv_Workers _ -> + "specify upper bound for image conv. threads number (Default: 4). -1 stands for unbound" + | Max_Writers _ -> + "specify upper bound for output image saving threads number (Default: -1). -1 stands for unbound" + | Max_Queue_size _ -> "specify upper bound for images in queue (Default: 4). -1 stands for unbound" + + +[] +let main args = + let parser = + ArgumentParser.Create( + errorHandler = + ProcessExiter( + colorizer = + function + | ErrorCode.HelpText -> None + | _ -> Some ConsoleColor.Red + ) + ) + + let args = parser.Parse args + + + let int_hndl def = + function + | n when n >= 0 -> Some n + | -1 -> None + | _ -> Some def + + let rec_flag_hndl = + function + | true -> SearchOption.AllDirectories + | false -> SearchOption.TopDirectoryOnly + + + let mode_hndl def mode rect_width rect_height = + match mode with + | ByRect -> + let size_hndl = + function + | Some -1 -> UnLimited + | Some n when n >= 0 -> Limited n + | _ -> Limited def + + paral_method.ByRect(size_hndl rect_width, size_hndl rect_height) + + | Seql -> paral_method.Seql + | ByPixel -> paral_method.ByPixel + | ByRow -> paral_method.ByRow + | ByColumn -> paral_method.ByColumn + + + let cnfg = + { source = args.GetResult(Source, defaultValue = [ "./" ]), args.Contains Recursive |> rec_flag_hndl + outdir = args.GetResult(Out_Dir, defaultValue = "./Out") + prefix = args.GetResult(Prefix, defaultValue = "Conv") + max_readers = int_hndl 1 @@ args.GetResult(Max_Readers, defaultValue = 1) + max_conv_performers = int_hndl 4 @@ args.GetResult(Max_Conv_Workers, defaultValue = 4) + max_writers = int_hndl (-1) @@ args.GetResult(Max_Writers, defaultValue = -1) + max_queue_size = int_hndl 4 @@ args.GetResult(Max_Queue_size, defaultValue = 4) + conv_mode = + mode_hndl + 9 + (args.GetResult(Conv_Mode, defaultValue = ByRow)) + (args.GetResult(Rect_Width, defaultValue = None)) + (args.GetResult(Rect_Height, defaultValue = None)) } + + + let kernel = + Mat.FromArray(Kernels.to_2d_array @@ args.GetResult(Kernel, defaultValue = Kernels.Id)) + + full_cycle_paral_conv cnfg kernel + kernel.Dispose() + 0 From fec7bb5855e9424e33c21a42a0f0264a41cacbc4 Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii <144726123+ns-58@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:48:13 +0300 Subject: [PATCH 11/16] chore: partition bench results --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index 8a4acd6..4b615dc 100644 --- a/README.md +++ b/README.md @@ -50,3 +50,52 @@ OPTIONS: ## Additional requirements: opencv4.7+ consider use of container https://github.com/users/shimat/packages/container/package/opencvsharp%2Fubuntu22-dotnet6sdk-opencv4.7.0 + +## Benchmarking +BenchmarkDotNet v0.13.8, Ubuntu 22.04.1 LTS (Jammy Jellyfish) (container) +11th Gen Intel Core i7-11850H 2.50GHz, 1 CPU, 16 logical and 8 physical cores +.NET SDK 6.0.405 + [Host] : .NET 6.0.13 (6.0.1322.58009), X64 RyuJIT AVX2 DEBUG + DefaultJob : .NET 6.0.13 (6.0.1322.58009), X64 RyuJIT AVX2 +### Image partition +sizes: +small.jpg --- 750x750 (108KB) +big.jpg --- + +| Method | file | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | +|-------------------- |------------- |-----------:|---------:|---------:|-------------:|------------:|----------:|-----------:| +| Seql | ../small.jpg | 670.9 ms | 1.14 ms | 1.01 ms | 53000.0000 | - | - | 643.78 MB | +| ByPixel | ../small.jpg | 360.1 ms | 3.03 ms | 2.83 ms | 54000.0000 | 1500.0000 | - | 646.74 MB | +| ByRow | ../small.jpg | 273.4 ms | 3.12 ms | 2.43 ms | 53500.0000 | 1000.0000 | - | 643.8 MB | +| ByColumn | ../small.jpg | 286.2 ms | 3.31 ms | 3.09 ms | 53500.0000 | 1000.0000 | - | 643.8 MB | +| ByRect1x1 | ../small.jpg | 1,192.1 ms | 23.46 ms | 45.75 ms | 216000.0000 | 24000.0000 | 1000.0000 | 2580.61 MB | +| ByRect1ker | ../small.jpg | 482.6 ms | 5.95 ms | 5.28 ms | 95000.0000 | 16000.0000 | - | 1144.73 MB | +| ByRect5ker | ../small.jpg | 324.7 ms | 6.28 ms | 6.45 ms | 61000.0000 | 2000.0000 | - | 731.14 MB | +| ByRect10ker | ../small.jpg | 338.2 ms | 8.71 ms | 25.69 ms | 57500.0000 | 1500.0000 | - | 686.81 MB | +| ByRect15ker | ../small.jpg | 300.8 ms | 3.49 ms | 3.27 ms | 56000.0000 | 1000.0000 | - | 672.3 MB | +| By1KerHeightRows | ../small.jpg | 378.0 ms | 4.79 ms | 4.00 ms | 71000.0000 | 1000.0000 | - | 858.11 MB | +| By3KerHeightRows | ../small.jpg | 314.2 ms | 6.08 ms | 5.39 ms | 59000.0000 | 1000.0000 | - | 715.24 MB | +| By10KerHeightRows | ../small.jpg | 297.4 ms | 5.77 ms | 6.17 ms | 55500.0000 | 1000.0000 | - | 664.96 MB | +| By1KerWidthColomns | ../small.jpg | 394.5 ms | 7.42 ms | 6.94 ms | 71000.0000 | 1000.0000 | - | 858.09 MB | +| By3KerWidthColomns | ../small.jpg | 310.6 ms | 4.66 ms | 4.13 ms | 59000.0000 | 1000.0000 | - | 715.24 MB | +| By10KerWidthColomns | ../small.jpg | 296.1 ms | 5.83 ms | 6.48 ms | 55500.0000 | 1000.0000 | - | 664.95 MB | + +| ByRow | ../big.jpg | 39.11 s | 0.323 s | 0.302 s | 7229000.0000 | 147000.0000 | - | 84.26 GB | +| ByColumn | ../big.jpg | 37.33 s | 0.166 s | 0.155 s | 7227000.0000 | 139000.0000 | - | 84.26 GB | +| ByRect15ker | ../big.jpg | 67.81 s | 1.350 s | 3.556 s | 7564000.0000 | 272000.0000 | 9000.0000 | 88.04 GB | +| By10KerHeightRows | ../big.jpg | 38.16 s | 0.295 s | 0.246 s | 7471000.0000 | 218000.0000 | 1000.0000 | 87.07 GB | +| By10KerWidthColomns | ../big.jpg | 37.44 s | 0.179 s | 0.150 s | 7468000.0000 | 142000.0000 | - | 87.07 GB | + + +// * Legends * + file : Value of the 'file' parameter + Mean : Arithmetic mean of all measurements + Error : Half of 99.9% confidence interval + StdDev : Standard deviation of all measurements + Gen0 : GC Generation 0 collects per 1000 operations + Gen1 : GC Generation 1 collects per 1000 operations + Gen2 : GC Generation 2 collects per 1000 operations + Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B) + +Итог: благодаря современным компиляторам/вычислителям способ разбиения изображения (по строчкам/столбцам/прямоугольникам) не оказывает существенного влияния на производительность свертки; на первую роль выходят издержки синхронизации ( ~ мелкость разбиения) и сложность опредения границ его участков + From b795fd302ffa24585fbfd743a90272a23538eede Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii <144726123+ns-58@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:51:30 +0300 Subject: [PATCH 12/16] chore: fix bench results analysis --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4b615dc..12bf2f2 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ BenchmarkDotNet v0.13.8, Ubuntu 22.04.1 LTS (Jammy Jellyfish) (container) DefaultJob : .NET 6.0.13 (6.0.1322.58009), X64 RyuJIT AVX2 ### Image partition sizes: -small.jpg --- 750x750 (108KB) -big.jpg --- +small.jpg --- 750x750 (108KB) + big.jpg --- 11256x4877 (59MB) | Method | file | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated | |-------------------- |------------- |-----------:|---------:|---------:|-------------:|------------:|----------:|-----------:| @@ -97,5 +97,5 @@ big.jpg --- Gen2 : GC Generation 2 collects per 1000 operations Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B) -Итог: благодаря современным компиляторам/вычислителям способ разбиения изображения (по строчкам/столбцам/прямоугольникам) не оказывает существенного влияния на производительность свертки; на первую роль выходят издержки синхронизации ( ~ мелкость разбиения) и сложность опредения границ его участков +Итог: благодаря современным компиляторам/вычислителям способ разбиения изображения (по строчкам/столбцам/прямоугольникам) не оказывает существенного влияния на производительность свертки; на первую роль выходят размер разбиения и сложность опредения границ его участков From 2d8bc8b06a6de17f4bf49811a2cde4419cf5fee6 Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii <144726123+ns-58@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:53:58 +0300 Subject: [PATCH 13/16] chore: fix table view --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 12bf2f2..cfa5e8f 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ small.jpg --- 750x750 (108KB) | By1KerWidthColomns | ../small.jpg | 394.5 ms | 7.42 ms | 6.94 ms | 71000.0000 | 1000.0000 | - | 858.09 MB | | By3KerWidthColomns | ../small.jpg | 310.6 ms | 4.66 ms | 4.13 ms | 59000.0000 | 1000.0000 | - | 715.24 MB | | By10KerWidthColomns | ../small.jpg | 296.1 ms | 5.83 ms | 6.48 ms | 55500.0000 | 1000.0000 | - | 664.95 MB | - +| - |- |- |- |- |- |- |- |- | | ByRow | ../big.jpg | 39.11 s | 0.323 s | 0.302 s | 7229000.0000 | 147000.0000 | - | 84.26 GB | | ByColumn | ../big.jpg | 37.33 s | 0.166 s | 0.155 s | 7227000.0000 | 139000.0000 | - | 84.26 GB | | ByRect15ker | ../big.jpg | 67.81 s | 1.350 s | 3.556 s | 7564000.0000 | 272000.0000 | 9000.0000 | 88.04 GB | From 272fffae230f2aa9552acd9bdc4664f9a0e745b1 Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii <144726123+ns-58@users.noreply.github.com> Date: Sun, 12 Oct 2025 19:55:12 +0300 Subject: [PATCH 14/16] chore: fix legends view --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cfa5e8f..d89201b 100644 --- a/README.md +++ b/README.md @@ -87,15 +87,15 @@ small.jpg --- 750x750 (108KB) | By10KerWidthColomns | ../big.jpg | 37.44 s | 0.179 s | 0.150 s | 7468000.0000 | 142000.0000 | - | 87.07 GB | -// * Legends * - file : Value of the 'file' parameter - Mean : Arithmetic mean of all measurements - Error : Half of 99.9% confidence interval - StdDev : Standard deviation of all measurements - Gen0 : GC Generation 0 collects per 1000 operations - Gen1 : GC Generation 1 collects per 1000 operations - Gen2 : GC Generation 2 collects per 1000 operations - Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B) +// * Legends * + file : Value of the 'file' parameter + Mean : Arithmetic mean of all measurements + Error : Half of 99.9% confidence interval + StdDev : Standard deviation of all measurements + Gen0 : GC Generation 0 collects per 1000 operations + Gen1 : GC Generation 1 collects per 1000 operations + Gen2 : GC Generation 2 collects per 1000 operations + Allocated : Allocated memory per single operation (managed only, inclusive, 1KB = 1024B) Итог: благодаря современным компиляторам/вычислителям способ разбиения изображения (по строчкам/столбцам/прямоугольникам) не оказывает существенного влияния на производительность свертки; на первую роль выходят размер разбиения и сложность опредения границ его участков From ad58ac48129f3a4ff76e1e50637d83ef0d3d4efb Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii Date: Sun, 26 Oct 2025 09:58:43 +0300 Subject: [PATCH 15/16] partition bench with short analysis --- Bench/{Partition.fs => Bench.fs} | 99 +++++++++++++++++++++++++++++++- Bench/Bench.fsproj | 2 +- README.md | 22 +++++++ 3 files changed, 121 insertions(+), 2 deletions(-) rename Bench/{Partition.fs => Bench.fs} (50%) diff --git a/Bench/Partition.fs b/Bench/Bench.fs similarity index 50% rename from Bench/Partition.fs rename to Bench/Bench.fs index 4aa2121..1f11afa 100644 --- a/Bench/Partition.fs +++ b/Bench/Bench.fs @@ -5,6 +5,8 @@ open BenchmarkDotNet.Running open OpenCvSharp open System.Runtime.CompilerServices open System.Runtime.InteropServices +open ReadConvWrite + let kernel = new Mat([| 3; 3 |], MatType.CV_32FC1, Scalar -1.) ~~~ (ind_fl kernel).set_Item 1 1 @@ float32 8 @@ -93,8 +95,103 @@ type PartitionConvComparision() = @@ ByRect(Limited @@ ( * ) 10 kernel.Width, UnLimited) +let def_cnfg = + { source = ([ "/root/conv/pics" ], SearchOption.TopDirectoryOnly) + outdir = "./Out" + prefix = "Conv" + max_readers = Some 1 + max_writers = None + max_conv_performers = Some 4 + max_queue_size = Some 4 + conv_mode = ByRow } + + +[] +type Pipelining() = + + [] + member self.R1C1parW1Qub() = + full_cycle_paral_conv + { def_cnfg with + max_readers = Some 1 + max_writers = Some 1 + max_conv_performers = Some 1 + max_queue_size = None + conv_mode = ByRow } + kernel + + [] + member self.R1C1parW1Q1() = + full_cycle_paral_conv + { def_cnfg with + max_readers = Some 1 + max_writers = Some 1 + max_conv_performers = Some 1 + max_queue_size = Some 1 + conv_mode = ByRow } + kernel + + + [] + member self.R1C4parW1Q4() = + full_cycle_paral_conv + { def_cnfg with + max_readers = Some 1 + max_writers = Some 1 + max_conv_performers = Some 4 + max_queue_size = Some 4 + conv_mode = ByRow } + kernel + + [] + member self.R1C4parW1Q8() = + full_cycle_paral_conv + { def_cnfg with + max_readers = Some 1 + max_writers = Some 1 + max_conv_performers = Some 4 + max_queue_size = Some 8 + conv_mode = ByRow } + kernel + + [] + member self.R1C4parWubQ4() = + full_cycle_paral_conv + { def_cnfg with + max_readers = Some 1 + max_writers = None + max_conv_performers = Some 4 + max_queue_size = Some 4 + conv_mode = ByRow } + kernel + + [] + member self.R1CubseqlWubQ16() = + full_cycle_paral_conv + { def_cnfg with + max_readers = Some 1 + max_writers = None + max_conv_performers = None + max_queue_size = Some 16 + conv_mode = Seql } + kernel + + [] + member self.R1C2parWubQ2() = + full_cycle_paral_conv + { def_cnfg with + max_readers = Some 1 + max_writers = None + max_conv_performers = Some 2 + max_queue_size = Some 4 + conv_mode = ByRow } + kernel + + [] + member self.Cleanup() = + Directory.Delete (def_cnfg.outdir, recursive = true ) [] let main _ = - BenchmarkRunner.Run typeof |> ignore + BenchmarkRunner.Run typeof |> ignore 0 diff --git a/Bench/Bench.fsproj b/Bench/Bench.fsproj index 90ec958..23bf845 100644 --- a/Bench/Bench.fsproj +++ b/Bench/Bench.fsproj @@ -6,7 +6,7 @@ - + diff --git a/README.md b/README.md index d89201b..119706f 100644 --- a/README.md +++ b/README.md @@ -99,3 +99,25 @@ small.jpg --- 750x750 (108KB) Итог: благодаря современным компиляторам/вычислителям способ разбиения изображения (по строчкам/столбцам/прямоугольникам) не оказывает существенного влияния на производительность свертки; на первую роль выходят размер разбиения и сложность опредения границ его участков +### Pipelining + +input: 99 650x650 images + +| Method | Mean | Error | StdDev | Median | +|---------------- |--------:|--------:|--------:|--------:| +| R1C1parW1Qub | 35.75 s | 0.563 s | 1.199 s | 35.55 s | +| R1C1parW1Q1 | 35.74 s | 0.240 s | 0.225 s | 35.77 s | +| R1C4parW1Q4 | 48.96 s | 0.954 s | 1.240 s | 48.72 s | +| R1C4parW1Q8 | 50.66 s | 0.750 s | 0.702 s | 50.55 s | +| R1C4parWubQ4 | 51.31 s | 1.019 s | 2.279 s | 52.50 s | +| R1CubseqlWubQ16 | 27.83 s | 0.187 s | 0.175 s | 27.91 s | +| R1C2parWubQ2 | 40.61 s | 0.439 s | 0.411 s | 40.66 s | + +where Rn --- n readers, Cn --- n convolutions, Wn --- n writers, Qn --- queue with size n, ub --- unbound + +Наблюдения и итоги: + одно чтение и одна запись способны "обслуживать" несколько сверток; + параллелить (внутри) несколько одновременных сверток неэфективно (из-за синхронизации на двух уровнях?); + наиболее эффективны вариант с одной параллельной сверткой и вариант с большим кол-вом последовательных, он быстрее всех (синхронизация внутри свертки нужна чаще); + + From 9818adc077f591ce2b569348b2606264f4d91636 Mon Sep 17 00:00:00 2001 From: Nikita Shchutskii <144726123+ns-58@users.noreply.github.com> Date: Sun, 26 Oct 2025 10:03:50 +0300 Subject: [PATCH 16/16] more precise legend in pipelining bench --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 119706f..61c1016 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ input: 99 650x650 images | R1CubseqlWubQ16 | 27.83 s | 0.187 s | 0.175 s | 27.91 s | | R1C2parWubQ2 | 40.61 s | 0.439 s | 0.411 s | 40.66 s | -where Rn --- n readers, Cn --- n convolutions, Wn --- n writers, Qn --- queue with size n, ub --- unbound +where Rn --- n readers, Cn\ --- n convolutions using partition mode \, Wn --- n writers, Qn --- queue with size n, ub --- unbound Наблюдения и итоги: одно чтение и одна запись способны "обслуживать" несколько сверток;