trainingsample 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {trainingsample-0.3.1 → trainingsample-0.3.2}/.github/workflows/ci.yml +2 -3
  2. {trainingsample-0.3.1 → trainingsample-0.3.2}/.github/workflows/publish.yml +30 -8
  3. {trainingsample-0.3.1 → trainingsample-0.3.2}/Cargo.lock +45 -46
  4. {trainingsample-0.3.1 → trainingsample-0.3.2}/Cargo.toml +4 -4
  5. {trainingsample-0.3.1 → trainingsample-0.3.2}/PKG-INFO +3 -2
  6. {trainingsample-0.3.1 → trainingsample-0.3.2}/pyproject.toml +5 -4
  7. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/python_bindings.rs +33 -33
  8. trainingsample-0.3.2/tests/coverage_tests.rs +427 -0
  9. {trainingsample-0.3.1 → trainingsample-0.3.2}/.cargo/config.toml +0 -0
  10. {trainingsample-0.3.1 → trainingsample-0.3.2}/.envrc +0 -0
  11. {trainingsample-0.3.1 → trainingsample-0.3.2}/.gitignore +0 -0
  12. {trainingsample-0.3.1 → trainingsample-0.3.2}/.pre-commit-config.yaml +0 -0
  13. {trainingsample-0.3.1 → trainingsample-0.3.2}/.rustfmt.toml +0 -0
  14. {trainingsample-0.3.1 → trainingsample-0.3.2}/BENCHMARKS.md +0 -0
  15. {trainingsample-0.3.1 → trainingsample-0.3.2}/Dockerfile +0 -0
  16. {trainingsample-0.3.1 → trainingsample-0.3.2}/LICENSE +0 -0
  17. {trainingsample-0.3.1 → trainingsample-0.3.2}/README.md +0 -0
  18. {trainingsample-0.3.1 → trainingsample-0.3.2}/clippy.toml +0 -0
  19. {trainingsample-0.3.1 → trainingsample-0.3.2}/docs/API_COMPAT_CV2.md +0 -0
  20. {trainingsample-0.3.1 → trainingsample-0.3.2}/docs/BUILDING_STATIC_OPENCV.md +0 -0
  21. {trainingsample-0.3.1 → trainingsample-0.3.2}/rust-toolchain.toml +0 -0
  22. {trainingsample-0.3.1 → trainingsample-0.3.2}/scripts/build-opencv-static.sh +0 -0
  23. {trainingsample-0.3.1 → trainingsample-0.3.2}/scripts/repair-wheel.sh +0 -0
  24. {trainingsample-0.3.1 → trainingsample-0.3.2}/scripts/run_competitive_benchmarks.sh +0 -0
  25. {trainingsample-0.3.1 → trainingsample-0.3.2}/setup-env.sh +0 -0
  26. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/core.rs +0 -0
  27. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/cropping.rs +0 -0
  28. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/cv_batch_ops.rs +0 -0
  29. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/cv_batch_ops_optimized.rs +0 -0
  30. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/cv_compat.rs +0 -0
  31. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/format_conversion_simd.rs +0 -0
  32. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/lib.rs +0 -0
  33. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/loading.rs +0 -0
  34. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/luminance.rs +0 -0
  35. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/luminance_simd.rs +0 -0
  36. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/opencv_ops.rs +0 -0
  37. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/tests.rs +0 -0
  38. {trainingsample-0.3.1 → trainingsample-0.3.2}/src/true_batch_ops.rs +0 -0
  39. {trainingsample-0.3.1 → trainingsample-0.3.2}/tests/__init__.py +0 -0
  40. {trainingsample-0.3.1 → trainingsample-0.3.2}/tests/comprehensive_tests.rs +0 -0
  41. {trainingsample-0.3.1 → trainingsample-0.3.2}/tests/test_comprehensive.py +0 -0
  42. {trainingsample-0.3.1 → trainingsample-0.3.2}/tests/test_performance_benchmarks.py +0 -0
  43. {trainingsample-0.3.1 → trainingsample-0.3.2}/tests/test_python_bindings.py +0 -0
@@ -108,7 +108,7 @@ jobs:
108
108
  runs-on: ubuntu-latest
109
109
  strategy:
110
110
  matrix:
111
- python-version: ["3.11", "3.12", "3.13"]
111
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
112
112
 
113
113
  steps:
114
114
  - uses: actions/checkout@v4
@@ -387,7 +387,7 @@ jobs:
387
387
  runs-on: ubuntu-24.04-arm
388
388
  strategy:
389
389
  matrix:
390
- python: ["3.11", "3.12", "3.13"]
390
+ python: ["3.11", "3.12", "3.13", "3.14"]
391
391
 
392
392
  steps:
393
393
  - uses: actions/checkout@v4
@@ -434,7 +434,6 @@ jobs:
434
434
  container: off
435
435
  manylinux: manylinux_2_39
436
436
  env:
437
- PYO3_USE_ABI3_FORWARD_COMPATIBILITY: '1'
438
437
  LIBCLANG_PATH: ${{ env.LIBCLANG_PATH }}
439
438
  LLVM_CONFIG_PATH: ${{ env.LLVM_CONFIG_PATH }}
440
439
  OPENCV_INCLUDE_PATHS: ${{ github.workspace }}/third_party/opencv-static/include/opencv4
@@ -25,6 +25,10 @@ jobs:
25
25
  target: x86_64
26
26
  manylinux: manylinux_2_35
27
27
  python: '3.13'
28
+ - os: ubuntu-22.04
29
+ target: x86_64
30
+ manylinux: manylinux_2_35
31
+ python: '3.14'
28
32
  # Ubuntu 24.04 (libc 2.39)
29
33
  - os: ubuntu-24.04
30
34
  target: x86_64
@@ -38,6 +42,10 @@ jobs:
38
42
  target: x86_64
39
43
  manylinux: manylinux_2_39
40
44
  python: '3.13'
45
+ - os: ubuntu-24.04
46
+ target: x86_64
47
+ manylinux: manylinux_2_39
48
+ python: '3.14'
41
49
  steps:
42
50
  - uses: actions/checkout@v4
43
51
  - uses: actions/setup-python@v5
@@ -104,8 +112,6 @@ jobs:
104
112
  container: off
105
113
  manylinux: ${{ matrix.manylinux }}
106
114
  env:
107
- # Use stable ABI for forward compatibility
108
- PYO3_USE_ABI3_FORWARD_COMPATIBILITY: '1'
109
115
  # LLVM for opencv-rust bindings (from earlier install step)
110
116
  LIBCLANG_PATH: ${{ env.LIBCLANG_PATH }}
111
117
  LLVM_CONFIG_PATH: ${{ env.LLVM_CONFIG_PATH }}
@@ -135,7 +141,7 @@ jobs:
135
141
  runs-on: ubuntu-24.04-arm
136
142
  strategy:
137
143
  matrix:
138
- python: ['3.11', '3.12', '3.13']
144
+ python: ['3.11', '3.12', '3.13', '3.14']
139
145
  steps:
140
146
  - uses: actions/checkout@v4
141
147
  - uses: actions/setup-python@v5
@@ -186,8 +192,6 @@ jobs:
186
192
  container: off
187
193
  manylinux: manylinux_2_39
188
194
  env:
189
- # Use stable ABI for forward compatibility
190
- PYO3_USE_ABI3_FORWARD_COMPATIBILITY: '1'
191
195
  # LLVM for opencv-rust bindings (from earlier install step)
192
196
  LIBCLANG_PATH: ${{ env.LIBCLANG_PATH }}
193
197
  LLVM_CONFIG_PATH: ${{ env.LLVM_CONFIG_PATH }}
