diff --git a/.gitignore b/.gitignore index 26c6b31da..c43b51452 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ pids *.seed *.pid.lock +# Python stuff +.venv + # Directory for instrumented libs generated by jscoverage/JSCover lib-cov diff --git a/Development.md b/Development.md new file mode 100644 index 000000000..ca55eee1a --- /dev/null +++ b/Development.md @@ -0,0 +1,26 @@ +# Development documentation + +## Writing unit tests + +### Tests that compare with OpenCV + +[OpenCV](https://opencv.org/) is a popular image processing library for C++ and Python. +We use it as a reference for some of the functions implemented in ImageJS. + +All images used for comparison with OpenCV are generated by the Python script [`test/img/opencv/generate.py`](./test/img/opencv/generate.py). + +To add a new test reference file: + +- Update [`generate.py`](./test/img/opencv/generate.py) with the OpenCV code that creates the file. +- Run `generate.py` (see following paragraph). +- Add the new filename to the [`TestImagePath`](./test/TestImagePath.ts) type (alphabetical order). + +To run the generation script, use the following steps: + +- Install Python 3.x +- Run `python3 -m venv .venv` to create a virtual environment for the project +- Activate the venv or run the local `pip` and `python` commands. + - `source .venv/bin/activate` (UNIX) + - `.venv/Scripts/Activate.ps1` (Windows) +- Run `pip install opencv-python` +- Run `python test/img/opencv/generate.py` diff --git a/README.md b/README.md index b96bf021a..da9dfa2d9 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,10 @@ Image processing and manipulation in JavaScript. Look at the [examples](./examples) directory for how the API is being designed. +## Development + +See [Development documentation](./Development.md). + ## License [MIT](./LICENSE) diff --git a/test/TestImagePath.ts b/test/TestImagePath.ts index 92c92414a..9e513eef8 100644 --- a/test/TestImagePath.ts +++ b/test/TestImagePath.ts @@ -1,5 +1,39 @@ // Update this type when test/img content changes. export type TestImagePath = + | 'align/cropped.png' + | 'align/cropped1.png' + | 'align/croppedRef.png' + | 'align/croppedRef1.png' + | 'correctColor/color-balance.png' + | 'correctColor/exposure-minus-1.png' + | 'correctColor/exposure-plus-1.png' + | 'correctColor/inverted.png' + | 'correctColor/offsets.png' + | 'correctColor/test.png' + | 'featureMatching/alphabet.jpg' + | 'featureMatching/alphabetRotated2.jpg' + | 'featureMatching/alphabetRotated5.jpg' + | 'featureMatching/alphabetRotated10.jpg' + | 'featureMatching/alphabetRotated-2.jpg' + | 'featureMatching/alphabetRotated-5.jpg' + | 'featureMatching/alphabetRotated-10.jpg' + | 'featureMatching/alphabetTranslated10.jpg' + | 'featureMatching/alphabetTranslated20.jpg' + | 'featureMatching/alphabetTranslated50.jpg' + | 'featureMatching/id-crops/crop1.png' + | 'featureMatching/id-crops/crop2.png' + | 'featureMatching/id-crops/crop3.png' + | 'featureMatching/patch.png' + | 'featureMatching/polygons/star.png' + | 'featureMatching/polygons/scaleneTriangle.png' + | 'featureMatching/polygons/scaleneTriangle2.png' + | 'featureMatching/polygons/scaleneTriangle10.png' + | 'featureMatching/polygons/scaleneTriangle90.png' + | 'featureMatching/polygons/scaleneTriangle180.png' + | 'featureMatching/polygons/polygon.png' + | 'featureMatching/polygons/polygon2.png' + | 'featureMatching/polygons/polygonRotated10degrees.png' + | 'featureMatching/polygons/polygonRotated180degrees.png' | 'formats/grey6.jpg' | 'formats/grey8.png' | 'formats/grey12.jpg' @@ -15,74 +49,40 @@ export type TestImagePath = | 'formats/rgba32.png' | 'formats/rgba64.png' | 'formats/tif/grey8.tif' + | 'formats/tif/grey8-multi.tif' | 'formats/tif/grey16.tif' + | 'formats/tif/grey16-multi.tif' | 'formats/tif/grey32.tif' | 'formats/tif/greya16.tif' | 'formats/tif/greya32.tif' + | 'formats/tif/palette.tif' | 'formats/tif/rgb16.tif' - | 'formats/tif/rgba8.tif' - | 'formats/tif/grey8-multi.tif' - | 'formats/tif/grey16-multi.tif' | 'formats/tif/rgb16-multi.tif' + | 'formats/tif/rgba8.tif' | 'formats/tif/rgba8-multi.tif' - | 'formats/tif/palette.tif' + | 'morphology/alphabetCannyEdge.png' + | 'morphology/grayscaleCannyEdge.png' + | 'morphology/grayscaleClearBorder.png' | 'opencv/test.png' | 'opencv/testAffineTransform.png' + | 'opencv/testAntiClockwiseRot90.png' | 'opencv/testBlur.png' + | 'opencv/testClockwiseRot90.png' | 'opencv/testConvolution.png' | 'opencv/testGaussianBlur.png' | 'opencv/testInterpolate.png' - | 'opencv/testRotateBicubic.png' - | 'opencv/testRotateBilinear.png' - | 'opencv/testTranslate.png' + | 'opencv/testReflect.png' | 'opencv/testResizeBilinear.png' | 'opencv/testResizeNearest.png' - | 'opencv/testAntiClockwiseRot90.png' - | 'opencv/testClockwiseRot90.png' + | 'opencv/testRotateBicubic.png' + | 'opencv/testRotateBilinear.png' | 'opencv/testScale.png' - | 'opencv/testReflect.png' - | 'various/grayscale_by_zimmyrose.png' - | 'various/alphabet.jpg' - | 'various/without-metadata.jpg' - | 'morphology/alphabetCannyEdge.png' - | 'morphology/grayscaleCannyEdge.png' - | 'morphology/grayscaleClearBorder.png' - | 'correctColor/test.png' - | 'correctColor/color-balance.png' - | 'correctColor/exposure-minus-1.png' - | 'correctColor/exposure-plus-1.png' - | 'correctColor/inverted.png' - | 'correctColor/offsets.png' - | 'ssim/ssim-original.png' - | 'ssim/ssim-contrast.png' - | 'ssim/ssim-saltPepper.png' + | 'opencv/testTranslate.png' | 'ssim/ssim-blurry.png' | 'ssim/ssim-compressed.png' - | 'featureMatching/alphabet.jpg' - | 'featureMatching/alphabetRotated2.jpg' - | 'featureMatching/alphabetRotated5.jpg' - | 'featureMatching/alphabetRotated10.jpg' - | 'featureMatching/alphabetRotated-2.jpg' - | 'featureMatching/alphabetRotated-5.jpg' - | 'featureMatching/alphabetRotated-10.jpg' - | 'featureMatching/alphabetTranslated10.jpg' - | 'featureMatching/alphabetTranslated20.jpg' - | 'featureMatching/alphabetTranslated50.jpg' - | 'featureMatching/id-crops/crop1.png' - | 'featureMatching/id-crops/crop2.png' - | 'featureMatching/id-crops/crop3.png' - | 'featureMatching/patch.png' - | 'featureMatching/polygons/star.png' - | 'featureMatching/polygons/scaleneTriangle.png' - | 'featureMatching/polygons/scaleneTriangle2.png' - | 'featureMatching/polygons/scaleneTriangle10.png' - | 'featureMatching/polygons/scaleneTriangle90.png' - | 'featureMatching/polygons/scaleneTriangle180.png' - | 'featureMatching/polygons/polygon.png' - | 'featureMatching/polygons/polygon2.png' - | 'featureMatching/polygons/polygonRotated180degrees.png' - | 'featureMatching/polygons/polygonRotated10degrees.png' - | 'align/cropped.png' - | 'align/croppedRef.png' - | 'align/cropped1.png' - | 'align/croppedRef1.png'; + | 'ssim/ssim-contrast.png' + | 'ssim/ssim-original.png' + | 'ssim/ssim-saltPepper.png' + | 'various/alphabet.jpg' + | 'various/grayscale_by_zimmyrose.png' + | 'various/without-metadata.jpg'; diff --git a/test/img/opencv/generate.py b/test/img/opencv/generate.py index df354b696..d4a6f91dc 100644 --- a/test/img/opencv/generate.py +++ b/test/img/opencv/generate.py @@ -1,7 +1,13 @@ import numpy as np import cv2 as cv +from os import path -img = cv.imread('./test.png') +dirname = path.dirname(path.abspath(__file__)) + +def writeImg(name, img): + cv.imwrite(path.join(dirname, name), img) + +img = cv.imread(path.join(dirname, 'test.png')) assert img is not None, "file could not be read, check with os.path.exists()" rows, cols = img.shape[0], img.shape[1] @@ -10,67 +16,65 @@ M = np.float32([[scale, 0, 0], [0, scale, 0]]) dst = cv.warpAffine(img, M, dsize=(cols * scale, rows * scale), flags=cv.INTER_LINEAR, borderMode=cv.BORDER_CONSTANT, borderValue=0) -cv.imwrite('testScale.png', dst) +writeImg('testScale.png', dst) -# Image resizing by 10. +# Image resizing. dst = cv.resize(img, (80, 100), interpolation=cv.INTER_NEAREST) -cv.imwrite('testResizeNearest.png', dst) +writeImg('testResizeNearest.png', dst) dst = cv.resize(img, (80, 100), interpolation=cv.INTER_LINEAR) -cv.imwrite('testResizeBilinear.png', dst) +writeImg('testResizeBilinear.png', dst) # Image rotate counter-clockwise by 90 degrees M = np.float32([[0, 1, 0], [-1, 0, cols - 1]]) dst = cv.warpAffine(img, M, (rows, cols), flags=cv.INTER_LINEAR, borderMode=cv.BORDER_CONSTANT) -cv.imwrite('testAntiClockwiseRot90.png', dst) +writeImg('testAntiClockwiseRot90.png', dst) # Image rotate clockwise by 90 degrees M = np.float32([[0, -1, cols + 1], [1, 0, 0]]) dst = cv.warpAffine(img, M, (rows, cols), flags=cv.INTER_LINEAR, borderMode=cv.BORDER_CONSTANT) -cv.imwrite('testClockwiseRot90.png', dst) +writeImg('testClockwiseRot90.png', dst) # Image interpolation matrix = cv.getRotationMatrix2D((2, 4), angle=30, scale=0.8) dst = cv.warpAffine(img, matrix, dsize=(cols, rows), flags=cv.INTER_NEAREST, borderMode=cv.BORDER_REFLECT) -cv.imwrite('testInterpolate.png', dst) +writeImg('testInterpolate.png', dst) # Image bilinear interpolation matrix = cv.getRotationMatrix2D((2, 4), angle=30, scale=1.4) dst = cv.warpAffine(img, matrix, dsize=(cols, rows), flags=cv.INTER_LINEAR, borderMode=cv.BORDER_REFLECT) -cv.imwrite('testRotateBilinear.png', dst) +writeImg('testRotateBilinear.png', dst) # Image bicubic interpolation matrix = cv.getRotationMatrix2D((2, 4), angle=30, scale=1.4) dst = cv.warpAffine(img, matrix, dsize=(cols, rows), flags=cv.INTER_CUBIC, borderMode=cv.BORDER_REFLECT) -cv.imwrite('testRotateBicubic.png', dst) +writeImg('testRotateBicubic.png', dst) # Image reflection M = np.float32([[1, 0, 0], [0, -1, rows - 1]]) dst = cv.warpAffine(img, M, (cols, rows), flags=cv.INTER_LINEAR, borderMode=cv.BORDER_CONSTANT) -cv.imwrite('testReflect.png', dst) +writeImg('testReflect.png', dst) # Image translation M = np.float32([[1, 0, 2], [0, 1, 4]]) dst = cv.warpAffine(img, M, (16, 20)) -cv.imwrite('testTranslate.png', dst) +writeImg('testTranslate.png', dst) # Image affine transformation M = np.float32([[2, 1, 2], [-1, 1, 2]]) dst = cv.warpAffine(img, M, (cols, rows), flags=cv.INTER_LINEAR, borderMode=cv.BORDER_CONSTANT) -cv.imwrite('testAffineTransform.png', dst) +writeImg('testAffineTransform.png', dst) # Image blur dst = cv.blur(img, (3, 5), borderType=cv.BORDER_REFLECT) -cv.imwrite('testBlur.png', dst) +writeImg('testBlur.png', dst) # Image gaussian blur kernel = cv.getGaussianKernel(3, 1) dst = cv.sepFilter2D(img, -1, kernel, kernel, borderType=cv.BORDER_REFLECT) -cv.imwrite('testGaussianBlur.png', dst) +writeImg('testGaussianBlur.png', dst) # Image convolution kernelX = np.float32([[0.1, 0.2, 0.3]]) - kernelY = np.float32([[0.4, 0.5, 0.6, -0.3, -0.4]]) - dst = cv.sepFilter2D(img, ddepth=-1, kernelX=kernelX, kernelY=kernelY, borderType=cv.BORDER_REFLECT) -cv.imwrite('testConvolution.png', dst) +writeImg('testConvolution.png', dst)