@@ -50,7 +50,15 @@ def image_transformer(name: str, config):
50
50
config .ROI_TRAPEZE_MIN_Y ,
51
51
config .ROI_TRAPEZE_MAX_Y
52
52
)
53
-
53
+ elif "TRAPEZE_EDGE" == name :
54
+ return cv_parts .ImgTrapezoidalEdgeMask (
55
+ config .ROI_TRAPEZE_UL ,
56
+ config .ROI_TRAPEZE_UR ,
57
+ config .ROI_TRAPEZE_LL ,
58
+ config .ROI_TRAPEZE_LR ,
59
+ config .ROI_TRAPEZE_MIN_Y ,
60
+ config .ROI_TRAPEZE_MAX_Y
61
+ )
54
62
elif "CROP" == name :
55
63
return cv_parts .ImgCropMask (
56
64
config .ROI_CROP_LEFT ,
@@ -75,7 +83,7 @@ def image_transformer(name: str, config):
75
83
return cv_parts .ImgHSV2BGR ()
76
84
elif "RGB2GRAY" == name :
77
85
return cv_parts .ImgRGB2GRAY ()
78
- elif "RBGR2GRAY " == name :
86
+ elif "BGR2GRAY " == name :
79
87
return cv_parts .ImgBGR2GRAY ()
80
88
elif "HSV2GRAY" == name :
81
89
return cv_parts .ImgHSV2GRAY ()
@@ -108,7 +116,7 @@ def image_transformer(name: str, config):
108
116
elif name .startswith ("CUSTOM" ):
109
117
return custom_transformer (name , config )
110
118
else :
111
- msg = f"{ name } is not a valid augmentation "
119
+ msg = f"{ name } is not a valid transformation "
112
120
logger .error (msg )
113
121
raise ValueError (msg )
114
122
@@ -213,3 +221,354 @@ def run(self, image):
213
221
else :
214
222
raise ValueError (f"Unable to load custom tranformation module at { file_path } " )
215
223
224
+
225
+ class ImgTransformList :
226
+ """
227
+ A list of image transforms supplied by
228
+ a json file and run in the order and with
229
+ the arguments specified in the json.
230
+ """
231
+ def __init__ (self , transforms ) -> None :
232
+ self .transforms = transforms
233
+
234
+ @staticmethod
235
+ def fromJson (filepath ):
236
+ config = load_img_transform_json (filepath )
237
+ transforms = img_transform_list_from_json (config )
238
+ return ImgTransformList (transforms )
239
+
240
+ def run (self , image ):
241
+ for transform in self .transforms :
242
+ image = transform .run (image )
243
+ return image
244
+
245
+ def shutdown (self ):
246
+ for transform in self .transforms :
247
+ if callable (getattr (transform , "shutdown" , None )):
248
+ transform .shutdown ()
249
+
250
+
251
+ def img_transform_from_json (transform_config ):
252
+ """
253
+ Construct a single Image transform from given dictionary.
254
+ The dictionary corresponds the the image transform's
255
+ constructor arguments, so it can be passed to the
256
+ constructor using object destructuring.
257
+ """
258
+ if not isinstance (transform_config , object ):
259
+ raise TypeError ("transform_config must be a dictionary" )
260
+
261
+
262
+ #
263
+ # a config is a [string, object] pair
264
+ # where the string specifies the transform
265
+ # and the optional object provides the arguments
266
+ # to it's constructor.
267
+ #
268
+ transformation = transform_config [0 ]
269
+ args = transform_config [1 ] if len (transform_config ) == 2 else None
270
+ transformer = None
271
+
272
+ #
273
+ # masking transformations
274
+ #
275
+ if "TRAPEZE_EDGE" == transformation :
276
+ transformer = cv_parts .ImgTrapezoidalEdgeMask (** args )
277
+ elif 'CROP' == transformation :
278
+ transformer = cv_parts .ImgCropMask (** args )
279
+
280
+ #
281
+ # color space transformations
282
+ #
283
+ elif "RGB2BGR" == transformation :
284
+ transformer = cv_parts .ImgRGB2BGR ()
285
+ elif "BGR2RGB" == transformation :
286
+ transformer = cv_parts .ImgBGR2RGB ()
287
+ elif "RGB2HSV" == transformation :
288
+ transformer = cv_parts .ImgRGB2HSV ()
289
+ elif "HSV2RGB" == transformation :
290
+ transformer = cv_parts .ImgHSV2RGB ()
291
+ elif "BGR2HSV" == transformation :
292
+ transformer = cv_parts .ImgBGR2HSV ()
293
+ elif "HSV2BGR" == transformation :
294
+ transformer = cv_parts .ImgHSV2BGR ()
295
+ elif "RGB2GREY" == transformation or "RGB2GRAY" == transformation :
296
+ transformer = cv_parts .ImgRGB2GRAY ()
297
+ elif "GREY2RGB" == transformation or "GRAY2RGB" == transformation :
298
+ transformer = cv_parts .ImgGRAY2RGB ()
299
+ elif "BGR2GREY" == transformation or "BGR2GRAY" == transformation :
300
+ transformer = cv_parts .ImgBGR2GRAY ()
301
+ elif "GREY2BGR" == transformation or "GRAY2BGR" == transformation :
302
+ transformer = cv_parts .ImgGRAY2BGR ()
303
+ elif "HSV2GREY" == transformation or "HSV2GRAY" == transformation :
304
+ transformer = cv_parts .ImgHSV2GRAY ()
305
+ elif "CANNY" == transformation :
306
+ # canny edge detection
307
+ transformer = cv_parts .ImgCanny (** args )
308
+ #
309
+ # blur transformations
310
+ #
311
+ elif "GBLUR" == transformation :
312
+ transformer = cv_parts .ImgGaussianBlur (** args )
313
+ elif "BLUR" == transformation :
314
+ transformer = cv_parts .ImgSimpleBlur (** args )
315
+ #
316
+ # resize transformations
317
+ #
318
+ elif "RESIZE" == transformation :
319
+ transformer = cv_parts .ImageResize (** args )
320
+ elif "SCALE" == transformation :
321
+ transformer = cv_parts .ImageScale (args .scale , args .scale_height )
322
+
323
+ #
324
+ # custom transform
325
+ #
326
+ elif transformation .startswith ("CUSTOM" ):
327
+ transformer = custom_transformer (transformation , args )
328
+
329
+ #
330
+ # not a valid transform name
331
+ #
332
+ else :
333
+ msg = f"'{ transformation } ' is not a valid transformation"
334
+ logger .error (msg )
335
+ raise ValueError (msg )
336
+
337
+ return transformer
338
+
339
+
340
+ def img_transform_list_from_json (transforms_config ):
341
+ """
342
+ Parse one or more Image transforms from given list
343
+ and return an ImgTransformer that applies
344
+ them with the arguments and in the order given
345
+ in the file.
346
+
347
+ """
348
+ if not isinstance (transforms_config , list ):
349
+ raise TypeError ("transforms_config must be a list" )
350
+
351
+ transformers = []
352
+
353
+ for transform_config in transforms_config :
354
+ transformers .append (img_transform_from_json (transform_config ))
355
+
356
+ return transformers
357
+
358
+
359
+ def load_img_transform_json (filepath ):
360
+ """
361
+ Load a json file that specifies a list with one or more
362
+ image transforms, their order and their arguments.
363
+
364
+ The list will contain a series of tuples as a two
365
+ element list. The first element of the tuple is the name
366
+ of the transform and the second element is a dictionary
367
+ the named arguments for the transform's constructor.
368
+ The named arguments using object destructuring except
369
+ for the custom transform where the dictionary is
370
+ pass as-is without destructuring.
371
+
372
+ You can look at the constructor for each image transform
373
+ in cv.py to see what the fields of the argument object in
374
+ the json should be. You may leave out an argument if it
375
+ has a default.
376
+
377
+ Here is an example that has one of each transformtion
378
+ specified with all of it's arguments.
379
+
380
+ ```
381
+ [
382
+ ["BGR2GRAY"],
383
+ ["BGR2HSV"],
384
+ ["BGR2RGB"],
385
+ ["BLUR", {"kernel_size": 5, "kernel_y": null}],
386
+ ["CANNY", {"low_threshold": 60, "high_threshold": 110, "aperture_size": 3, "l2gradient": false}],
387
+ ["CROP", {"left": 0, "top": 0, "right": 0, "bottom": 0}],
388
+ ["GBLUR", {"kernel_size": 5, "kernel_y": null}],
389
+ ["GRAY2BGR"],
390
+ ["GRAY2RGB"],
391
+ ["HSV2BGR"],
392
+ ["HSV2RGB"],
393
+ ["HSV2GRAY"],
394
+ ["RESIZE", {"width": 160, "height": 120}],
395
+ ["RGB2BGR"],
396
+ ["RGB2GRAY"],
397
+ ["RGB2HSV"],
398
+ ["SCALE", {"scale": 1.0, "scale_height": null}],
399
+ ["TRAPEZE", {"left":0, "right":0, "bottom_left":0, "bottom_right":0, "top":0, "bottom":0, "fill": [255,255,255]}],
400
+ ["TRAPEZE_EDGE", {"upper_left":0, "upper_right":0, "lower_left":0, "lower_right":0, "top":0, "bottom":0, "fill": [255,255,255]}]
401
+ ]
402
+ ```
403
+
404
+ """
405
+ import json
406
+
407
+ #
408
+ # load and parse the file
409
+ #
410
+ try :
411
+ with open (filepath ) as f :
412
+ try :
413
+ data = json .load (f )
414
+ #
415
+ # TODO: validate json data against a schema
416
+ #
417
+ return data
418
+ except e :
419
+ logger .error (f"Can't parse transforms json due to error: { e } " )
420
+ raise e
421
+ except OSError as e :
422
+ logger .error (f"Can't open transforms json file '{ filepath } ' due to error: { e } " )
423
+ raise e
424
+
425
+
426
+ if __name__ == "__main__" :
427
+ """
428
+ Image transforms self test.
429
+ You provide a json file that specifies a transformation pipeline
430
+ and configure either an single image to be loaded or a camera to be used.
431
+ The image transformation pipeline is constructed and applied the
432
+ the configured image source and shown in an opencv window.
433
+
434
+ This json specifies a pipeline that applies canny edge detection to the image.
435
+
436
+ ```
437
+ [
438
+ ["RGB2GRAY"],
439
+ ["BLUR", {}],
440
+ ["CANNY", {}],
441
+ ["CROP", {"left": 0, "top": 45, "right": 0, "bottom": 0}],
442
+ ["GRAY2RGB"]
443
+ ]
444
+ ```
445
+
446
+ Here the "BLUR" and "CANNY" transforms are using default parameters, so the
447
+ argument object is empty. The "CROP" transform is supplied with a argument object
448
+ that specifies all named parameters and their values. The color conversion transforms
449
+ "RGB2GRAY" and "GRAY2RGB" do not have arguments so no argument object is supplied.
450
+
451
+ If it was in a json file named `canny_pipeline.json` in `pi` home folder the usage would be:
452
+
453
+ ```
454
+ cd donkeycar/parts
455
+ python image_transformations.py --width=640 --height=480 --json=/Home/pi/canny_pipeline.json
456
+ ```
457
+
458
+ """
459
+ import argparse
460
+ import sys
461
+ import time
462
+ import cv2
463
+ import numpy as np
464
+ import logging
465
+
466
+ # parse arguments
467
+ parser = argparse .ArgumentParser ()
468
+ parser .add_argument ("-c" , "--camera" , type = int , default = 0 ,
469
+ help = "index of camera if using multiple cameras" )
470
+ parser .add_argument ("-wd" , "--width" , type = int , default = 160 ,
471
+ help = "width of image to capture" )
472
+ parser .add_argument ("-ht" , "--height" , type = int , default = 120 ,
473
+ help = "height of image to capture" )
474
+ parser .add_argument ("-f" , "--file" , type = str ,
475
+ help = "path to image file to user rather that a camera" )
476
+ parser .add_argument ("-js" , "--json" , type = str ,
477
+ help = "path to json file with list of tranforms" )
478
+
479
+
480
+ # Read arguments from command line
481
+ args = parser .parse_args ()
482
+
483
+ image_source = None
484
+ help = []
485
+ if args .file is None :
486
+ if args .camera < 0 :
487
+ help .append ("-c/--camera must be >= 0" )
488
+ if args .width is None or args .width < 160 :
489
+ help .append ("-wd/--width must be >= 160" )
490
+ if args .height is None or args .height < 120 :
491
+ help .append ("-ht/--height must be >= 120" )
492
+
493
+ if args .json is None :
494
+ help .append ("-js/--json must be supplied to specify the json file with transformers." )
495
+
496
+
497
+ if len (help ) > 0 :
498
+ parser .print_help ()
499
+ for h in help :
500
+ print (" " + h )
501
+ sys .exit (1 )
502
+
503
+ #
504
+ # load file OR setup camera
505
+ #
506
+ cap = None
507
+ width = None
508
+ height = None
509
+ depth = 3
510
+ if args .file is not None :
511
+ print (f"Loading image from file `{ args .file } `..." )
512
+ image_source = cv_parts .CvImgFromFile (args .file , image_w = args .width , image_h = args .height , copy = True )
513
+ height , width , depth = cv_parts .image_shape (image_source .run ())
514
+ else :
515
+ print ("Initializing camera..." )
516
+ width = args .width
517
+ height = args .height
518
+ image_source = cv_parts .CvCam (image_w = width , image_h = height , iCam = args .camera )
519
+ print ("done." )
520
+
521
+ #
522
+ # read list transformations from json file
523
+ # with fields like:
524
+ #
525
+ # [
526
+ # ["BGR2GRAY"],
527
+ # ["BGR2HSV"],
528
+ # ["BGR2RGB"],
529
+ # ["BLUR", {"gaussian": false, "kernel": 5, "kernel_y": null}]
530
+ # ["CANNY", {"low_threshold": 60, "high_threshold": 110, "aperture": 3}]
531
+ # ["CROP", {"left": 0, "top": 0, "right": 0, "bottom": 0}],
532
+ # ["GRAY2BGR"],
533
+ # ["GRAY2RGB"],
534
+ # ["HSV2BGR"],
535
+ # ["HSV2RGB"],
536
+ # ["HSV2GRAY"],
537
+ # ["RESIZE", {"width": 160, "height": 120}],
538
+ # ["RGB2BGR"],
539
+ # ["RGB2GRAY"],
540
+ # ["RGB2HSV"],
541
+ # ["SCALE", {"scale_width": 1.0, "scale_height": 1.0}],
542
+ # ["TRAPEZE", {"upper_left":0, "upper_right":0, "lower_left":0, "lower_right":0, "top":0, "bottom":0}]
543
+ # ]
544
+ #
545
+ print ("Loading tranform list from json file `{args.json}`..." )
546
+ transformer = ImgTransformList .fromJson (args .json )
547
+ print ("done." )
548
+
549
+ # Creating a window for later use
550
+ window_name = 'image_tranformer'
551
+ cv2 .namedWindow (window_name )
552
+
553
+ while (1 ):
554
+
555
+ frame = image_source .run ()
556
+
557
+ #
558
+ # apply the augmentation
559
+ #
560
+ transformed_image = transformer .run (frame )
561
+
562
+ #
563
+ # show augmented image
564
+ #
565
+ cv2 .imshow (window_name , transformed_image )
566
+
567
+ k = cv2 .waitKey (5 ) & 0xFF
568
+ if k == ord ('q' ) or k == ord ('Q' ): # 'Q' or 'q'
569
+ break
570
+
571
+ if cap is not None :
572
+ cap .release ()
573
+
574
+ cv2 .destroyAllWindows ()
0 commit comments