@@ -217,7 +221,7 @@ jobs:
217
221
  strategy:
218
222
  matrix:
219
223
  target: [aarch64]
220
- python: ['3.11', '3.12', '3.13']
224
+ python: ['3.11', '3.12', '3.13', '3.14']
221
225
  steps:
222
226
  - uses: actions/checkout@v4
223
227
  - uses: actions/setup-python@v5
@@ -251,8 +255,6 @@ jobs:
251
255
  args: --release --out dist -i python --no-default-features --features python-bindings,simd,opencv,metal
252
256
  sccache: 'false'
253
257
  env:
254
- # Use stable ABI for forward compatibility
255
- PYO3_USE_ABI3_FORWARD_COMPATIBILITY: '1'
256
258
  # OpenCV static linking configuration
257
259
  OPENCV_INCLUDE_PATHS: ${{ github.workspace }}/third_party/opencv-static/include/opencv4
258
260
  OPENCV_LINK_PATHS: ${{ github.workspace }}/third_party/opencv-static/lib
@@ -306,6 +308,11 @@ jobs:
306
308
  manylinux: manylinux_2_35
307
309
  python: '3.13'
308
310
  artifact_name: wheels-ubuntu-22.04-x86_64-py3.13
311
+ - os: ubuntu-22.04
312
+ target: x86_64
313
+ manylinux: manylinux_2_35
314
+ python: '3.14'
315
+ artifact_name: wheels-ubuntu-22.04-x86_64-py3.14
309
316
  # Ubuntu 24.04 (libc 2.39)
310
317
  - os: ubuntu-24.04
311
318
  target: x86_64
@@ -322,6 +329,11 @@ jobs:
322
329
  manylinux: manylinux_2_39
323
330
  python: '3.13'
324
331
  artifact_name: wheels-ubuntu-24.04-x86_64-py3.13
332
+ - os: ubuntu-24.04
333
+ target: x86_64
334
+ manylinux: manylinux_2_39
335
+ python: '3.14'
336
+ artifact_name: wheels-ubuntu-24.04-x86_64-py3.14
325
337
  # Linux aarch64 (DGX Spark / GB10)
326
338
  - os: ubuntu-24.04-arm
327
339
  target: aarch64
@@ -338,6 +350,11 @@ jobs:
338
350
  manylinux: manylinux_2_39
339
351
  python: '3.13'
340
352
  artifact_name: wheels-linux-aarch64-py3.13
353
+ - os: ubuntu-24.04-arm
354
+ target: aarch64
355
+ manylinux: manylinux_2_39
356
+ python: '3.14'
357
+ artifact_name: wheels-linux-aarch64-py3.14
341
358
  # macOS aarch64
342
359
  - os: macos-latest
343
360
  target: aarch64
@@ -354,6 +371,11 @@ jobs:
354
371
  manylinux: ''
355
372
  python: '3.13'
356
373
  artifact_name: wheels-macos-latest-aarch64-py3.13
374
+ - os: macos-latest
375
+ target: aarch64
376
+ manylinux: ''
377
+ python: '3.14'
378
+ artifact_name: wheels-macos-latest-aarch64-py3.14
357
379
  steps:
358
380
  - uses: actions/download-artifact@v4
359
381
  with:
@@ -224,9 +224,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
224
224
 
225
225
  [[package]]
226
226
  name = "cc"
227
- version = "1.4.2"
227
+ version = "1.4.4"
228
228
  source = "registry+https://github.com/rust-lang/crates.io-index"
