Skip to content

Commit 1398eec

Browse files
authored
add a __main__ self-test configured with a json file (autorope#1134)
* add a __main__ self-test configured with a json file - the __main__ self-test can load an image from a file or use the video stream from a camera - It takes the path to a json file that specifies a list of image transforms, there order and their arguments - functions have been added to load and parse the json file and use it to construct the transform pipeline. - The transform pipeline is implemented with a run() method that applies the transforms in order. - Comments in the code document the json file format. * version=5.0.dev2
1 parent 3dee532 commit 1398eec

File tree

2 files changed

+363
-4
lines changed

2 files changed

+363
-4
lines changed

donkeycar/parts/image_transformations.py

Lines changed: 362 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,15 @@ def image_transformer(name: str, config):
5050
config.ROI_TRAPEZE_MIN_Y,
5151
config.ROI_TRAPEZE_MAX_Y
5252
)
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+
)
5462
elif "CROP" == name:
5563
return cv_parts.ImgCropMask(
5664
config.ROI_CROP_LEFT,
@@ -75,7 +83,7 @@ def image_transformer(name: str, config):
7583
return cv_parts.ImgHSV2BGR()
7684
elif "RGB2GRAY" == name:
7785
return cv_parts.ImgRGB2GRAY()
78-
elif "RBGR2GRAY" == name:
86+
elif "BGR2GRAY" == name:
7987
return cv_parts.ImgBGR2GRAY()
8088
elif "HSV2GRAY" == name:
8189
return cv_parts.ImgHSV2GRAY()
@@ -108,7 +116,7 @@ def image_transformer(name: str, config):
108116
elif name.startswith("CUSTOM"):
109117
return custom_transformer(name, config)
110118
else:
111-
msg = f"{name} is not a valid augmentation"
119+
msg = f"{name} is not a valid transformation"
112120
logger.error(msg)
113121
raise ValueError(msg)
114122

@@ -213,3 +221,354 @@ def run(self, image):
213221
else:
214222
raise ValueError(f"Unable to load custom tranformation module at {file_path}")
215223

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()

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def package_files(directory, strip_leading):
2424
long_description = fh.read()
2525

2626
setup(name='donkeycar',
27-
version="5.0.dev1",
27+
version="5.0.dev2",
2828
long_description=long_description,
2929
description='Self driving library for python.',
3030
url='https://github.com/autorope/donkeycar',

0 commit comments

Comments
 (0)