229
- checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
229
+ checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273"
230
230
  dependencies = [
231
231
  "find-msvc-tools",
232
232
  "jobserver",
@@ -348,9 +348,9 @@ dependencies = [
348
348
 
349
349
  [[package]]
350
350
  name = "crc32fast"
351
- version = "1.5.0"
351
+ version = "1.5.1"
352
352
  source = "registry+https://github.com/rust-lang/crates.io-index"
353
- checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
353
+ checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550"
354
354
  dependencies = [
355
355
  "cfg-if",
356
356
  ]
@@ -430,9 +430,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
430
430
 
431
431
  [[package]]
432
432
  name = "either"
433
- version = "1.17.0"
433
+ version = "1.18.0"
434
434
  source = "registry+https://github.com/rust-lang/crates.io-index"
435
- checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
435
+ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
436
436
 
437
437
  [[package]]
438
438
  name = "equator"
@@ -504,9 +504,9 @@ dependencies = [
504
504
 
505
505
  [[package]]
506
506
  name = "find-msvc-tools"
507
- version = "0.1.10"
507
+ version = "0.1.11"
508
508
  source = "registry+https://github.com/rust-lang/crates.io-index"
509
- checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
509
+ checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
510
510
 
511
511
  [[package]]
512
512
  name = "flate2"
@@ -536,7 +536,7 @@ checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225"
536
536
  dependencies = [
537
537
  "proc-macro2",
538
538
  "quote",
539
- "syn 3.0.3",
539
+ "syn 3.0.4",
540
540
  ]
541
541
 
542
542
  [[package]]
@@ -547,21 +547,21 @@ checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
547
547
 
548
548
  [[package]]
549
549
  name = "futures-core"
550
- version = "0.3.33"
550
+ version = "0.3.34"
551
551
  source = "registry+https://github.com/rust-lang/crates.io-index"
552
- checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
552
+ checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
553
553
 
554
554
  [[package]]
555
555
  name = "futures-task"
556
- version = "0.3.33"
556
+ version = "0.3.34"
557
557
  source = "registry+https://github.com/rust-lang/crates.io-index"
558
- checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
558
+ checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
559
559
 
560
560
  [[package]]
561
561
  name = "futures-util"
562
- version = "0.3.33"
562
+ version = "0.3.34"
563
563
  source = "registry+https://github.com/rust-lang/crates.io-index"
564
- checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
564
+ checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
565
565
  dependencies = [
566
566
  "futures-core",
567
567
  "futures-task",
@@ -833,9 +833,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
833
833
 
834
834
  [[package]]
835
835
  name = "log"
836
- version = "0.4.33"
836
+ version = "0.4.34"
837
837
  source = "registry+https://github.com/rust-lang/crates.io-index"
838
- checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
838
+ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
839
839
 
840
840
  [[package]]
841
841
  name = "loop9"
@@ -942,9 +942,9 @@ dependencies = [
942
942
 
943
943
  [[package]]
944
944
  name = "ndarray"
945
- version = "0.16.1"
945
+ version = "0.17.2"
946
946
  source = "registry+https://github.com/rust-lang/crates.io-index"
947
- checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841"
947
+ checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
948
948
  dependencies = [
949
949
  "matrixmultiply",
950
950
  "num-complex",
@@ -1032,9 +1032,9 @@ dependencies = [
1032
1032
 
1033
1033
  [[package]]
1034
1034
  name = "num-integer"
1035
- version = "0.1.46"
1035
+ version = "0.1.47"
1036
1036
  source = "registry+https://github.com/rust-lang/crates.io-index"
1037
- checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
1037
+ checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
1038
1038
  dependencies = [
1039
1039
  "num-traits",
1040
1040
  ]
@@ -1082,9 +1082,9 @@ dependencies = [
1082
1082
 
1083
1083
  [[package]]
1084
1084
  name = "numpy"
1085
- version = "0.22.1"
1085
+ version = "0.27.1"
1086
1086
  source = "registry+https://github.com/rust-lang/crates.io-index"
1087
- checksum = "edb929bc0da91a4d85ed6c0a84deaa53d411abfb387fc271124f91bf6b89f14e"
1087
+ checksum = "7aac2e6a6e4468ffa092ad43c39b81c79196c2bb773b8db4085f695efe3bba17"
1088
1088
  dependencies = [
1089
1089
  "libc",
1090
1090
  "ndarray",
@@ -1092,6 +1092,7 @@ dependencies = [
1092
1092
  "num-integer",
1093
1093
  "num-traits",
1094
1094
  "pyo3",
1095
+ "pyo3-build-config",
1095
1096
  "rustc-hash",
1096
1097
  ]
1097
1098
 
@@ -1196,9 +1197,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
1196
1197
 
1197
1198
  [[package]]
1198
1199
  name = "pkg-config"
1199
- version = "0.3.33"
1200
+ version = "0.3.34"
1200
1201
  source = "registry+https://github.com/rust-lang/crates.io-index"
1201
- checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
1202
+ checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
1202
1203
 
1203
1204
  [[package]]
1204
1205
  name = "plotters"
@@ -1324,11 +1325,10 @@ checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
1324
1325
 
1325
1326
  [[package]]
1326
1327
  name = "pyo3"
1327
- version = "0.22.6"
1328
+ version = "0.27.2"
1328
1329
  source = "registry+https://github.com/rust-lang/crates.io-index"
1329
- checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884"
1330
+ checksum = "ab53c047fcd1a1d2a8820fe84f05d6be69e9526be40cb03b73f86b6b03e6d87d"
1330
1331
  dependencies = [
1331
- "cfg-if",
1332
1332
  "indoc",
1333
1333
  "libc",
1334
1334
  "memoffset",
@@ -1342,19 +1342,18 @@ dependencies = [
1342
1342
 
1343
1343
  [[package]]
1344
1344
  name = "pyo3-build-config"
1345
- version = "0.22.6"
1345
+ version = "0.27.2"
1346
1346
  source = "registry+https://github.com/rust-lang/crates.io-index"
1347
- checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38"
1347
+ checksum = "b455933107de8642b4487ed26d912c2d899dec6114884214a0b3bb3be9261ea6"
1348
1348
  dependencies = [
1349
- "once_cell",
1350
1349
  "target-lexicon",
1351
1350
  ]
1352
1351
 
1353
1352
  [[package]]
1354
1353
  name = "pyo3-ffi"
1355
- version = "0.22.6"
1354
+ version = "0.27.2"
1356
1355
  source = "registry+https://github.com/rust-lang/crates.io-index"
1357
- checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636"
1356
+ checksum = "1c85c9cbfaddf651b1221594209aed57e9e5cff63c4d11d1feead529b872a089"
1358
1357
  dependencies = [
1359
1358
  "libc",
1360
1359
  "pyo3-build-config",
@@ -1362,9 +1361,9 @@ dependencies = [
1362
1361
 
1363
1362
  [[package]]
1364
1363
  name = "pyo3-macros"
1365
- version = "0.22.6"
1364
+ version = "0.27.2"
1366
1365
  source = "registry+https://github.com/rust-lang/crates.io-index"
1367
- checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453"
1366
+ checksum = "0a5b10c9bf9888125d917fb4d2ca2d25c8df94c7ab5a52e13313a07e050a3b02"
1368
1367
  dependencies = [
1369
1368
  "proc-macro2",
1370
1369
  "pyo3-macros-backend",
@@ -1374,9 +1373,9 @@ dependencies = [
1374
1373
 
1375
1374
  [[package]]
1376
1375
  name = "pyo3-macros-backend"
1377
- version = "0.22.6"
1376
+ version = "0.27.2"
1378
1377
  source = "registry+https://github.com/rust-lang/crates.io-index"
1379
- checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe"
1378
+ checksum = "03b51720d314836e53327f5871d4c0cfb4fb37cc2c4a11cc71907a86342c40f9"
1380
1379
  dependencies = [
1381
1380
  "heck",
1382
1381
  "proc-macro2",
@@ -1618,9 +1617,9 @@ checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
1618
1617
 
1619
1618
  [[package]]
1620
1619
  name = "rustc-hash"
1621
- version = "1.1.0"
1620
+ version = "2.1.3"
1622
1621
  source = "registry+https://github.com/rust-lang/crates.io-index"
1623
- checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
1622
+ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
1624
1623
 
1625
1624
  [[package]]
1626
1625
  name = "rustix"
@@ -1692,7 +1691,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
1692
1691
  dependencies = [
1693
1692
  "proc-macro2",
1694
1693
  "quote",
1695
- "syn 3.0.3",
1694
+ "syn 3.0.4",
1696
1695
  ]
1697
1696
 
1698
1697
  [[package]]
@@ -1773,9 +1772,9 @@ dependencies = [
1773
1772
 
1774
1773
  [[package]]
1775
1774
  name = "syn"
1776
- version = "3.0.3"
1775
+ version = "3.0.4"
1777
1776
  source = "registry+https://github.com/rust-lang/crates.io-index"
1778
- checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
1777
+ checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f"
1779
1778
  dependencies = [
1780
1779
  "proc-macro2",
1781
1780
  "quote",
@@ -1784,9 +1783,9 @@ dependencies = [
1784
1783
 
1785
1784
  [[package]]
1786
1785
  name = "target-lexicon"
1787
- version = "0.12.16"
1786
+ version = "0.13.5"
1788
1787
  source = "registry+https://github.com/rust-lang/crates.io-index"
1789
- checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
1788
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
1790
1789
 
1791
1790
  [[package]]
1792
1791
  name = "tempfile"
@@ -1818,7 +1817,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
1818
1817
  dependencies = [
1819
1818
  "proc-macro2",
1820
1819
  "quote",
1821
- "syn 3.0.3",
1820
+ "syn 3.0.4",
1822
1821
  ]
1823
1822
 
1824
1823
  [[package]]
@@ -1847,7 +1846,7 @@ dependencies = [
1847
1846
 
1848
1847
  [[package]]
1849
1848
  name = "trainingsample"
1850
- version = "0.3.1"
1849
+ version = "0.3.2"
1851
1850
  dependencies = [
1852
1851
  "anyhow",
1853
1852
  "criterion",
@@ -5,15 +5,15 @@ image = {version = "0.25", features = ["png", "jpeg", "webp"]}
5
5
  imageproc = "0.25"
6
6
  # Metal GPU acceleration
7
7
  metal = {version = "0.27", optional = true}
8
- ndarray = "0.16"
8
+ ndarray = "0.17"
9
9
  num_cpus = "1.16"
10
- numpy = {version = "0.22", optional = true}
10
+ numpy = {version = "0.27", optional = true}
11
11
  objc = {version = "0.2", optional = true}
12
12
  # OpenCV bindings for performance parity
13
13
  # Limit binding generation to the modules this crate actually uses.
14
14
  opencv = {version = "0.99", optional = true, default-features = false, features = ["clang-runtime", "imgproc", "videoio"]}
15
15
  # Python bindings (optional)
16
- pyo3 = {version = "0.22", features = ["extension-module"], optional = true}
16
+ pyo3 = {version = "0.27", features = ["extension-module"], optional = true}
17
17
  rayon = "1.8"
18
18
  tempfile = {version = "3.10", optional = true}
19
19
  # SIMD optimizations
@@ -39,7 +39,7 @@ description = "High-performance Rust reimplementation of GIL-blocking video/imag
39
39
  edition = "2021"
40
40
  license = "MIT"
41
41
  name = "trainingsample"
42
- version = "0.3.1"
42
+ version = "0.3.2"
43
43
  readme = "README.md"
44
44
 
45
45
  [profile.release]
@@ -1,17 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trainingsample
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: Implementation :: CPython
6
6
  Classifier: Programming Language :: Python :: Implementation :: PyPy
7
7
  Classifier: Programming Language :: Python :: 3.11
8
8
  Classifier: Programming Language :: Python :: 3.12
9
9
  Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
10
11
  Requires-Dist: numpy>=1.21.0
11
12
  Requires-Dist: pillow>=8.0.0
12
13
  Requires-Dist: pytest>=6.0 ; extra == 'dev'
13
14
  Requires-Dist: pytest-benchmark ; extra == 'dev'
14
- Requires-Dist: maturin>=1.0,<2.0 ; extra == 'dev'
15
+ Requires-Dist: maturin>=1.9.5,<2.0 ; extra == 'dev'
15
16
  Requires-Dist: pre-commit ; extra == 'dev'
16
17
  Provides-Extra: dev
17
18
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [build-system]
2
2
  build-backend = "maturin"
3
- requires = ["maturin>=1.0,<2.0"]
3
+ requires = ["maturin>=1.9.5,<2.0"]
4
4
 
5
5
  [project]
6
6
  authors = [{name = "bghira", email = "bghira@users.noreply.github.com"}]
@@ -10,17 +10,18 @@ classifiers = [
10
10
  "Programming Language :: Python :: Implementation :: PyPy",
11
11
  "Programming Language :: Python :: 3.11",
12
12
  "Programming Language :: Python :: 3.12",
13
- "Programming Language :: Python :: 3.13"
13
+ "Programming Language :: Python :: 3.13",
14
+ "Programming Language :: Python :: 3.14"
14
15
  ]
15
16
  dependencies = ["numpy>=1.21.0", "pillow>=8.0.0"]
16
17
  description = "High-performance Rust reimplementation of GIL-blocking video/image operations"
17
18
  license = {text = "MIT"}
18
19
  name = "trainingsample"
19
20
  requires-python = ">=3.11"
20
- version = "0.3.1"
21
+ version = "0.3.2"
21
22
 
22
23
  [project.optional-dependencies]
23
- dev = ["pytest>=6.0", "pytest-benchmark", "maturin>=1.0,<2.0", "pre-commit"]
24
+ dev = ["pytest>=6.0", "pytest-benchmark", "maturin>=1.9.5,<2.0", "pre-commit"]
24
25
 
25
26
  [tool.black]
26
27
  extend-exclude = '''
@@ -88,7 +88,7 @@ impl BufferPool {
88
88
 
89
89
  #[cfg(feature = "python-bindings")]
90
90
  #[pyfunction]
91
- pub fn load_image_batch(py: Python, image_paths: Vec<String>) -> PyResult<Vec<PyObject>> {
91
+ pub fn load_image_batch(py: Python, image_paths: Vec<String>) -> PyResult<Vec<Py<PyAny>>> {
92
92
  use rayon::prelude::*;
93
93
 
94
94
  let results: Vec<_> = image_paths
@@ -100,7 +100,7 @@ pub fn load_image_batch(py: Python, image_paths: Vec<String>) -> PyResult<Vec<Py
100
100
  for result in results {
101
101
  match result {
102
102
  Ok(image_data) => {
103
- let py_bytes = PyBytes::new_bound(py, &image_data);
103
+ let py_bytes = PyBytes::new(py, &image_data);
104
104
  py_results.push(py_bytes.into_any().unbind());
105
105
  }
106
106
  Err(_) => {
@@ -155,7 +155,7 @@ pub unsafe fn batch_crop_images_zero_copy<'py>(
155
155
 
156
156
  let array = ndarray::Array3::from_shape_vec((height, width, channels), output_buffer)
157
157
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Shape error: {}", e)))?;
158
- let py_array = PyArray3::from_owned_array_bound(py, array);
158
+ let py_array = PyArray3::from_owned_array(py, array);
159
159
  py_results.push(py_array);
160
160
  }
161
161
 
@@ -206,7 +206,7 @@ pub unsafe fn batch_center_crop_images_zero_copy<'py>(
206
206
  .map_err(|e| {
207
207
  pyo3::exceptions::PyValueError::new_err(format!("Shape error: {}", e))
208
208
  })?;
209
- let py_array = PyArray3::from_owned_array_bound(py, array);
209
+ let py_array = PyArray3::from_owned_array(py, array);
210
210
  py_results.push(py_array);
211
211
  }
212
212
 
@@ -390,10 +390,10 @@ pub fn batch_resize_images_zero_copy<'py>(
390
390
  // Convert to PyArray3 and return as Python list
391
391
  let py_results: Vec<Bound<'py, PyArray3<u8>>> = results
392
392
  .into_iter()
393
- .map(|array| PyArray3::from_owned_array_bound(py, array))
393
+ .map(|array| PyArray3::from_owned_array(py, array))
394
394
  .collect();
395
395
 
396
- Ok(PyList::new_bound(py, py_results).into_any())
396
+ Ok(PyList::new(py, py_results)?.into_any())
397
397
  }
398
398
 
399
399
  /// Ultra-fast OpenCV-powered resize using zero-copy Mat headers
@@ -552,7 +552,7 @@ fn resize_single_image_direct<'py>(
552
552
  })?;
553
553
 
554
554
  // DIRECT return - no Vec wrapper overhead!
555
- Ok(PyArray3::from_owned_array_bound(py, result))
555
+ Ok(PyArray3::from_owned_array(py, result))
556
556
  }
557
557
 
558
558
  #[cfg(not(feature = "opencv"))]
@@ -578,12 +578,12 @@ fn is_tuple_or_list_of_two(obj: &Bound<PyAny>) -> bool {
578
578
  use pyo3::types::{PyList, PyTuple};
579
579
 
580
580
  // Check if it's a tuple of length 2
581
- if let Ok(tuple) = obj.downcast::<PyTuple>() {
581
+ if let Ok(tuple) = obj.cast::<PyTuple>() {
582
582
  return tuple.len() == 2;
583
583
  }
584
584
 
585
585
  // Check if it's a list of length 2 containing numbers (not nested lists)
586
- if let Ok(list) = obj.downcast::<PyList>() {
586
+ if let Ok(list) = obj.cast::<PyList>() {
587
587
  if list.len() == 2 {
588
588
  // Check if first element is a number (not a nested list/tuple)
589
589
  if let Ok(first) = list.get_item(0) {
@@ -616,7 +616,7 @@ impl ResizeIterator {
616
616
  // Transfer the completed allocation directly into the ndarray instead
617
617
  // of cloning the full resized image during iteration.
618
618
  match ndarray::Array3::from_shape_vec((height, width, channels), buffer) {
619
- Ok(array) => Some(PyArray3::from_owned_array_bound(py, array)),
619
+ Ok(array) => Some(PyArray3::from_owned_array(py, array)),
620
620
  Err(_) => None, // Malformed buffer; end iteration (StopIteration)
621
621
  }
622
622
  }
@@ -872,7 +872,7 @@ pub fn batch_crop_images<'py>(
872
872
  let img_view = image.as_array();
873
873
  match crop_image_array(&img_view, x, y, width, height) {
874
874
  Ok(cropped) => {
875
- let py_array = PyArray3::from_owned_array_bound(py, cropped);
875
+ let py_array = PyArray3::from_owned_array(py, cropped);
876
876
  py_results.push(py_array);
877
877
  }
878
878
  Err(e) => {
@@ -905,7 +905,7 @@ pub fn batch_center_crop_images<'py>(
905
905
  let img_view = image.as_array();
906
906
  match crate::cropping::center_crop_image_array(&img_view, target_width, target_height) {
907
907
  Ok(cropped) => {
908
- let py_array = PyArray3::from_owned_array_bound(py, cropped);
908
+ let py_array = PyArray3::from_owned_array(py, cropped);
909
909
  py_results.push(py_array);
910
910
  }
911
911
  Err(e) => {
@@ -932,7 +932,7 @@ pub fn batch_random_crop_images<'py>(
932
932
  let img_view = image.as_array();
933
933
  match random_crop_image_array(&img_view, target_width, target_height) {
934
934
  Ok(cropped) => {
935
- let py_array = PyArray3::from_owned_array_bound(py, cropped);
935
+ let py_array = PyArray3::from_owned_array(py, cropped);
936
936
  py_results.push(py_array);
937
937
  }
938
938
  Err(e) => {
@@ -958,7 +958,7 @@ pub fn batch_calculate_luminance(
958
958
 
959
959
  let image_views: Vec<_> = images.iter().map(|image| image.as_array()).collect();
960
960
 
961
- Ok(py.allow_threads(|| {
961
+ Ok(py.detach(|| {
962
962
  image_views
963
963
  .par_iter()
964
964
  .map(crate::luminance::calculate_luminance_array_sequential)
@@ -984,7 +984,7 @@ pub fn rgb_to_rgba_optimized<'py>(
984
984
  let rgba_array = ndarray::Array3::from_shape_vec((height, width, 4), rgba_data)
985
985
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Shape error: {}", e)))?;
986
986
 
987
- let py_array = PyArray3::from_owned_array_bound(py, rgba_array);
987
+ let py_array = PyArray3::from_owned_array(py, rgba_array);
988
988
  Ok((py_array, metrics.throughput_mpixels_per_sec))
989
989
  }
990
990
 
@@ -1013,7 +1013,7 @@ pub fn rgba_to_rgb_optimized<'py>(
1013
1013
  let rgb_array = ndarray::Array3::from_shape_vec((height, width, 3), rgb_data)
1014
1014
  .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Shape error: {}", e)))?;
1015
1015
 
1016
- let py_array = PyArray3::from_owned_array_bound(py, rgb_array);
1016
+ let py_array = PyArray3::from_owned_array(py, rgb_array);
1017
1017
  Ok((py_array, metrics.throughput_mpixels_per_sec))
1018
1018
  }
1019
1019
 
@@ -1057,7 +1057,7 @@ pub fn batch_resize_images<'py>(
1057
1057
  Ok(resized_images) => {
1058
1058
  let py_results: Vec<_> = resized_images
1059
1059
  .into_iter()
1060
- .map(|resized| PyArray3::from_owned_array_bound(py, resized))
1060
+ .map(|resized| PyArray3::from_owned_array(py, resized))
1061
1061
  .collect();
1062
1062
  Ok(py_results)
1063
1063
  }
@@ -1095,7 +1095,7 @@ pub fn batch_resize_videos<'py>(
1095
1095
  Ok(resized_videos) => {
1096
1096
  let py_results: Vec<_> = resized_videos
1097
1097
  .into_iter()
1098
- .map(|resized| PyArray4::from_owned_array_bound(py, resized))
1098
+ .map(|resized| PyArray4::from_owned_array(py, resized))
1099
1099
  .collect();
1100
1100
  Ok(py_results)
1101
1101
  }
@@ -1134,7 +1134,7 @@ pub fn resize_bilinear_opencv<'py>(
1134
1134
 
1135
1135
  match resize_bilinear_opencv(&image_array, target_width, target_height) {
1136
1136
  Ok(resized) => {
1137
- let py_array = PyArray3::from_owned_array_bound(py, resized);
1137
+ let py_array = PyArray3::from_owned_array(py, resized);
1138
1138
  Ok(py_array)
1139
1139
  }
1140
1140
  Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
@@ -1158,7 +1158,7 @@ pub fn resize_lanczos4_opencv<'py>(
1158
1158
 
1159
1159
  match resize_lanczos4_opencv(&image_array, target_width, target_height) {
1160
1160
  Ok(resized) => {
1161
- let py_array = PyArray3::from_owned_array_bound(py, resized);
1161
+ let py_array = PyArray3::from_owned_array(py, resized);
1162
1162
  Ok(py_array)
1163
1163
  }
1164
1164
  Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
@@ -1218,11 +1218,11 @@ pub fn imdecode_py<'py>(
1218
1218
  }
1219
1219
  };
1220
1220
 
1221
- let decoded = py.allow_threads(move || imdecode(&buf, imread_flags));
1221
+ let decoded = py.detach(move || imdecode(&buf, imread_flags));
1222
1222
 
1223
1223
  match decoded {
1224
1224
  Ok(image) => {
1225
- let py_array = PyArray3::from_owned_array_bound(py, image);
1225
+ let py_array = PyArray3::from_owned_array(py, image);
1226
1226
  Ok(py_array)
1227
1227
  }
1228
1228
  Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
@@ -1258,7 +1258,7 @@ pub fn cvt_color_py<'py>(
1258
1258
  let src_array = src.as_array();
1259
1259
  match cvt_color(&src_array, color_code) {
1260
1260
  Ok(converted) => {
1261
- let py_array = PyArray3::from_owned_array_bound(py, converted);
1261
+ let py_array = PyArray3::from_owned_array(py, converted);
1262
1262
  Ok(py_array)
1263
1263
  }
1264
1264
  Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
@@ -1281,7 +1281,7 @@ pub fn canny_py<'py>(
1281
1281
  let image_array = image.as_array();
1282
1282
  match canny(&image_array, threshold1, threshold2) {
1283
1283
  Ok(edges) => {
1284
- let py_array = PyArray3::from_owned_array_bound(py, edges);
1284
+ let py_array = PyArray3::from_owned_array(py, edges);
1285
1285
  Ok(py_array)
1286
1286
  }
1287
1287
  Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
@@ -1317,7 +1317,7 @@ pub fn resize_py<'py>(
1317
1317
  let src_array = src.as_array();
1318
1318
  match resize(&src_array, dsize, interp) {
1319
1319
  Ok(resized) => {
1320
- let py_array = PyArray3::from_owned_array_bound(py, resized);
1320
+ let py_array = PyArray3::from_owned_array(py, resized);
1321
1321
  Ok(py_array)
1322
1322
  }
1323
1323
  Err(e) => Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
@@ -1328,7 +1328,7 @@ pub fn resize_py<'py>(
1328
1328
  }
1329
1329
 
1330
1330
  #[cfg(feature = "python-bindings")]
1331
- #[pyclass]
1331
+ #[pyclass(unsendable)]
1332
1332
  pub struct PyVideoCapture {
1333
1333
  inner: VideoCapture,
1334
1334
  temp_path: Option<TempPath>,
@@ -1419,7 +1419,7 @@ impl PyVideoCapture {
1419
1419
  fn read<'py>(&mut self, py: Python<'py>) -> PyResult<(bool, Option<Bound<'py, PyArray3<u8>>>)> {
1420
1420
  let (ret, frame) = self.inner.read();
1421
1421
  if let Some(frame_data) = frame {
1422
- let py_array = PyArray3::from_owned_array_bound(py, frame_data);
1422
+ let py_array = PyArray3::from_owned_array(py, frame_data);
1423
1423
  Ok((ret, Some(py_array)))
1424
1424
  } else {
1425
1425
  Ok((ret, None))
@@ -1639,7 +1639,7 @@ impl PyBatchProcessor {
1639
1639
  Ok(results) => {
1640
1640
  let py_results: Vec<_> = results
1641
1641
  .into_iter()
1642
- .map(|result| PyArray3::from_owned_array_bound(py, result))
1642
+ .map(|result| PyArray3::from_owned_array(py, result))
1643
1643
  .collect();
1644
1644
  Ok(py_results)
1645
1645
  }
@@ -1679,7 +1679,7 @@ impl PyBatchProcessor {
1679
1679
  Ok(results) => {
1680
1680
  let py_results: Vec<_> = results
1681
1681
  .into_iter()
1682
- .map(|result| PyArray3::from_owned_array_bound(py, result))
1682
+ .map(|result| PyArray3::from_owned_array(py, result))
1683
1683
  .collect();
1684
1684
  Ok(py_results)
1685
1685
  }
@@ -1704,7 +1704,7 @@ impl PyBatchProcessor {
1704
1704
  Ok(results) => {
1705
1705
  let py_results: Vec<_> = results
1706
1706
  .into_iter()
1707
- .map(|result| PyArray3::from_owned_array_bound(py, result))
1707
+ .map(|result| PyArray3::from_owned_array(py, result))
1708
1708
  .collect();
1709
1709
  Ok(py_results)
1710
1710
  }
@@ -1782,7 +1782,7 @@ impl PyBatchProcessor {
1782
1782
  Ok(results) => {
1783
1783
  let py_results: Vec<_> = results
1784
1784
  .into_iter()
1785
- .map(|result| PyArray3::from_owned_array_bound(py, result))
1785
+ .map(|result| PyArray3::from_owned_array(py, result))
1786
1786
  .collect();
1787
1787
  Ok(py_results)
1788
1788
  }
@@ -1903,7 +1903,7 @@ impl PyTrueBatchProcessor {
1903
1903
  Ok(results) => {
1904
1904
  let py_results: Vec<_> = results
1905
1905
  .into_iter()
1906
- .map(|result| PyArray3::from_owned_array_bound(py, result))
1906
+ .map(|result| PyArray3::from_owned_array(py, result))
1907
1907
  .collect();
1908
1908
  Ok(py_results)
1909
1909
  }
@@ -1939,7 +1939,7 @@ impl PyTrueBatchProcessor {
1939
1939
  Ok(results) => {
1940
1940
  let py_results: Vec<_> = results
1941
1941
  .into_iter()
1942
- .map(|result| PyArray3::from_owned_array_bound(py, result))
1942
+ .map(|result| PyArray3::from_owned_array(py, result))
1943
1943
  .collect();
1944
1944
  Ok(py_results)
1945
1945
  }
@@ -0,0 +1,427 @@
1
+ use image::{DynamicImage, ImageFormat, RgbImage};
2
+ use ndarray::{Array3, Array4};
3
+ use std::io::Cursor;
4
+ use std::path::PathBuf;
5
+ use std::time::{SystemTime, UNIX_EPOCH};
6
+ use trainingsample::{
7
+ batch_load_images, fourcc, load_and_decode_image, load_image_from_path, BatchProcessor,
8
+ CascadeClassifier, ColorConversion, ColorConversionCode, ImreadFlags, OptimizedBatchProcessor,
9
+ ResizeInterpolation, TrueBatchProcessor, VideoCapture, VideoCaptureProperties, VideoOperation,
10
+ VideoWriter,
11
+ };
12
+
13
+ #[cfg(feature = "simd")]
14
+ use trainingsample::{
15
+ rgb_to_rgba_optimized, rgb_to_rgba_scalar, rgba_to_rgb_optimized, rgba_to_rgb_scalar,
16
+ FormatConversionMetrics,
17
+ };
18
+
19
+ fn sample_rgb(height: usize, width: usize) -> Array3<u8> {
20
+ Array3::from_shape_fn((height, width, 3), |(y, x, channel)| {
21
+ ((y * 37 + x * 19 + channel * 71) % 256) as u8
22
+ })
23
+ }
24
+
25
+ fn encode_png(image: &Array3<u8>) -> Vec<u8> {
26
+ let (height, width, channels) = image.dim();
27
+ assert_eq!(channels, 3);
28
+
29
+ let rgb = RgbImage::from_raw(width as u32, height as u32, image.iter().copied().collect())
30
+ .expect("test image dimensions should match its data");
31
+ let mut encoded = Cursor::new(Vec::new());
32
+ DynamicImage::ImageRgb8(rgb)
33
+ .write_to(&mut encoded, ImageFormat::Png)
34
+ .expect("test image should encode");
35
+ encoded.into_inner()
36
+ }
37
+
38
+ fn temporary_path(extension: &str) -> PathBuf {
39
+ let nonce = SystemTime::now()
40
+ .duration_since(UNIX_EPOCH)
41
+ .expect("system clock should follow the Unix epoch")
42
+ .as_nanos();
43
+ std::env::temp_dir().join(format!(
44
+ "trainingsample-coverage-{}-{nonce}.{extension}",
45
+ std::process::id()
46
+ ))
47
+ }
48
+
49
+ struct TemporaryImage(PathBuf);
50
+
51
+ impl TemporaryImage {
52
+ fn create(contents: &[u8]) -> Self {
53
+ let path = temporary_path("png");
54
+ std::fs::write(&path, contents).expect("temporary image should be writable");
55
+ Self(path)
56
+ }
57
+ }
58
+
59
+ impl Drop for TemporaryImage {
60
+ fn drop(&mut self) {
61
+ let _ = std::fs::remove_file(&self.0);
62
+ }
63
+ }
64
+
65
+ #[test]
66
+ #[cfg(feature = "simd")]
67
+ fn format_conversion_implementations_preserve_pixels() {
68
+ let image = sample_rgb(2, 8);
69
+
70
+ let (scalar_rgba, scalar_metrics) = rgb_to_rgba_scalar(&image.view(), 173);
71
+ let (optimized_rgba, optimized_metrics) = rgb_to_rgba_optimized(&image.view(), 173);
72
+ assert_eq!(optimized_rgba, scalar_rgba);
73
+ assert!(scalar_rgba.chunks_exact(4).all(|pixel| pixel[3] == 173));
74
+ assert_eq!(scalar_metrics.pixels_processed, 16);
75
+ assert_eq!(scalar_metrics.simd_width, 1);
76
+ assert_eq!(scalar_metrics.implementation, "scalar_rgb_to_rgba");
77
+ assert_eq!(optimized_metrics.pixels_processed, 16);
78
+ assert_eq!(optimized_metrics.simd_width, 8);
79
+ assert_eq!(optimized_metrics.implementation, "simd_rgb_to_rgba");
80
+
81
+ let (scalar_rgb, _) = rgba_to_rgb_scalar(&scalar_rgba, 8, 2);
82
+ let (optimized_rgb, optimized_reverse_metrics) = rgba_to_rgb_optimized(&scalar_rgba, 8, 2);
83
+ assert_eq!(optimized_rgb, scalar_rgb);
84
+ assert_eq!(scalar_rgb, image.iter().copied().collect::<Vec<_>>());
85
+ assert_eq!(optimized_reverse_metrics.pixels_processed, 16);
86
+
87
+ let metrics = FormatConversionMetrics::new(1_000_000, 1_000_000_000, 4, "test");
88
+ assert_eq!(metrics.throughput_mpixels_per_sec, 1.0);
89
+ assert_eq!(metrics.simd_width, 4);
90
+ }
91
+
92
+ #[test]
93
+ fn loading_functions_return_raw_and_decoded_images() {
94
+ let image = sample_rgb(3, 5);
95
+ let encoded = encode_png(&image);
96
+ let temporary = TemporaryImage::create(&encoded);
97
+ let path = temporary
98
+ .0
99
+ .to_str()
100
+ .expect("temporary test path should be valid UTF-8");
101
+
102
+ assert_eq!(load_image_from_path(path).unwrap(), encoded);
103
+ assert_eq!(
104
+ load_and_decode_image(path).unwrap().to_rgb8().dimensions(),
105
+ (5, 3)
106
+ );
107
+
108
+ let missing = temporary.0.with_extension("missing");
109
+ let results = batch_load_images(&[temporary.0.as_path(), missing.as_path()]);
110
+ assert_eq!(results.len(), 2);
111
+ assert_eq!(results[0].as_ref().unwrap(), &encoded);
112
+ assert!(results[1].is_err());
113
+ }
114
+
115
+ #[test]
116
+ fn batch_processor_covers_sequential_pipelines_and_validation() {
117
+ let processor = BatchProcessor::with_config(false, 2);
118
+ let image = sample_rgb(8, 8);
119
+ let second = sample_rgb(6, 10);
120
+ let views = [image.view(), second.view()];
121
+ let encoded = encode_png(&image);
122
+
123
+ let decoded = processor.batch_imdecode(&[encoded.as_slice()], ImreadFlags::ImreadColor);
124
+ assert_eq!(decoded[0].as_ref().unwrap().dim(), image.dim());
125
+ assert!(processor.batch_imdecode(&[b"not an image"], ImreadFlags::ImreadColor)[0].is_err());
126
+
127
+ assert!(processor
128
+ .batch_cvt_color(&[], ColorConversionCode::ColorBgr2Rgb)
129
+ .unwrap()
130
+ .is_empty());
131
+ let swapped = processor
132
+ .batch_cvt_color(&views, ColorConversionCode::ColorBgr2Rgb)
133
+ .unwrap();
134
+ assert_eq!(swapped[0][[0, 0, 0]], image[[0, 0, 2]]);
135
+ assert_eq!(swapped[0][[0, 0, 2]], image[[0, 0, 0]]);
136
+
137
+ assert!(processor
138
+ .batch_resize(&views, &[(4, 4)], ResizeInterpolation::InterLinear)
139
+ .is_err());
140
+ assert!(processor
141
+ .batch_resize(&[], &[], ResizeInterpolation::InterLinear)
142
+ .unwrap()
143
+ .is_empty());
144
+ let resized = processor
145
+ .batch_resize(&views, &[(4, 4), (5, 3)], ResizeInterpolation::InterNearest)
146
+ .unwrap();
147
+ assert_eq!(resized[0].dim(), (4, 4, 3));
148
+ assert_eq!(resized[1].dim(), (3, 5, 3));
149
+
150
+ assert!(processor.batch_canny(&[], 25.0, 50.0).unwrap().is_empty());
151
+ let edges = processor.batch_canny(&views, 25.0, 50.0).unwrap();
152
+ assert_eq!(edges[0].dim(), (8, 8, 1));
153
+ assert_eq!(edges[1].dim(), (6, 10, 1));
154
+
155
+ assert!(processor
156
+ .batch_preprocess_pipeline(
157
+ &[encoded.as_slice()],
158
+ &[],
159
+ None,
160
+ ImreadFlags::ImreadColor,
161
+ ResizeInterpolation::InterLinear,
162
+ )
163
+ .is_err());
164
+ let preprocessed = processor
165
+ .batch_preprocess_pipeline(
166
+ &[encoded.as_slice()],
167
+ &[(5, 5)],
168
+ Some(ColorConversionCode::ColorRgb2Gray),
169
+ ImreadFlags::ImreadColor,
170
+ ResizeInterpolation::InterCubic,
171
+ )
172
+ .unwrap();
173
+ assert_eq!(preprocessed[0].dim(), (5, 5, 1));
174
+
175
+ let detections = processor
176
+ .batch_face_detection_pipeline(&views, 1.1, 3)
177
+ .unwrap();
178
+ assert_eq!(detections, vec![Vec::new(), Vec::new()]);
179
+ }
180
+
181
+ #[test]
182
+ fn batch_processor_covers_parallel_and_video_paths() {
183
+ let processor = BatchProcessor::with_config(true, 1);
184
+ let images = [sample_rgb(5, 6), sample_rgb(5, 6), sample_rgb(5, 6)];
185
+ let views: Vec<_> = images.iter().map(|image| image.view()).collect();
186
+ let buffers: Vec<_> = images.iter().map(encode_png).collect();
187
+ let buffer_views: Vec<_> = buffers.iter().map(Vec::as_slice).collect();
188
+
189
+ assert_eq!(
190
+ processor
191
+ .batch_imdecode(&buffer_views, ImreadFlags::ImreadColor)
192
+ .len(),
193
+ 3
194
+ );
195
+ assert_eq!(
196
+ processor
197
+ .batch_cvt_color(&views, ColorConversionCode::ColorRgb2Gray)
198
+ .unwrap()
199
+ .len(),
200
+ 3
201
+ );
202
+ let resized = processor
203
+ .batch_resize(
204
+ &views,
205
+ &[(12, 10), (6, 5), (12, 10)],
206
+ ResizeInterpolation::InterLanczos4,
207
+ )
208
+ .unwrap();
209
+ assert_eq!(resized[0].dim(), (10, 12, 3));
210
+ assert_eq!(resized[1].dim(), (5, 6, 3));
211
+ assert_eq!(processor.batch_canny(&views, 10.0, 20.0).unwrap().len(), 3);
212
+
213
+ let video = Array4::from_shape_fn((2, 5, 6, 3), |(frame, y, x, channel)| {
214
+ ((frame * 43 + y * 17 + x * 11 + channel * 67) % 256) as u8
215
+ });
216
+ let processed = processor
217
+ .batch_video_frame_processing(
218
+ &[video.view()],
219
+ (12, 10),
220
+ &[
221
+ VideoOperation::Resize(ResizeInterpolation::InterLinear),
222
+ VideoOperation::ColorConvert(ColorConversionCode::ColorBgr2Rgb),
223
+ VideoOperation::EdgeDetection(10.0, 20.0),
224
+ ],
225
+ )
226
+ .unwrap();
227
+ assert_eq!(processed[0].dim(), (2, 10, 12, 1));
228
+
229
+ let empty_video = Array4::<u8>::zeros((0, 5, 6, 3));
230
+ let empty_processed = processor
231
+ .batch_video_frame_processing(&[empty_video.view()], (4, 3), &[])
232
+ .unwrap();
233
+ assert_eq!(empty_processed[0].dim(), empty_video.dim());
234
+ }
235
+
236
+ #[test]
237
+ fn optimized_batch_processor_covers_conversion_and_resize_paths() {
238
+ let sequential = OptimizedBatchProcessor {
239
+ use_parallel: false,
240
+ chunk_size: 8,
241
+ };
242
+ let image = sample_rgb(2, 4);
243
+ let views = [image.view()];
244
+
245
+ assert!(sequential
246
+ .batch_cvt_color_optimized(&[], ColorConversionCode::ColorBgr2Rgb)
247
+ .unwrap()
248
+ .is_empty());
249
+ let swapped = sequential
250
+ .batch_cvt_color_optimized(&views, ColorConversionCode::ColorBgr2Rgb)
251
+ .unwrap();
252
+ assert_eq!(swapped[0][[1, 3, 0]], image[[1, 3, 2]]);
253
+ assert_eq!(swapped[0][[1, 3, 2]], image[[1, 3, 0]]);
254
+
255
+ let grayscale = sequential
256
+ .batch_cvt_color_optimized(&views, ColorConversionCode::ColorRgb2Gray)
257
+ .unwrap();
258
+ assert_eq!(grayscale[0].dim(), (2, 4, 1));
259
+
260
+ let single_channel = Array3::from_shape_vec((1, 2, 1), vec![25, 200]).unwrap();
261
+ let expanded = sequential
262
+ .batch_cvt_color_optimized(&[single_channel.view()], ColorConversionCode::ColorGray2Rgb)
263
+ .unwrap();
264
+ assert_eq!(expanded[0].dim(), (1, 2, 3));
265
+ assert_eq!(expanded[0][[0, 1, 0]], 200);
266
+ assert!(sequential
267
+ .batch_cvt_color_optimized(&[single_channel.view()], ColorConversionCode::ColorRgb2Gray,)
268
+ .is_err());
269
+
270
+ assert!(sequential
271
+ .batch_resize_optimized(&views, &[], ResizeInterpolation::InterLinear)
272
+ .is_err());
273
+ let resized = sequential
274
+ .batch_resize_optimized(&views, &[(2, 1)], ResizeInterpolation::InterNearest)
275
+ .unwrap();
276
+ assert_eq!(resized[0].dim(), (1, 2, 3));
277
+
278
+ let parallel = OptimizedBatchProcessor {
279
+ use_parallel: true,
280
+ chunk_size: 1,
281
+ };
282
+ let parallel_images = [sample_rgb(4, 4), sample_rgb(4, 4), sample_rgb(4, 4)];
283
+ let parallel_views: Vec<_> = parallel_images.iter().map(|image| image.view()).collect();
284
+ assert_eq!(
285
+ parallel
286
+ .batch_cvt_color_optimized(&parallel_views, ColorConversionCode::ColorRgb2Gray)
287
+ .unwrap()
288
+ .len(),
289
+ 3
290
+ );
291
+ let parallel_resized = parallel
292
+ .batch_resize_optimized(
293
+ &parallel_views,
294
+ &[(2, 2), (3, 3), (2, 2)],
295
+ ResizeInterpolation::InterCubic,
296
+ )
297
+ .unwrap();
298
+ assert_eq!(parallel_resized[0].dim(), (2, 2, 3));
299
+ assert_eq!(parallel_resized[1].dim(), (3, 3, 3));
300
+ }
301
+
302
+ #[test]
303
+ fn true_batch_processor_covers_resize_color_and_luminance_paths() {
304
+ let sequential = TrueBatchProcessor {
305
+ use_parallel: false,
306
+ chunk_size: 8,
307
+ simd_threshold: 4,
308
+ };
309
+ let image = sample_rgb(4, 5);
310
+ let views = [image.view()];
311
+
312
+ assert!(sequential
313
+ .true_batch_resize(&[], &[], ResizeInterpolation::InterLinear)
314
+ .unwrap()
315
+ .is_empty());
316
+ #[cfg(feature = "opencv")]
317
+ assert!(sequential
318
+ .true_batch_resize(&views, &[], ResizeInterpolation::InterLinear)
319
+ .is_err());
320
+
321
+ for interpolation in [
322
+ ResizeInterpolation::InterNearest,
323
+ ResizeInterpolation::InterLinear,
324
+ ResizeInterpolation::InterCubic,
325
+ ResizeInterpolation::InterLanczos4,
326
+ ] {
327
+ let resized = sequential
328
+ .true_batch_resize(&views, &[(3, 2)], interpolation)
329
+ .unwrap();
330
+ assert_eq!(resized[0].dim(), (2, 3, 3));
331
+ }
332
+
333
+ assert!(sequential
334
+ .true_batch_cvt_color(&[], ColorConversion::BgrToRgb)
335
+ .unwrap()
336
+ .is_empty());
337
+ let swapped = sequential
338
+ .true_batch_cvt_color(&views, ColorConversion::BgrToRgb)
339
+ .unwrap();
340
+ assert_eq!(swapped[0][[0, 0, 0]], image[[0, 0, 2]]);
341
+ assert_eq!(swapped[0][[0, 0, 2]], image[[0, 0, 0]]);
342
+ assert_eq!(
343
+ sequential
344
+ .true_batch_cvt_color(&views, ColorConversion::RgbToBgr)
345
+ .unwrap()[0]
346
+ .dim(),
347
+ image.dim()
348
+ );
349
+ assert_eq!(
350
+ sequential
351
+ .true_batch_cvt_color(&views, ColorConversion::RgbToGray)
352
+ .unwrap()[0]
353
+ .dim(),
354
+ (4, 5, 1)
355
+ );
356
+ assert!(sequential
357
+ .true_batch_cvt_color(&views, ColorConversion::GrayToRgb)
358
+ .is_err());
359
+
360
+ let luminance = sequential.strided_luminance(&views, 0).unwrap();
361
+ assert_eq!(luminance.len(), 1);
362
+ assert!((0.0..=255.0).contains(&luminance[0]));
363
+ let empty_image = Array3::<u8>::zeros((0, 0, 3));
364
+ assert_eq!(
365
+ sequential
366
+ .strided_luminance(&[empty_image.view()], 3)
367
+ .unwrap(),
368
+ vec![0.0]
369
+ );
370
+ let invalid_image = Array3::<u8>::zeros((2, 2, 1));
371
+ assert!(sequential
372
+ .strided_luminance(&[invalid_image.view()], 1)
373
+ .is_err());
374
+
375
+ let parallel = TrueBatchProcessor {
376
+ use_parallel: true,
377
+ chunk_size: 1,
378
+ simd_threshold: 1,
379
+ };
380
+ let parallel_images = [sample_rgb(4, 5), sample_rgb(4, 5)];
381
+ let parallel_views: Vec<_> = parallel_images.iter().map(|image| image.view()).collect();
382
+ assert_eq!(
383
+ parallel
384
+ .strided_luminance(&parallel_views, 2)
385
+ .unwrap()
386
+ .len(),
387
+ 2
388
+ );
389
+ }
390
+
391
+ #[test]
392
+ fn compatibility_objects_cover_success_and_error_states() {
393
+ assert_eq!(fourcc('M', 'J', 'P', 'G'), "MJPG");
394
+
395
+ let missing_video = temporary_path("avi");
396
+ let mut capture = VideoCapture::new(missing_video.to_str().unwrap()).unwrap();
397
+ assert!(!capture.is_opened());
398
+ assert_eq!(capture.get(VideoCaptureProperties::CapPropFps), 0.0);
399
+ assert_eq!(capture.get(VideoCaptureProperties::CapPropFrameWidth), 0.0);
400
+ assert_eq!(capture.get(VideoCaptureProperties::CapPropFrameHeight), 0.0);
401
+ assert_eq!(capture.get(VideoCaptureProperties::CapPropFrameCount), 0.0);
402
+ assert_eq!(capture.read(), (false, None));
403
+ capture.release();
404
+
405
+ let image = sample_rgb(2, 3);
406
+ let mut writer = VideoWriter::new("unused.avi", "MJPG", 24.0, (3, 2)).unwrap();
407
+ assert!(writer.is_opened());
408
+ writer.write(&image.view()).unwrap();
409
+ assert!(writer
410
+ .write(&Array3::<u8>::zeros((2, 3, 1)).view())
411
+ .is_err());
412
+ assert!(writer
413
+ .write(&Array3::<u8>::zeros((3, 2, 3)).view())
414
+ .is_err());
415
+ writer.release().unwrap();
416
+ writer.release().unwrap();
417
+ assert!(!writer.is_opened());
418
+ assert!(writer.write(&image.view()).is_err());
419
+
420
+ let missing_cascade = temporary_path("xml");
421
+ let cascade = CascadeClassifier::new(missing_cascade.to_str().unwrap()).unwrap();
422
+ assert!(cascade.empty());
423
+ assert!(cascade
424
+ .detect_multi_scale(&image.view(), 1.1, 3)
425
+ .unwrap()
426
+ .is_empty());
427
+ }
File without changes
File without changes
File without changes