├── pycoreimage ├── __init__.py ├── resources │ ├── Food_1.jpg │ ├── Food_3.jpg │ ├── Sunset_5.jpg │ ├── Sunset_6.jpg │ ├── Flowers_1.jpg │ ├── GGBridge_2.jpg │ ├── Landscape_1.jpg │ └── Landscape_3.jpg ├── pyci_launcher.py ├── pyci_demo.py └── pyci.py ├── .gitignore ├── CoreImagePython.xcodeproj ├── .xcodesamplecode.plist └── project.pbxproj ├── Configuration └── SampleCode.xcconfig ├── LICENSE └── LICENSE.txt ├── CoreImagePython └── main.swift ├── setup.py └── README.md /pycoreimage/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.0.1' -------------------------------------------------------------------------------- /pycoreimage/resources/Food_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarshure/CoreImagePython/HEAD/pycoreimage/resources/Food_1.jpg -------------------------------------------------------------------------------- /pycoreimage/resources/Food_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarshure/CoreImagePython/HEAD/pycoreimage/resources/Food_3.jpg -------------------------------------------------------------------------------- /pycoreimage/resources/Sunset_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarshure/CoreImagePython/HEAD/pycoreimage/resources/Sunset_5.jpg -------------------------------------------------------------------------------- /pycoreimage/resources/Sunset_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarshure/CoreImagePython/HEAD/pycoreimage/resources/Sunset_6.jpg -------------------------------------------------------------------------------- /pycoreimage/resources/Flowers_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarshure/CoreImagePython/HEAD/pycoreimage/resources/Flowers_1.jpg -------------------------------------------------------------------------------- /pycoreimage/resources/GGBridge_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarshure/CoreImagePython/HEAD/pycoreimage/resources/GGBridge_2.jpg -------------------------------------------------------------------------------- /pycoreimage/resources/Landscape_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarshure/CoreImagePython/HEAD/pycoreimage/resources/Landscape_1.jpg -------------------------------------------------------------------------------- /pycoreimage/resources/Landscape_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yarshure/CoreImagePython/HEAD/pycoreimage/resources/Landscape_3.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See LICENSE folder for this sample’s licensing information. 2 | # 3 | # Apple sample code gitignore configuration. 4 | 5 | # Finder 6 | .DS_Store 7 | 8 | # Xcode - User files 9 | xcuserdata/ 10 | *.xcworkspace 11 | -------------------------------------------------------------------------------- /CoreImagePython.xcodeproj/.xcodesamplecode.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Configuration/SampleCode.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // See LICENSE folder for this sample’s licensing information. 3 | // 4 | // SampleCode.xcconfig 5 | // 6 | 7 | // The `SAMPLE_CODE_DISAMBIGUATOR` configuration is to make it easier to build 8 | // and run a sample code project. Once you set your project's development team, 9 | // you'll have a unique bundle identifier. This is because the bundle identifier 10 | // is derived based on the 'SAMPLE_CODE_DISAMBIGUATOR' value. Do not use this 11 | // approach in your own projects—it's only useful for sample code projects because 12 | // they are frequently downloaded and don't have a development team set. 13 | SAMPLE_CODE_DISAMBIGUATOR=${DEVELOPMENT_TEAM} 14 | -------------------------------------------------------------------------------- /LICENSE/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2018 Apple Inc. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | 9 | -------------------------------------------------------------------------------- /CoreImagePython/main.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Contains the main routine for setting up pycoreimage. 6 | */ 7 | 8 | import Foundation 9 | 10 | func execute() { 11 | 12 | let script = Bundle.main.path(forResource: "pyci_demo", ofType: "py") 13 | let img = Bundle.main.path(forResource: "YourImagePath", ofType: "HEIC") 14 | let matte = Bundle.main.path(forResource: "YourFacePhotoPath", ofType: "HEIC") 15 | let dataset = URL(fileURLWithPath: img!).deletingLastPathComponent().path 16 | 17 | print(script!) 18 | print(img!) 19 | print(matte!) 20 | print(dataset) 21 | 22 | let pipe = Pipe() 23 | let file = pipe.fileHandleForReading; 24 | let task = Process() 25 | task.launchPath = "/usr/bin/python" 26 | task.arguments = [script, img, matte, dataset] as? [String] 27 | task.standardOutput = pipe 28 | task.launch() 29 | 30 | let data = file.readDataToEndOfFile() 31 | file.closeFile() 32 | 33 | let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue) 34 | print(output!) 35 | } 36 | 37 | print("Hello, World!") 38 | execute() 39 | -------------------------------------------------------------------------------- /pycoreimage/pyci_launcher.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os, subprocess, sys 4 | import pip 5 | 6 | if __name__ == '__main__': 7 | cwd = os.path.dirname(os.path.dirname(__file__)) 8 | print('Running demo launcher from', cwd) 9 | 10 | try: 11 | os.chdir('/') 12 | import numpy 13 | import matplotlib 14 | import pycoreimage 15 | 16 | from pycoreimage import pyci 17 | print(numpy.__file__) 18 | print(matplotlib.__file__) 19 | print(pycoreimage.__file__) 20 | 21 | except Exception as e: 22 | print(e) 23 | 24 | print('It appears that you are missing packages for this script to run properly.') 25 | 26 | cmd = 'python setup.py develop --user' 27 | sys.stdout.write('OK to run `{}` from `{}` ? [Y/N] '.format(cmd, cwd)) 28 | answer = raw_input() 29 | 30 | if answer.lower() == 'y': 31 | output = subprocess.check_output(cmd, cwd=cwd, shell=True) 32 | print(output) 33 | print('Done with installation. Run this script again.') 34 | exit(0) 35 | else: 36 | exit(1) 37 | 38 | # Run demo code 39 | img = os.path.join(cwd, 'pycoreimage', 'resources', 'Food_1.jpg') 40 | dataset = os.path.join(cwd, 'pycoreimage', 'resources') 41 | script = os.path.join(cwd, 'pycoreimage', 'pyci_demo.py') 42 | cmd = 'python {} {} {}'.format(script, img, dataset) 43 | subprocess.check_call(cmd, cwd=cwd, shell=True) 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """pycoreimage setup module. 2 | 3 | To install pycoreimage, open a terminal window, and 4 | - cd /path/to/this/directory/ 5 | - python setup.py --user 6 | 7 | To experiment and make changes to the source code: 8 | - cd /path/to/this/directory/ 9 | - python setup.py develop --user 10 | """ 11 | 12 | # https://packaging.python.org/guides/distributing-packages-using-setuptools/ 13 | from setuptools import setup 14 | 15 | # Get current __version__ 16 | #execfile('pycoreimage/__init__.py') 17 | exec(open('pycoreimage/__init__.py').read()) 18 | 19 | setup( 20 | name='pycoreimage', 21 | 22 | version=__version__, 23 | 24 | description='Python bindings for Core Image', 25 | 26 | # FIXME: 27 | url='https://developer.apple.com/sample-code/wwdc/2016/', 28 | 29 | author='Apple Inc.', 30 | 31 | # https://pypi.org/classifiers/ 32 | classifiers=[ 33 | 'Development Status :: 3 - Beta', 34 | 'Intended Audience :: Developers', 35 | 'Topic :: Software Development :: Build Tools', 36 | 'Topic :: Scientific/Engineering :: Artificial Intelligence', 37 | 'Topic :: Scientific/Engineering :: Image Recognition', 38 | 'Topic :: Scientific/Engineering :: Information Analysis', 39 | 'Topic :: Scientific/Engineering :: Visualization', 40 | 'License :: OSI Approved :: MIT License', 41 | 'Programming Language :: Python :: 2', 42 | 'Programming Language :: Python :: 3', 43 | ], 44 | 45 | keywords='CoreImage', 46 | 47 | packages=['pycoreimage'], 48 | 49 | install_requires=['numpy>=1.13.0', 50 | 'matplotlib>=1.3.1', 51 | 'pyobjc>=4.2', 52 | ], 53 | 54 | ) 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prototyping Your App's Core Image Pipeline with Python 2 | 3 | Write and test Core Image filters, kernels, and geometric transformations in Python. 4 | 5 | ## Overview 6 | 7 | The Python bindings available in `pycoreimage` allow you to prototype your app’s Core Image pipeline without writing production code in Objective-C or Swift, accelerating the process of fine-tuning filters and experimenting with Core Image effects. This sample code project demonstrates `pycoreimage` filter usage for common image processing operations, such as depth filtering, color warping, and geometric transformation. 8 | 9 | ## Install `pycoreimage` 10 | 11 | The version of Python that shipped with your system may not correspond to the most recent release of Python; after upgrading to Python 3.5, set up your Python environment by downloading and installing `pip` beforehand by inputting the command into Terminal: 12 | 13 | >>> easy_install pip 14 | 15 | `pip` is an installer that you'll use to fetch and install Python packages, such as `numpy` for numerical computing and `scikit-image` for displaying images. Ensure `setuptools` is up to date by calling the command: 16 | 17 | >>> pip install setuptools --upgrade 18 | 19 | Next, install `pycoreimage` by executing the following script from your Terminal command line: 20 | 21 | >>> python setup.py --user 22 | 23 | The script installs necessary packages to run the demo, such as `numpy` and `pyobjc`, so you don't need to install them separately. 24 | 25 | The console will ask to install `pycoreimage`. This allows your program to access Core Image functionality such as Python bindings for Core Image filters. From Python, you can apply the Portrait Matte effect, custom GPU kernels, barcode generators, and geometrical transformations. 26 | 27 | The package `scikit-image` is not required for `pycoreimage`, but it is necessary to run the sample demo since we are displaying images on the screen. 28 | 29 | >>> pip install scikit-image --user 30 | 31 | ## Run the Sample Code in a Python Environment 32 | 33 | The sample code loads and runs in Xcode, but you can also run it in your preferred Python interpreter. Editing the code in an interpreter, you can see the resulting image change in real-time as you might in a REPL compiler or IDE. 34 | 35 | ## Load Images Using `pycoreimage` 36 | 37 | `pycoreimage` supports much of the functionality that Core Image offers to replicate the recipe-based environment of image processing on GPUs. 38 | 39 | Start by importing the Core Image class from `pyci`: 40 | 41 | ``` 42 | from pycoreimage.pyci import cimg 43 | ``` 44 | 45 | `pycoreimage` supports all file formats that Core Image supports, including JPEG, PNG, and HEIC. 46 | 47 | Load images of any standard file type from disk into the native cimg format. 48 | 49 | ``` 50 | fpath = 'resources/YourFacialImage.HEIC' 51 | image = cimg.fromFile(fpath) 52 | ``` 53 | 54 | ## Work with Depth Data 55 | 56 | Obtain the image size using the `size` property: 57 | 58 | ``` 59 | W, H = image.size 60 | ``` 61 | 62 | Access depth data that you can use to perform image processing operations such as thresholding, segmentation, and mask morphology: 63 | 64 | ``` 65 | depth = cimg.fromFile(fpath, useDepth=True) 66 | w, h = depth.size 67 | 68 | # Set the threshold depth at the 50th percentile. 69 | depth_img = depth.render()[..., :3] 70 | p = np.percentile(depth_img, 50) 71 | mask = depth_img < p 72 | mask = cimg(mask.astype(np.float32)) 73 | mask = mask.morphologyMaximum(radius=5) 74 | mask = mask.gaussianBlur(radius=30) 75 | mask = mask.render() 76 | ``` 77 | 78 | Render the final result in the Python interpreter, with immediate feedback in real time: 79 | 80 | ``` 81 | show(img.clip(0, 1), 221) 82 | show(depth.render()[..., 0].clip(0, 1), 222, map='jet') 83 | show(img_feather.clip(0, 1), 223, suptitle='Demo 6: depth') 84 | show(mask[..., 0], 224, map='gray') 85 | ``` 86 | 87 | ## See the Portrait Matte Effect 88 | 89 | The demo in `pyci_demo.py` applies Core Image filters to achieve a number of image processing effects, as shown in the [WWDC presentation](https://developer.apple.com/videos/play/wwdc2018/719/), but you must substitute your own facial images with the corresponding portrait effect depth data to see the Portrait Matte effect. 90 | 91 | Using a facial image captured in portrait mode on iOS 12, you can load the Portrait Matte effect with the following command: 92 | 93 | ``` 94 | matte = cimg.fromFile(fpath, useMatte=True) 95 | ``` 96 | 97 | ## Apply Core Image Filters by Name 98 | 99 | With your image loaded, you can apply any of over 200 Core Image filters, calling the same methods that Objective-C calls in a production environment. See [Core Image Filter Reference](https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/) for a listing of these filters. You can invoke any filter explicitly by name: 100 | 101 | ``` 102 | img = img.CIGaussianBlur(radius=1) 103 | ``` 104 | 105 | Alternatively, you can invoke and apply filters with their string names by using the `applyFilter` function: 106 | 107 | ``` 108 | # Load an input image from file. 109 | img = cimg.fromFile('resources/sunset_1.png') 110 | 111 | # Resize to half size. 112 | img = img.scale(0.5, 0.5) 113 | 114 | # Create a blank image. 115 | composite = np.zeros((img.size[1], img.size[0], 3)) 116 | 117 | filters = 'pixellate', 'edgeWork', 'gaussianBlur', 'comicEffect', 'hexagonalPixellate' 118 | rows = int(img.size[1]) / len(filters) 119 | for i, filter in enumerate(filters): 120 | # Apply the filter. 121 | slice = img.applyFilter(filter) 122 | 123 | # Slice and add to composite. 124 | lo = i * rows 125 | hi = (i + 1) * rows 126 | composite[lo:hi, :, :3] = slice[lo:hi, :, :3] 127 | ``` 128 | 129 | You can query available filters by their name by using the `print` command: 130 | 131 | ``` 132 | print(cimg.filters()) 133 | ``` 134 | 135 | For a given filter, query its available outputs by using the `inputs` property: 136 | 137 | ``` 138 | print(cimg.inputs('gaussianBlur')) 139 | ``` 140 | 141 | 142 | ## Define and Apply a Custom GPU Kernel to an Image 143 | 144 | In the Python prototyping environment, you can create a custom kernel by writing an inline shader in the Core Image Kernel Language. For example, write a color kernel by processing only the color fragment. The `src` keyword tells Python that the code enclosed in `"""` is kernel source code: 145 | 146 | ``` 147 | src = """ 148 | kernel vec4 crush_red(__sample img, float a, float b) { 149 | // Crush shadows from red. 150 | img.rgb *= smoothstep(a, b, img.r); 151 | return img; 152 | } 153 | """ 154 | ``` 155 | 156 | Apply the GPU kernel to an image by calling `applyKernel`: 157 | 158 | ``` 159 | img2 = img.applyKernel(src, # kernel source code 160 | 0.25, # kernel arg 1 161 | 0.9) # kernel arg 2 162 | show([img, img2], title=['input', 'GPU color kernel']) 163 | ``` 164 | 165 | For instance, apply a general bilateral filter with the following kernel, written in the Core Image Kernel Language: 166 | 167 | ``` 168 | src = """ 169 | kernel vec4 bilateral(sampler u, float k, float colorInv, float spatialInv) 170 | { 171 | vec2 dc = destCoord(); 172 | vec2 pu = samplerCoord(u); 173 | vec2 uDelta = samplerTransform(u, dc+vec2(1.0)) - pu; 174 | vec4 u_0 = sample(u, pu); 175 | vec4 C = vec4(0.0); 176 | float W = 0.0; 177 | for (float x = -k; x <= k; x++) { 178 | for (float y = -k; y <= k; y++){ 179 | float ws = exp(-(x*x+y*y) * spatialInv); 180 | vec4 u_xy = sample(u, pu + vec2(x,y)*uDelta); 181 | vec3 diff = u_xy.rgb-u_0.rgb; 182 | float wc = exp(-dot(diff,diff) * colorInv); 183 | W += ws * wc; 184 | C += ws * wc * u_xy; 185 | } 186 | } 187 | return W < 0.0001 ? u_0 : C / W; 188 | } 189 | """ 190 | ``` 191 | 192 | ## Generate a QR Code from Data 193 | 194 | A number of `CIFilter`s, like all barcode-creating filters, generate procedural images and don't take input images. For example, you can create a QR code from arbitrary text with the `fromGenerator` function: 195 | 196 | ``` 197 | cimg.fromGenerator('CIQRCodeGenerator', message='Hello World!') 198 | ``` 199 | 200 | ## Apply Geometrical Transformations 201 | 202 | Shift the image by the amount `(tx, ty)` with the `translate` command: 203 | 204 | ``` 205 | img.translate(tx, ty) 206 | ``` 207 | 208 | Use the `scale` command to resize an image: 209 | 210 | ``` 211 | img.scale(sx, sy) 212 | ``` 213 | 214 | Rotate the image about its center point with the `rotate` command: 215 | 216 | ``` 217 | img.rotate(radians) 218 | ``` 219 | 220 | Crop the image to the rectangle `[x, y, width, height]` with the `crop` command: 221 | 222 | ``` 223 | img.crop(0, 0, 1024, 768) 224 | ``` 225 | 226 | ## Fine-Tune Your Output Image Live 227 | 228 | One advantage to using Python to prototype a filter chain is its immediate feedback. Write out the resulting image with the `save` command: 229 | 230 | ``` 231 | img.save('demo2.jpg') 232 | ``` 233 | 234 | Calling `show` displays the image onscreen in the Python editor: 235 | 236 | ``` 237 | show(img, title='Demo 2: from file + slicing') 238 | ``` 239 | 240 | The sample code contains a number of other common image-processing routines you can customize for your app, such as sharpening kernels, zoom and motion blur, and image slicing. Create a set of test images and run the code on them to see the effects live. 241 | -------------------------------------------------------------------------------- /pycoreimage/pyci_demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | pycoreimage 3 | 4 | Copyright 2018 Apple Inc. All rights reserved. 5 | 6 | # Install 7 | 1. pip install pyobjc --ignore-installed --user 8 | 2. pip install numpy --ignore-installed --user 9 | 3. pip install scikit-image --user 10 | 11 | """ 12 | 13 | from pycoreimage.pyci import * 14 | 15 | 16 | def demo_metadata(fpath): 17 | img = cimg.fromFile(fpath) 18 | depth = cimg.fromFile(fpath, useDepth=True) 19 | matte = cimg.fromFile(fpath, useMatte=True) 20 | 21 | print(img.ciimage.properties()) 22 | 23 | # show([img, depth, matte], title=['input', 'depth', 'matte']) 24 | show(img, at=221, title='RGB {}x{}'.format(*img.size)) 25 | show(depth, at=223, title='depth {}x{}'.format(*depth.size)) 26 | show(matte, at=224, title='matte {}x{}'.format(*matte.size)) 27 | 28 | 29 | def demo_filters(fpath): 30 | """ Example: filters, loading, saving """ 31 | 32 | # Support for most common image file types, including raw. 33 | img = cimg.fromFile(fpath) 34 | 35 | # Check our image type 36 | print(type(img)) 37 | 38 | # Inspect the backing CIImage 39 | print(img.ciimage) 40 | 41 | # Print available filters 42 | for i, f in enumerate(cimg.filters()): 43 | print('{:3d} {}'.format(i, f)) 44 | 45 | # Print more info (including inputs) for a given filter 46 | print(cimg.inputs('gaussianBlur')) 47 | 48 | radii = [10, 50, 100] 49 | for i, r in enumerate(radii): 50 | # Apply a Gaussian blur filter on the input image 51 | # Note: can also use the full filter name "CIGaussianBlur" 52 | blur = img.gaussianBlur(radius=r) 53 | 54 | # Save to disk 55 | blur.save(fpath + '.CIGaussianBlur{}.jpg'.format(r)) 56 | 57 | # Display on screen 58 | show(blur, at='1{}{}'.format(len(radii), i + 1), title='blur with radius={}'.format(r)) 59 | 60 | 61 | def demo_generators(): 62 | """ Example: CoreImage generators. """ 63 | 64 | qrcode = cimg.fromGenerator('CIQRCodeGenerator', message='robot barf') 65 | checker = cimg.fromGenerator('CICheckerboardGenerator', crop=1024) 66 | stripes = cimg.fromGenerator('CIStripesGenerator', crop=1024) 67 | 68 | show([qrcode, checker, stripes], title=['QR code', 'Checkboard', 'Stripes'], interpolation='nearest') 69 | 70 | 71 | def demo_numpy_to(fpath): 72 | """ Example: from CoreImage to NumPy """ 73 | import numpy as np 74 | 75 | # Apply a non trivial effect 76 | img = cimg.fromFile(fpath) 77 | vib = img.vibrance(amount=1.0) 78 | 79 | # Render to a NumPy array 80 | ary = vib.render(); 81 | 82 | # Operate on the NumPy array 83 | print(ary[0, 0, 0]) 84 | print(ary.min()) 85 | coefs = 0.299, 0.587, 0.114 86 | lum = np.tensordot(ary, coefs, axes=([2, 0])) 87 | 88 | show([img, vib, lum], title=['input', 'vibrance', 'luminance']) 89 | 90 | 91 | def demo_numpy_from(): 92 | """ Example: from NumPy to CoreImage """ 93 | import numpy as np 94 | 95 | # Create a NumPy array 96 | noise = np.random.rand(512, 512, 3) 97 | noise[noise < 0.75] = 0 98 | show(noise, title='NumPy', interpolation='nearest') 99 | 100 | # CoreImage convenience wrapper 101 | img = cimg(noise) 102 | print(type(img)) 103 | 104 | # Apply filters 105 | img = img.discBlur(radius=10).photoEffectChrome() 106 | img = img.lightTunnel(center=(256, 256), radius=64) 107 | img = img.exposureAdjust(EV=2) 108 | img = img.gammaAdjust(power=2) 109 | 110 | show(img, title='NumPy to Core Image') 111 | 112 | 113 | def demo_slices(fpath): 114 | """ Example: NumPy-style slicing. """ 115 | import numpy as np 116 | 117 | img = cimg.fromFile(fpath) 118 | 119 | # Resize 120 | s = img.size 121 | img = img.resize(1024, preserveAspect=1) 122 | show(img, title='Resized from {} to {}'.format(s, img.size)) 123 | 124 | # Create a blank NumPy array 125 | labelWidth = 400 126 | composite = np.zeros((img.size[1], img.size[0] + labelWidth, 3)) 127 | rows = img.size[1] // 5 128 | show(composite) 129 | 130 | # Create our band processing function 131 | def addBand(i, name, args): 132 | # Band indices 133 | lo, hi = i * rows, (i + 1) * rows 134 | 135 | # Apply filter via explicit name and args 136 | band = img.applyFilter(name, **args) 137 | 138 | # Create a label with the filter name 139 | label = cimg.fromGenerator('CITextImageGenerator', 140 | text=name, 141 | fontName='HelveticaNeue', 142 | fontSize=40, 143 | scaleFactor=1) 144 | 145 | # Make the text red 146 | label = label.colorInvert().whitePointAdjust(color=color(1, 0, 0, 1)) 147 | 148 | # Translate to left hand side 149 | label = label.translate(-labelWidth, composite.shape[0] - hi) 150 | 151 | # Composite over the image 152 | band = label.over(band) 153 | 154 | # Slice the CIImage here: render only happens in that band 155 | composite[lo:hi, ...] = band[lo:hi, ...] 156 | show(composite) 157 | 158 | # Create composite bands using various filters 159 | addBand(0, 'pixellate', {'center': (0, 0), 'scale': 10}) 160 | addBand(1, 'edgeWork', {'radius': 3}) 161 | addBand(2, 'gaussianBlur', {'radius': 5}) 162 | addBand(3, 'comicEffect', {}) 163 | addBand(4, 'hexagonalPixellate', {'center': (0, 0), 'scale': 10}) 164 | 165 | 166 | def demo_gpu_color(fpath): 167 | """ Example: GPU color kernel """ 168 | 169 | img = cimg.fromFile(fpath) 170 | 171 | # GPU shader written in the Core Image Kernel Language 172 | # This is a Color Kernel, since only the color fragment is processed 173 | src = """ 174 | kernel vec4 crush_red(__sample img, float a, float b) { 175 | // Crush shadows from red 176 | img.rgb *= smoothstep(a, b, img.r); 177 | return img; 178 | } 179 | """ 180 | # Apply 181 | img2 = img.applyKernel(src, # kernel source code 182 | 0.25, # kernel arg 1 183 | 0.9) # kernel arg 2 184 | show([img, img2], title=['input', 'GPU color kernel']) 185 | 186 | 187 | def demo_gpu_general(fpath): 188 | """ Example: GPU general kernel """ 189 | img = cimg.fromFile(fpath) 190 | img = img.resize(512, preserveAspect=2) 191 | 192 | # Bilateral filter written in the Core Image Kernel Language 193 | src = """ 194 | kernel vec4 bilateral(sampler u, float k, float colorInv, float spatialInv) 195 | { 196 | vec2 dc = destCoord(); 197 | vec2 pu = samplerCoord(u); 198 | vec2 uDelta = samplerTransform(u, dc+vec2(1.0)) - pu; 199 | vec4 u_0 = sample(u, pu); 200 | vec4 C = vec4(0.0); 201 | float W = 0.0; 202 | for (float x = -k; x <= k; x++) { 203 | for (float y = -k; y <= k; y++){ 204 | float ws = exp(-(x*x+y*y) * spatialInv); 205 | vec4 u_xy = sample(u, pu + vec2(x,y)*uDelta); 206 | vec3 diff = u_xy.rgb-u_0.rgb; 207 | float wc = exp(-dot(diff,diff) * colorInv); 208 | W += ws * wc; 209 | C += ws * wc * u_xy; 210 | } 211 | } 212 | return W < 0.0001 ? u_0 : C / W; 213 | } 214 | """ 215 | 216 | # Apply 217 | sigmaSpatial = 20 218 | sigmaColor = 0.15 219 | bil = img.applyKernel(src, # kernel source 220 | 3 * sigmaSpatial, # kernel arg 1 221 | sigmaColor ** -2, # kernel arg 2 222 | sigmaSpatial ** -2, # kernel arg 3 223 | # region of interest (ROI) callback 224 | roi=lambda index, r: inset(r, 225 | -3 * sigmaSpatial, 226 | -3 * sigmaSpatial)) 227 | 228 | show([img, bil], title=['input', 'bilateral']) 229 | 230 | # Create the detail layer 231 | details = img.render() - bil.render() 232 | show((details - details.min()) / (details.max() - details.min()) ** 1.5, title='detail layer') 233 | 234 | # Bilateral sharpen 235 | result = img.render() + 1.5 * details 236 | show([img, result], title=['input', 'bilateral sharpening']) 237 | 238 | 239 | def demo_geometry(paths): 240 | """ Example: Affine transformations. """ 241 | import numpy as np 242 | 243 | # Load our images in 244 | imgs = [cimg.fromFile(path) for path in paths] 245 | 246 | # Composite params 247 | n = 100 248 | size = 1024 249 | composite = None 250 | np.random.seed(3) 251 | 252 | # Utility randomization function 253 | def randomize(bar, atCenter=None): 254 | # Apply random scale 255 | scale = 0.025 + 0.075 * np.random.rand() 256 | 257 | if atCenter: 258 | scale = 0.1 259 | 260 | bar = bar.scale(scale, scale) 261 | 262 | # Zero origin 263 | w, h = bar.size 264 | bar = bar.translate(-h / 2, -w / 2) 265 | 266 | # Apply random rotation 267 | angle = np.random.rand() * 2.0 * np.pi 268 | bar = bar.rotate(angle) 269 | 270 | # Apply random translation 271 | tx = np.random.rand() * size 272 | ty = np.random.rand() * size 273 | 274 | if atCenter: 275 | tx, ty = atCenter 276 | 277 | bar = bar.translate(tx, ty) 278 | return bar 279 | 280 | # Create the composite 281 | for i in range(n): 282 | if composite: 283 | composite = composite.gaussianBlur(radius=1.5) 284 | 285 | # Pick next image 286 | bar = imgs[np.random.randint(0, len(imgs))] 287 | bar = randomize(bar, atCenter=(size / 2, size / 2) if i == n - 1 else None) 288 | 289 | # Composite over the image 290 | composite = bar if not composite else bar.over(composite) 291 | 292 | # Crop to input size 293 | composite = composite.crop(size, size) 294 | 295 | show(composite) 296 | 297 | 298 | def demo_depth(fpath): 299 | """ Example: depth processing """ 300 | import numpy as np 301 | 302 | # Load image 303 | foo = cimg.fromFile(fpath) 304 | W, H = foo.size 305 | 306 | # Load depth 307 | depth = cimg.fromFile(fpath, useDepth=True) 308 | w, h = depth.size 309 | 310 | # Diagonal 311 | d = np.sqrt(w ** 2 + h ** 2) 312 | 313 | # Params 314 | blur = min(100, 0.2 * d) 315 | morpho = blur / 6.0 316 | perc = 20 317 | 318 | # Threshold depth at 75th percentile 319 | depth_img = depth.render() 320 | p = np.percentile(depth_img, perc) 321 | mask = depth_img < p 322 | mask = cimg(mask.astype(np.float32)) 323 | mask = mask.morphologyMaximum(radius=morpho) 324 | mask = mask.gaussianBlur(radius=blur) 325 | mask = mask.render() 326 | 327 | # Downscale original 328 | ds = cimg(foo).scale(w / float(W), h / float(H)) 329 | 330 | # Desaturate the background 331 | bg = ds.photoEffectNoir() 332 | 333 | # Make the foreground stand out 334 | fg = ds.exposureAdjust(EV=0.5).colorControls(saturation=0.8, contrast=1.4) 335 | 336 | # Render 337 | bg = bg.render() 338 | fg = fg.render() 339 | 340 | # Make the foreground stand out 341 | result = mask * fg + (1 - mask) * bg 342 | 343 | # Show on screen 344 | show(ds, at=221, title='input') 345 | show(depth, at=222, color='jet', title='depth') 346 | show(result, at=223, title='result') 347 | show(mask[..., 0], at=224, color='gray', title='mask') 348 | 349 | 350 | def demo_depth_blur(fpath): 351 | """ Example: depth blur""" 352 | img = cimg.fromFile(fpath) 353 | matte = cimg.fromFile(fpath, useMatte=True) 354 | disparity = cimg.fromFile(fpath, useDisparity=True) 355 | 356 | for aperture in [2, 6, 22]: 357 | effect = img.depthBlurEffect( 358 | inputDisparityImage=disparity, 359 | inputMatteImage=matte, 360 | inputAperture=aperture 361 | ) 362 | show(effect, title='Aperture = {}'.format(aperture)) 363 | 364 | 365 | if __name__ == '__main__': 366 | 367 | import argparse, os 368 | 369 | # Support file formats for dataset demo 370 | exts = '.jpg', '.jpeg', '.heic', '.tiff', '.png' 371 | 372 | # print('Syntax: pycoreimage_sandbox.py img.jpg imgDepthMatte.heic /path/to/directory/') 373 | parser = argparse.ArgumentParser() 374 | 375 | parser.add_argument('image', help='input image', type=str) 376 | parser.add_argument('directory', help='directory containing images {}'.format(exts), type=str) 377 | parser.add_argument('--imageWithDepth', help='input image containing depth and Portrait Effects Matte', type=str) 378 | parser.add_argument('--tree', action='store_true', help='enable CI_PRINT_TREE=4') 379 | args = parser.parse_args() 380 | 381 | # CI_PRINT_TREE needs to be set before the first render call 382 | if args.tree: 383 | set_print_tree(4) 384 | 385 | # Input check 386 | abort = False 387 | if not os.path.exists(args.image): 388 | abort = True 389 | print('Image not found:', args.image) 390 | 391 | if args.imageWithDepth: 392 | if not os.path.exists(args.imageWithDepth): 393 | abort = True 394 | print('Depth+matte image not found:', args.imageWithDepth) 395 | 396 | if not os.path.exists(args.directory): 397 | abort = True 398 | print('Directory not found:', args.directory) 399 | 400 | # Only process selected bitmap file formats 401 | dataset = [] 402 | if not abort: 403 | dataset = os.listdir(args.directory) 404 | dataset = [os.path.join(args.directory, f) for f in dataset if os.path.splitext(f)[1].lower() in exts] 405 | 406 | if len(dataset) == 0: 407 | abort = True 408 | print('No valid bitmap ({}) found in:'.format(exts), args.directory) 409 | 410 | if abort: 411 | exit(1) 412 | 413 | print('Using input image', args.image) 414 | 415 | if args.imageWithDepth: 416 | print('Using input image with depth', args.imageWithDepth) 417 | else: 418 | print('Not using an image with depth. Set --imageWithDepth') 419 | 420 | print('Using dataset', dataset) 421 | 422 | # Demos 423 | 424 | print('Running demo: filters') 425 | demo_filters(args.image) 426 | 427 | print('Running demo: generators') 428 | demo_generators() 429 | 430 | print('Running demo: to numpy') 431 | demo_numpy_to(args.image) 432 | 433 | print('Running demo: from numpy') 434 | demo_numpy_from() 435 | 436 | print('Running demo: slicing') 437 | demo_slices(args.image) 438 | 439 | print('Running demo: GPU kernels (color)') 440 | demo_gpu_color(args.image) 441 | 442 | print('Running demo: GPU kernels (general)') 443 | demo_gpu_general(args.image) 444 | 445 | print('Running demo: geometry dataset') 446 | demo_geometry(dataset) 447 | 448 | if args.imageWithDepth: 449 | try: 450 | print('Running demo: depth processing') 451 | demo_depth(args.imageWithDepth) 452 | 453 | print('Running demo: depth blur') 454 | demo_depth_blur(args.imageWithDepth) 455 | 456 | except Exception as e: 457 | print('Encountered an exception while preparing the Portrait depth and matte demo', e) 458 | print('Make sure your input image ({}) has both embedded as metadata.'.format(args.imageWithDepth)) 459 | -------------------------------------------------------------------------------- /CoreImagePython.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5670B6CB20B63398003E6B64 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5670B6CA20B63398003E6B64 /* main.swift */; }; 11 | 5679C26C20B9DE0A00BF1244 /* pyci_demo.py in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5679C26B20B9DDFB00BF1244 /* pyci_demo.py */; }; 12 | 5679C26D20B9DE0A00BF1244 /* pyci.py in CopyFiles */ = {isa = PBXBuildFile; fileRef = 56AFAD9720B634CB002A7A12 /* pyci.py */; }; 13 | 5679C29020B9E04F00BF1244 /* Sunset_5.jpg in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5679C27320B9E04200BF1244 /* Sunset_5.jpg */; }; 14 | 5679C29520B9E04F00BF1244 /* Sunset_6.jpg in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5679C27820B9E04200BF1244 /* Sunset_6.jpg */; }; 15 | 5679C29620B9E04F00BF1244 /* GGBridge_2.jpg in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5679C27920B9E04200BF1244 /* GGBridge_2.jpg */; }; 16 | 5679C29820B9E04F00BF1244 /* Food_3.jpg in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5679C27B20B9E04200BF1244 /* Food_3.jpg */; }; 17 | 5679C29C20B9E04F00BF1244 /* Food_1.jpg in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5679C27F20B9E04200BF1244 /* Food_1.jpg */; }; 18 | 5679C2A120B9E04F00BF1244 /* Flowers_1.jpg in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5679C28420B9E04200BF1244 /* Flowers_1.jpg */; }; 19 | 5679C2AA20B9E04F00BF1244 /* Landscape_3.jpg in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5679C28D20B9E04200BF1244 /* Landscape_3.jpg */; }; 20 | 5679C2AC20B9E04F00BF1244 /* Landscape_1.jpg in CopyFiles */ = {isa = PBXBuildFile; fileRef = 5679C28F20B9E04200BF1244 /* Landscape_1.jpg */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXCopyFilesBuildPhase section */ 24 | 5670B6C520B63398003E6B64 /* CopyFiles */ = { 25 | isa = PBXCopyFilesBuildPhase; 26 | buildActionMask = 12; 27 | dstPath = ""; 28 | dstSubfolderSpec = 6; 29 | files = ( 30 | 5679C29020B9E04F00BF1244 /* Sunset_5.jpg in CopyFiles */, 31 | 5679C29520B9E04F00BF1244 /* Sunset_6.jpg in CopyFiles */, 32 | 5679C29620B9E04F00BF1244 /* GGBridge_2.jpg in CopyFiles */, 33 | 5679C29820B9E04F00BF1244 /* Food_3.jpg in CopyFiles */, 34 | 5679C29C20B9E04F00BF1244 /* Food_1.jpg in CopyFiles */, 35 | 5679C2A120B9E04F00BF1244 /* Flowers_1.jpg in CopyFiles */, 36 | 5679C2AA20B9E04F00BF1244 /* Landscape_3.jpg in CopyFiles */, 37 | 5679C2AC20B9E04F00BF1244 /* Landscape_1.jpg in CopyFiles */, 38 | 5679C26C20B9DE0A00BF1244 /* pyci_demo.py in CopyFiles */, 39 | 5679C26D20B9DE0A00BF1244 /* pyci.py in CopyFiles */, 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | /* End PBXCopyFilesBuildPhase section */ 44 | 45 | /* Begin PBXFileReference section */ 46 | 5670B6C720B63398003E6B64 /* CoreImagePython */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = CoreImagePython; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 5670B6CA20B63398003E6B64 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 48 | 5679C26B20B9DDFB00BF1244 /* pyci_demo.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = pyci_demo.py; sourceTree = ""; }; 49 | 5679C27320B9E04200BF1244 /* Sunset_5.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Sunset_5.jpg; sourceTree = ""; }; 50 | 5679C27820B9E04200BF1244 /* Sunset_6.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Sunset_6.jpg; sourceTree = ""; }; 51 | 5679C27920B9E04200BF1244 /* GGBridge_2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = GGBridge_2.jpg; sourceTree = ""; }; 52 | 5679C27B20B9E04200BF1244 /* Food_3.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Food_3.jpg; sourceTree = ""; }; 53 | 5679C27F20B9E04200BF1244 /* Food_1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Food_1.jpg; sourceTree = ""; }; 54 | 5679C28420B9E04200BF1244 /* Flowers_1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Flowers_1.jpg; sourceTree = ""; }; 55 | 5679C28D20B9E04200BF1244 /* Landscape_3.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Landscape_3.jpg; sourceTree = ""; }; 56 | 5679C28F20B9E04200BF1244 /* Landscape_1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = Landscape_1.jpg; sourceTree = ""; }; 57 | 5694223620C454A9008DCBA8 /* setup.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = setup.py; sourceTree = SOURCE_ROOT; }; 58 | 5694223720C45580008DCBA8 /* pyci_launcher.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; name = pyci_launcher.py; path = pycoreimage/pyci_launcher.py; sourceTree = SOURCE_ROOT; }; 59 | 56AFAD8F20B633C0002A7A12 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 60 | 56AFAD9620B634CB002A7A12 /* __init__.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = __init__.py; sourceTree = ""; }; 61 | 56AFAD9720B634CB002A7A12 /* pyci.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = pyci.py; sourceTree = ""; }; 62 | E7C63110E7C6314000000001 /* LICENSE.txt */ = {isa = PBXFileReference; includeInIndex = 1; path = LICENSE.txt; sourceTree = ""; }; 63 | E7D62850E7D93EE000000001 /* SampleCode.xcconfig */ = {isa = PBXFileReference; name = SampleCode.xcconfig; path = Configuration/SampleCode.xcconfig; sourceTree = ""; }; 64 | /* End PBXFileReference section */ 65 | 66 | /* Begin PBXFrameworksBuildPhase section */ 67 | 5670B6C420B63398003E6B64 /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXFrameworksBuildPhase section */ 75 | 76 | /* Begin PBXGroup section */ 77 | 5670B6BE20B63398003E6B64 = { 78 | isa = PBXGroup; 79 | children = ( 80 | 56AFAD8F20B633C0002A7A12 /* README.md */, 81 | 5670B6C920B63398003E6B64 /* CoreImagePython */, 82 | 5670B6C820B63398003E6B64 /* Products */, 83 | E7D60620E7D5FB0000000001 /* Configuration */, 84 | E7F6F070E7F6F04000000001 /* LICENSE */, 85 | ); 86 | sourceTree = ""; 87 | }; 88 | 5670B6C820B63398003E6B64 /* Products */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 5670B6C720B63398003E6B64 /* CoreImagePython */, 92 | ); 93 | name = Products; 94 | sourceTree = ""; 95 | }; 96 | 5670B6C920B63398003E6B64 /* CoreImagePython */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 5670B6CA20B63398003E6B64 /* main.swift */, 100 | 5694223620C454A9008DCBA8 /* setup.py */, 101 | 56AFAD9420B634CB002A7A12 /* pycoreimage */, 102 | ); 103 | path = CoreImagePython; 104 | sourceTree = ""; 105 | }; 106 | 5679C27220B9E04200BF1244 /* resources */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | 5679C27320B9E04200BF1244 /* Sunset_5.jpg */, 110 | 5679C27820B9E04200BF1244 /* Sunset_6.jpg */, 111 | 5679C27920B9E04200BF1244 /* GGBridge_2.jpg */, 112 | 5679C27B20B9E04200BF1244 /* Food_3.jpg */, 113 | 5679C27F20B9E04200BF1244 /* Food_1.jpg */, 114 | 5679C28420B9E04200BF1244 /* Flowers_1.jpg */, 115 | 5679C28D20B9E04200BF1244 /* Landscape_3.jpg */, 116 | 5679C28F20B9E04200BF1244 /* Landscape_1.jpg */, 117 | ); 118 | path = resources; 119 | sourceTree = ""; 120 | }; 121 | 56AFAD9420B634CB002A7A12 /* pycoreimage */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 56AFAD9620B634CB002A7A12 /* __init__.py */, 125 | 5694223720C45580008DCBA8 /* pyci_launcher.py */, 126 | 5679C26B20B9DDFB00BF1244 /* pyci_demo.py */, 127 | 56AFAD9720B634CB002A7A12 /* pyci.py */, 128 | 5679C27220B9E04200BF1244 /* resources */, 129 | ); 130 | path = pycoreimage; 131 | sourceTree = SOURCE_ROOT; 132 | }; 133 | E7D60620E7D5FB0000000001 /* Configuration */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | E7D62850E7D93EE000000001 /* SampleCode.xcconfig */, 137 | ); 138 | name = Configuration; 139 | sourceTree = ""; 140 | }; 141 | E7F6F070E7F6F04000000001 /* LICENSE */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | E7C63110E7C6314000000001 /* LICENSE.txt */, 145 | ); 146 | name = LICENSE; 147 | path = LICENSE; 148 | sourceTree = ""; 149 | }; 150 | /* End PBXGroup section */ 151 | 152 | /* Begin PBXNativeTarget section */ 153 | 5670B6C620B63398003E6B64 /* CoreImagePython */ = { 154 | isa = PBXNativeTarget; 155 | buildConfigurationList = 5670B6CE20B63398003E6B64 /* Build configuration list for PBXNativeTarget "CoreImagePython" */; 156 | buildPhases = ( 157 | 5670B6C320B63398003E6B64 /* Sources */, 158 | 5670B6C420B63398003E6B64 /* Frameworks */, 159 | 5670B6C520B63398003E6B64 /* CopyFiles */, 160 | ); 161 | buildRules = ( 162 | ); 163 | dependencies = ( 164 | ); 165 | name = CoreImagePython; 166 | productName = CoreImagePython; 167 | productReference = 5670B6C720B63398003E6B64 /* CoreImagePython */; 168 | productType = "com.apple.product-type.tool"; 169 | }; 170 | /* End PBXNativeTarget section */ 171 | 172 | /* Begin PBXLegacyTarget section */ 173 | 5694223120C35F8E008DCBA8 /* pycoreimage */ = { 174 | isa = PBXLegacyTarget; 175 | buildArgumentsString = "$(ACTION)"; 176 | buildConfigurationList = 5694223420C35F8E008DCBA8 /* Build configuration list for PBXLegacyTarget "pycoreimage" */; 177 | buildPhases = ( 178 | ); 179 | buildRules = ( 180 | ); 181 | buildToolPath = /usr/bin/python; 182 | dependencies = ( 183 | ); 184 | name = pycoreimage; 185 | passBuildSettingsInEnvironment = 1; 186 | productName = pycoreimage; 187 | }; 188 | /* End PBXLegacyTarget section */ 189 | 190 | /* Begin PBXProject section */ 191 | 5670B6BF20B63398003E6B64 /* Project object */ = { 192 | isa = PBXProject; 193 | attributes = { 194 | LastSwiftUpdateCheck = 0930; 195 | LastUpgradeCheck = 1000; 196 | ORGANIZATIONNAME = Apple; 197 | TargetAttributes = { 198 | 5670B6C620B63398003E6B64 = { 199 | CreatedOnToolsVersion = 9.3; 200 | }; 201 | 5694223120C35F8E008DCBA8 = { 202 | CreatedOnToolsVersion = 10.0; 203 | }; 204 | }; 205 | }; 206 | buildConfigurationList = 5670B6C220B63398003E6B64 /* Build configuration list for PBXProject "CoreImagePython" */; 207 | compatibilityVersion = "Xcode 9.3"; 208 | developmentRegion = en; 209 | hasScannedForEncodings = 0; 210 | knownRegions = ( 211 | en, 212 | ); 213 | mainGroup = 5670B6BE20B63398003E6B64; 214 | productRefGroup = 5670B6C820B63398003E6B64 /* Products */; 215 | projectDirPath = ""; 216 | projectRoot = ""; 217 | targets = ( 218 | 5670B6C620B63398003E6B64 /* CoreImagePython */, 219 | 5694223120C35F8E008DCBA8 /* pycoreimage */, 220 | ); 221 | }; 222 | /* End PBXProject section */ 223 | 224 | /* Begin PBXSourcesBuildPhase section */ 225 | 5670B6C320B63398003E6B64 /* Sources */ = { 226 | isa = PBXSourcesBuildPhase; 227 | buildActionMask = 2147483647; 228 | files = ( 229 | 5670B6CB20B63398003E6B64 /* main.swift in Sources */, 230 | ); 231 | runOnlyForDeploymentPostprocessing = 0; 232 | }; 233 | /* End PBXSourcesBuildPhase section */ 234 | 235 | /* Begin XCBuildConfiguration section */ 236 | 5670B6CC20B63398003E6B64 /* Debug */ = { 237 | isa = XCBuildConfiguration; 238 | baseConfigurationReference = E7D62850E7D93EE000000001 /* SampleCode.xcconfig */; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_ANALYZER_NONNULL = YES; 242 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 243 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 244 | CLANG_CXX_LIBRARY = "libc++"; 245 | CLANG_ENABLE_MODULES = YES; 246 | CLANG_ENABLE_OBJC_ARC = YES; 247 | CLANG_ENABLE_OBJC_WEAK = YES; 248 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 249 | CLANG_WARN_BOOL_CONVERSION = YES; 250 | CLANG_WARN_COMMA = YES; 251 | CLANG_WARN_CONSTANT_CONVERSION = YES; 252 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 253 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 254 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 255 | CLANG_WARN_EMPTY_BODY = YES; 256 | CLANG_WARN_ENUM_CONVERSION = YES; 257 | CLANG_WARN_INFINITE_RECURSION = YES; 258 | CLANG_WARN_INT_CONVERSION = YES; 259 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 260 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 261 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 263 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 264 | CLANG_WARN_STRICT_PROTOTYPES = YES; 265 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 266 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | CODE_SIGN_IDENTITY = "Mac Developer"; 270 | COPY_PHASE_STRIP = NO; 271 | DEBUG_INFORMATION_FORMAT = dwarf; 272 | ENABLE_STRICT_OBJC_MSGSEND = YES; 273 | ENABLE_TESTABILITY = YES; 274 | GCC_C_LANGUAGE_STANDARD = gnu11; 275 | GCC_DYNAMIC_NO_PIC = NO; 276 | GCC_NO_COMMON_BLOCKS = YES; 277 | GCC_OPTIMIZATION_LEVEL = 0; 278 | GCC_PREPROCESSOR_DEFINITIONS = ( 279 | "DEBUG=1", 280 | "$(inherited)", 281 | ); 282 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 283 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 284 | GCC_WARN_UNDECLARED_SELECTOR = YES; 285 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 286 | GCC_WARN_UNUSED_FUNCTION = YES; 287 | GCC_WARN_UNUSED_VARIABLE = YES; 288 | MACOSX_DEPLOYMENT_TARGET = 10.14; 289 | MTL_ENABLE_DEBUG_INFO = YES; 290 | ONLY_ACTIVE_ARCH = YES; 291 | SDKROOT = macosx; 292 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 294 | }; 295 | name = Debug; 296 | }; 297 | 5670B6CD20B63398003E6B64 /* Release */ = { 298 | isa = XCBuildConfiguration; 299 | baseConfigurationReference = E7D62850E7D93EE000000001 /* SampleCode.xcconfig */; 300 | buildSettings = { 301 | ALWAYS_SEARCH_USER_PATHS = NO; 302 | CLANG_ANALYZER_NONNULL = YES; 303 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 304 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 305 | CLANG_CXX_LIBRARY = "libc++"; 306 | CLANG_ENABLE_MODULES = YES; 307 | CLANG_ENABLE_OBJC_ARC = YES; 308 | CLANG_ENABLE_OBJC_WEAK = YES; 309 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 310 | CLANG_WARN_BOOL_CONVERSION = YES; 311 | CLANG_WARN_COMMA = YES; 312 | CLANG_WARN_CONSTANT_CONVERSION = YES; 313 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 314 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 315 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 316 | CLANG_WARN_EMPTY_BODY = YES; 317 | CLANG_WARN_ENUM_CONVERSION = YES; 318 | CLANG_WARN_INFINITE_RECURSION = YES; 319 | CLANG_WARN_INT_CONVERSION = YES; 320 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 321 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 322 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 323 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 324 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 325 | CLANG_WARN_STRICT_PROTOTYPES = YES; 326 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 327 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 328 | CLANG_WARN_UNREACHABLE_CODE = YES; 329 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 330 | CODE_SIGN_IDENTITY = "Mac Developer"; 331 | COPY_PHASE_STRIP = NO; 332 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 333 | ENABLE_NS_ASSERTIONS = NO; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | GCC_C_LANGUAGE_STANDARD = gnu11; 336 | GCC_NO_COMMON_BLOCKS = YES; 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | MACOSX_DEPLOYMENT_TARGET = 10.14; 344 | MTL_ENABLE_DEBUG_INFO = NO; 345 | SDKROOT = macosx; 346 | SWIFT_COMPILATION_MODE = wholemodule; 347 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 348 | }; 349 | name = Release; 350 | }; 351 | 5670B6CF20B63398003E6B64 /* Debug */ = { 352 | isa = XCBuildConfiguration; 353 | baseConfigurationReference = E7D62850E7D93EE000000001 /* SampleCode.xcconfig */; 354 | buildSettings = { 355 | CODE_SIGN_IDENTITY = "Mac Developer"; 356 | CODE_SIGN_STYLE = Automatic; 357 | DEVELOPMENT_TEAM = ""; 358 | PRODUCT_NAME = "$(TARGET_NAME)"; 359 | PROVISIONING_PROFILE_SPECIFIER = ""; 360 | SWIFT_VERSION = 4.0; 361 | }; 362 | name = Debug; 363 | }; 364 | 5670B6D020B63398003E6B64 /* Release */ = { 365 | isa = XCBuildConfiguration; 366 | baseConfigurationReference = E7D62850E7D93EE000000001 /* SampleCode.xcconfig */; 367 | buildSettings = { 368 | CODE_SIGN_IDENTITY = "Mac Developer"; 369 | CODE_SIGN_STYLE = Automatic; 370 | DEVELOPMENT_TEAM = ""; 371 | PRODUCT_NAME = "$(TARGET_NAME)"; 372 | PROVISIONING_PROFILE_SPECIFIER = ""; 373 | SWIFT_VERSION = 4.0; 374 | }; 375 | name = Release; 376 | }; 377 | 5694223220C35F8E008DCBA8 /* Debug */ = { 378 | isa = XCBuildConfiguration; 379 | baseConfigurationReference = E7D62850E7D93EE000000001 /* SampleCode.xcconfig */; 380 | buildSettings = { 381 | CODE_SIGN_STYLE = Automatic; 382 | DEBUGGING_SYMBOLS = YES; 383 | DEBUG_INFORMATION_FORMAT = dwarf; 384 | DEVELOPMENT_TEAM = ""; 385 | GCC_GENERATE_DEBUGGING_SYMBOLS = YES; 386 | GCC_OPTIMIZATION_LEVEL = 0; 387 | OTHER_CFLAGS = ""; 388 | OTHER_LDFLAGS = ""; 389 | PRODUCT_NAME = "$(TARGET_NAME)"; 390 | PROVISIONING_PROFILE_SPECIFIER = ""; 391 | }; 392 | name = Debug; 393 | }; 394 | 5694223320C35F8E008DCBA8 /* Release */ = { 395 | isa = XCBuildConfiguration; 396 | baseConfigurationReference = E7D62850E7D93EE000000001 /* SampleCode.xcconfig */; 397 | buildSettings = { 398 | CODE_SIGN_STYLE = Automatic; 399 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 400 | DEVELOPMENT_TEAM = ""; 401 | OTHER_CFLAGS = ""; 402 | OTHER_LDFLAGS = ""; 403 | PRODUCT_NAME = "$(TARGET_NAME)"; 404 | PROVISIONING_PROFILE_SPECIFIER = ""; 405 | }; 406 | name = Release; 407 | }; 408 | /* End XCBuildConfiguration section */ 409 | 410 | /* Begin XCConfigurationList section */ 411 | 5670B6C220B63398003E6B64 /* Build configuration list for PBXProject "CoreImagePython" */ = { 412 | isa = XCConfigurationList; 413 | buildConfigurations = ( 414 | 5670B6CC20B63398003E6B64 /* Debug */, 415 | 5670B6CD20B63398003E6B64 /* Release */, 416 | ); 417 | defaultConfigurationIsVisible = 0; 418 | defaultConfigurationName = Release; 419 | }; 420 | 5670B6CE20B63398003E6B64 /* Build configuration list for PBXNativeTarget "CoreImagePython" */ = { 421 | isa = XCConfigurationList; 422 | buildConfigurations = ( 423 | 5670B6CF20B63398003E6B64 /* Debug */, 424 | 5670B6D020B63398003E6B64 /* Release */, 425 | ); 426 | defaultConfigurationIsVisible = 0; 427 | defaultConfigurationName = Release; 428 | }; 429 | 5694223420C35F8E008DCBA8 = { 430 | isa = XCConfigurationList; 431 | buildConfigurations = ( 432 | 5694223220C35F8E008DCBA8 /* Debug */, 433 | 5694223320C35F8E008DCBA8 /* Release */, 434 | ); 435 | defaultConfigurationIsVisible = 0; 436 | defaultConfigurationName = Release; 437 | }; 438 | /* End XCConfigurationList section */ 439 | }; 440 | rootObject = 5670B6BF20B63398003E6B64 /* Project object */; 441 | } 442 | -------------------------------------------------------------------------------- /pycoreimage/pyci.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyci.py 3 | Python bindings for CoreImage 4 | 5 | Copyright 2018 Apple Inc. All rights reserved. 6 | 7 | # Install 8 | 1. pip install pyobjc --ignore-installed --user 9 | 2. pip install numpy --ignore-installed --user 10 | 11 | """ 12 | 13 | """ Flags """ 14 | FLAG_DEBUG = False 15 | 16 | """ Standard imports """ 17 | from warnings import warn 18 | import inspect 19 | from difflib import get_close_matches 20 | import os 21 | 22 | """ Frameworks """ 23 | import numpy as np 24 | from numpy import ndarray 25 | 26 | """ Foundation """ 27 | from Foundation import NSData, NSURL, NSMutableData 28 | 29 | """ Core Graphics """ 30 | from Quartz import CoreGraphics 31 | from Quartz import CGDataProviderCreateWithCFData, CGImageCreate 32 | from Quartz import CGColorSpaceCreateWithName, CGColorSpaceCreateDeviceGray 33 | from Quartz import CGAffineTransform, CGAffineTransformMake, CGAffineTransformMakeScale, \ 34 | CGAffineTransformMakeTranslation, CGAffineTransformMakeRotation 35 | from Quartz import CGRect, CGSize, CGPoint 36 | from Quartz import CGRectMake, CGRectInset, CGRectIntegral 37 | 38 | """ Core Image """ 39 | from Quartz import CIImage, CIFilter, CIContext, CIColor, CIVector, CIKernel, CIColorKernel 40 | 41 | """ Constants """ 42 | from Quartz import kCGBitmapByteOrderDefault, kCGRenderingIntentDefault 43 | from Quartz import kCGBitmapFloatComponents, kCGBitmapByteOrder32Little, kCGBitmapByteOrder16Little 44 | from Quartz import kCGColorSpaceSRGB, kCGColorSpaceLinearSRGB 45 | 46 | # Enumerate missing keys 47 | kCIFormatRGBA8 = 24 48 | kCIFormatRGBAh = 31 49 | kCIFormatRGBAf = 34 50 | # kCIFormatRGB8 = 19 51 | # kCIFormatRGBh = 30 52 | # kCIFormatRGBf = 32 53 | 54 | kCIContextOutputColorSpace = 'output_color_space' 55 | kCIContextWorkingColorSpace = 'working_color_space' 56 | kCIContextWorkingFormat = 'working_format' 57 | kCIContextUseSoftwareRenderer = 'software_renderer' 58 | kCIContextQuality = 'quality' 59 | kCIContextHighQualityDownsample = 'high_quality_downsample' 60 | kCIContextOutputPremultiplied = 'output_premultiplied' 61 | kCIContextCacheIntermediates = 'kCIContextCacheIntermediates' 62 | kCIActiveKeys = 'activeKeys' 63 | 64 | 65 | def show(img, title=None, color='gray', at=None, interpolation='bilinear', forced=False, sub=False): 66 | """ matplotlib.imshow helper """ 67 | import matplotlib 68 | import matplotlib.pylab as plt 69 | matplotlib.pylab.rcParams['figure.figsize'] = (18.0, 18 / 1.6) 70 | 71 | if isinstance(img, (tuple, list)): 72 | n = len(img) 73 | for i in range(n): 74 | t = title[i] if isinstance(title, (tuple, list)) else title 75 | show(img[i], title=t, at='1{}{}'.format(n, i + 1), color=color, interpolation=interpolation, forced=forced, 76 | sub=True) 77 | if 'inline' not in matplotlib.get_backend(): 78 | plt.show() 79 | return 80 | 81 | if isinstance(img, cimg): 82 | img = img.render() 83 | 84 | if at: 85 | if isinstance(at, (int)): 86 | plt.subplot(at) 87 | elif isinstance(at, (tuple, list)) and len(at) == 3: 88 | plt.subplot(*at) 89 | else: 90 | plt.subplot(at) 91 | 92 | if img is not None: 93 | plt.imshow(img.clip(0, 1), interpolation=interpolation, cmap=plt.get_cmap(color)) 94 | 95 | if title: 96 | plt.title(title, color='w') 97 | 98 | plt.gca().axes.get_xaxis().set_visible(False) 99 | plt.gca().axes.get_yaxis().set_visible(False) 100 | 101 | last = False 102 | if isinstance(at, (int)): 103 | last = at and int(str(at)[2]) == int(str(at)[0]) * int(str(at)[1]) 104 | elif isinstance(at, (list, tuple)) and len(at) == 3: 105 | last = at[2] == at[0] * at[1] 106 | 107 | if (forced or 'inline' not in matplotlib.get_backend()) and not sub and not at or last: 108 | import matplotlib.pylab 109 | plt.show() 110 | 111 | 112 | """ Defaults """ 113 | DEFAULT_FORMAT_RENDER = np.float32 # render from CIImage to numpy 114 | DEFAULT_FORMAT_WORKING = np.float32 # intermediates 115 | DEFAULT_FORMAT_FROMFILE = np.float32 116 | DEFAULT_COLORSPACE = kCGColorSpaceSRGB 117 | 118 | """ Conversion helpers """ 119 | _pixelFormatsRGBA = (np.float16, kCIFormatRGBAh), (np.float32, kCIFormatRGBAf), (np.uint8, kCIFormatRGBA8) 120 | dtype2formatRGBA = {d: f for (d, f) in _pixelFormatsRGBA} 121 | format2dtypeRGBA = {f: d for (d, f) in _pixelFormatsRGBA} 122 | SUPPORTED_DTYPES = [dtype for (dtype, format) in _pixelFormatsRGBA] 123 | 124 | """ Module Singleton Accessors """ 125 | 126 | 127 | def setContext(context): 128 | global singletonContext 129 | singletonContext = context 130 | 131 | 132 | def setColorspace(colorspace): 133 | global singletonColorspace 134 | singletonColorspace = colorspace 135 | 136 | 137 | def createContext(useDefaults=False, colorspaceWorking=None, colorspaceOutput=None, workingFormat=None, linear=False): 138 | """ 139 | Args: 140 | colorspaceWorking (CGColorSpace): Working colorspace. 141 | colorspaceOutput (CGColorSpace): Output colorspace. 142 | workingFormat (numpy.dtype): Working format. 143 | 144 | Note: 145 | kCIContextOutputColorSpace = 'output_color_space' 146 | kCIContextWorkingColorSpace = 'working_color_space' 147 | kCIContextWorkingFormat = 'working_format' 148 | kCIContextUseSoftwareRenderer = 'software_renderer' 149 | kCIContextQuality = 'quality' 150 | kCIContextHighQualityDownsample = 'high_quality_downsample' 151 | kCIContextOutputPremultiplied = 'output_premultiplied' 152 | kCIContextCacheIntermediates = 'kCIContextCacheIntermediates'xw 153 | 154 | """ 155 | 156 | options = {} 157 | options[kCIContextOutputColorSpace] = colorspaceOutput 158 | options[kCIContextWorkingColorSpace] = colorspaceWorking 159 | options[kCIContextWorkingFormat] = dtype2formatRGBA[ 160 | workingFormat] if workingFormat in SUPPORTED_DTYPES else workingFormat 161 | 162 | # Prune empty keys 163 | empty = [option for option in options if option is None] 164 | for option in empty: 165 | options.pop(option) 166 | 167 | # Default override 168 | if useDefaults: 169 | options = { 170 | kCIContextWorkingFormat: dtype2formatRGBA[DEFAULT_FORMAT_WORKING], 171 | kCIContextWorkingColorSpace: singletonColorspace 172 | } 173 | 174 | if linear: 175 | options[kCIContextWorkingColorSpace] = CGColorSpaceCreateWithName(kCGColorSpaceLinearSRGB) 176 | options[kCIContextOutputColorSpace] = CGColorSpaceCreateWithName(kCGColorSpaceLinearSRGB) 177 | 178 | context = CIContext.contextWithOptions_(options) 179 | 180 | if not context: 181 | raise RuntimeError('unable to create context') 182 | 183 | setContext(context) 184 | 185 | 186 | def createColorspace(name=DEFAULT_COLORSPACE): 187 | colorspace = CGColorSpaceCreateWithName(name) 188 | 189 | if not colorspace: 190 | raise RuntimeError('unable to create colorspace') 191 | 192 | setColorspace(colorspace) 193 | 194 | 195 | # Create Module Singletons 196 | createColorspace() 197 | createContext(useDefaults=True) 198 | 199 | 200 | # Utilities 201 | def set_print_tree(level, more=None): 202 | if level < 0 or level > 8: 203 | warn('CI_PRINT_TREE should be in the range (0,8)') 204 | return 205 | 206 | os.environ['CI_PRINT_TREE'] = str(level) 207 | 208 | 209 | def get_print_tree(): 210 | os.environ['CI_PRINT_TREE'] 211 | 212 | 213 | def vector(*args): 214 | # Support either Vector(1, 1, 1, 1) or Vector([1, 1, 1, 1]) 215 | lst = args if len(args) > 1 else args[0] 216 | 217 | if not isinstance(lst, (list, tuple)): 218 | raise RuntimeError('Can only create a CIVector from tuple types.') 219 | 220 | length = len(lst) 221 | if length > 4: 222 | raise RuntimeError('Can only create a CIVector from tuples with less than or equal to 4 components.') 223 | elif length == 1: 224 | return CIVector.vectorWithX_(*lst) 225 | elif length == 2: 226 | return CIVector.vectorWithX_Y_(*lst) 227 | elif length == 3: 228 | return CIVector.vectorWithX_Y_Z_(*lst) 229 | elif length == 4: 230 | return CIVector.vectorWithX_Y_Z_W_(*lst) 231 | 232 | 233 | def color(*args): 234 | # Support either Color(1, 1, 1, X) or Color([1, 1, 1, X]) 235 | lst = args if len(args) > 1 else args[0] 236 | 237 | if not isinstance(lst, (list, tuple)): 238 | raise RuntimeError('Can only create a CIColor from tuple types.') 239 | 240 | length = len(lst) 241 | 242 | if length == 3: 243 | return CIColor.colorWithRed_green_blue_(*lst) 244 | elif length == 4: 245 | return CIColor.colorWithRed_green_blue_alpha_(*lst) 246 | else: 247 | raise RuntimeError('Can only create a CIColor from tuples with 3 or 4 components but received {}.'.format(args)) 248 | 249 | 250 | def inset(rect, dx, dy, integral=True): 251 | if isinstance(rect, (list, tuple)): 252 | rect = CGRectMake(*rect) 253 | 254 | rect = CGRectInset(rect, dx, dy) 255 | 256 | if integral: 257 | rect = CGRectIntegral(rect) 258 | 259 | return rect.origin.x, rect.origin.y, rect.size.width, rect.size.height 260 | 261 | 262 | def rectify(rect): 263 | if isinstance(rect, (list, tuple)): 264 | rect = CGRectMake(*rect) 265 | return rect 266 | 267 | 268 | class cimg: 269 | DEFAULT_RENDER_DTYPE = np.float32 270 | _kernelCounter = 0 271 | 272 | @staticmethod 273 | def filters(): 274 | return CIFilter.filterNamesInCategory_(None) 275 | 276 | @staticmethod 277 | def inputs(filterName): 278 | if not filterName.startswith('CI'): 279 | filterName = 'CI' + filterName[0].upper() + filterName[1:] 280 | return CIFilter.filterWithName_(filterName).attributes() 281 | 282 | def __init__(self, data, dtype=None): 283 | 284 | self.dtype = dtype if dtype else DEFAULT_FORMAT_RENDER 285 | 286 | if isinstance(data, cimg): 287 | # copy constructor 288 | self._cake = data._cake 289 | self._dirty = data._dirty 290 | self._ciimage = data._ciimage 291 | 292 | elif isinstance(data, ndarray): 293 | # numpy array 294 | # FIXME: might need to enforce byte ordering 295 | # data = data.astype(data.dtype, order='C', casting='unsafe', subok=True, copy=True) 296 | 297 | if data.ndim == 2: 298 | data = data[..., np.newaxis] 299 | 300 | if data.ndim != 3 or (data.ndim == 3 and data.shape[2] == 2): 301 | raise RuntimeError( 302 | 'Only single channel, RGB and RGBA images supported but received {}'.format(data.shape)) 303 | 304 | if dtype: 305 | data = data.astype(dtype) 306 | 307 | self._cake = data 308 | self._dirty = False 309 | self._ciimage = cimg.create_ciimage_from_numpy(data) 310 | 311 | elif isinstance(data, CIImage): 312 | # native CIImage 313 | self._cake = None 314 | self._dirty = True 315 | self._ciimage = data 316 | 317 | else: 318 | raise NotImplementedError('Constructor not supported: {}'.format(type(data))) 319 | 320 | # Add filter lambdas 321 | if not getattr(self, '_lambdified', False): 322 | self._add_filter_lambdas() 323 | self._add_imageby_lambdas() 324 | setattr(self, '_lambdified', True) 325 | 326 | def __str__(self): 327 | return '<{}.{} at {}> extent={} dtype={}'.format(self.__class__.__module__, 328 | self.__class__.__name__, 329 | hex(id(self)), 330 | self.extent, 331 | self.dtype) 332 | 333 | def __getitem__(self, index): 334 | """ numpy style slicing. """ 335 | # Only fully index, non-stepping slicing supported with cropping 336 | pre_crop = (isinstance(index, tuple) and len(index) == 3 337 | and (index[0] == Ellipsis or index[0].step is None) 338 | and (index[1] == Ellipsis or index[1].step is None) 339 | and (index[2] == Ellipsis or index[2].step is None)) 340 | 341 | if pre_crop: 342 | rows, cols, dims = index 343 | w, h = self.size 344 | 345 | x0 = cols.start if cols.start else 0 346 | x1 = cols.stop if cols.stop else w 347 | 348 | y0 = h - rows.stop if rows.stop else 0 349 | y1 = h - rows.start if rows.start else h 350 | 351 | rect = x0, y0, x1 - x0, y1 - y0 352 | crop = self.crop(*rect) 353 | ary = crop.render() 354 | return ary[..., index[2]] if index[2] is not Ellipsis else ary[...] 355 | 356 | else: 357 | return self.render()[index] 358 | 359 | def __setitem__(self, idx, value): 360 | # TODO: create a recipe instead of baking the array 361 | ary = self.render() 362 | ary[idx] = value 363 | self._cake = ary 364 | self._dirty = False 365 | self._ciimage = cimg.create_ciimage_from_numpy(ary) 366 | 367 | def _add_filter_lambdas(self): 368 | 369 | # Generate lambdas for all filters here, so that we can call them directly on the array. 370 | for filterName in cimg.filters(): 371 | # Skip CICrop, will handle it as an attribute 372 | if filterName == 'CICrop': 373 | continue 374 | 375 | # 1) CIFilterName() 376 | if not hasattr(cimg, filterName): 377 | setattr(cimg, filterName, lambda self, foo=filterName, **kwargs: self.applyFilter(foo, **kwargs)) 378 | 379 | # 2) FilterName() 380 | filterName = filterName[2:] 381 | setattr(cimg, filterName, lambda self, foo=filterName, **kwargs: self.applyFilter(foo, **kwargs)) 382 | 383 | # 3) filterName() 384 | filterName = filterName[0].lower() + filterName[1:] 385 | setattr(cimg, filterName, lambda self, foo=filterName, **kwargs: self.applyFilter(foo, **kwargs)) 386 | 387 | def _add_imageby_lambdas(self): 388 | # Native CoreImage 389 | imageByMethods = ['applyTransform', 'clamp'] 390 | 391 | # Utility 392 | imageByMethods += ['transform', 'scale', 'translate', 'rotate'] 393 | 394 | def apply(imageByMethod): 395 | return lambda self, *args: self.applyImageBy(imageByMethod, *args) 396 | 397 | for imageByMethod in imageByMethods: 398 | setattr(cimg, imageByMethod, apply(imageByMethod)) 399 | 400 | @property 401 | def origin(self): 402 | return self.extent[0], self.extent[1] 403 | 404 | @property 405 | def extent(self): 406 | extent = self._ciimage.extent() 407 | return extent.origin.x, extent.origin.y, extent.size.width, extent.size.height 408 | 409 | @property 410 | def size(self): 411 | return self.extent[2], self.extent[3] 412 | 413 | @property 414 | def ciimage(self): 415 | return self._ciimage 416 | 417 | def render(self, dtype=None, alpha=None): 418 | """ 419 | Render the underlying CIImage to a numpy array. 420 | """ 421 | if dtype is None: 422 | dtype = self.dtype 423 | 424 | # Strict dtype checks since not all are supported by CoreImage 425 | if dtype not in SUPPORTED_DTYPES: 426 | raise NotImplementedError( 427 | 'Incompatible image type: {}. Must be one of {}.'.format(dtype, SUPPORTED_DTYPES)) 428 | 429 | if FLAG_DEBUG: 430 | print('\nbaking for:', inspect.stack()[1][3]) 431 | 432 | if self._dirty: 433 | if FLAG_DEBUG: 434 | print('Baking to', dtype) 435 | 436 | # Render 437 | self._cake = cimg.realize_numpy_from_ciimage(self._ciimage, dtype=dtype) 438 | self._dirty = False 439 | 440 | # FIXME: render directly to RGBx 441 | if not alpha: 442 | self._cake = self._cake[..., :3] 443 | 444 | else: 445 | if FLAG_DEBUG: 446 | print('Not dirty, returning existing cake ({})'.format(self._cake is not None)) 447 | 448 | return self._cake.copy() 449 | 450 | def save(self, filename): 451 | """ Save to disk. """ 452 | ext = os.path.splitext(filename)[1].lower() 453 | supported = ['.jpg', '.jpeg', '.png', '.tiff', '.heif'] 454 | 455 | if not any([ext == e for e in supported]): 456 | warn('{} only supported for now but received {}'.format(supported, ext)) 457 | 458 | outURL = NSURL.fileURLWithPath_(filename) 459 | options = {} 460 | 461 | if ext in ['.jpg', '.jpeg']: 462 | singletonContext.writeJPEGRepresentationOfImage_toURL_colorSpace_options_error_(self._ciimage, 463 | outURL, 464 | singletonColorspace, 465 | options, 466 | None) 467 | 468 | elif ext in ['.png']: 469 | singletonContext.writePNGRepresentationOfImage_toURL_format_colorSpace_options_error_(self._ciimage, 470 | outURL, 471 | kCIFormatRGBA8, 472 | singletonColorspace, 473 | options, 474 | None) 475 | 476 | elif ext in ['.tiff']: 477 | singletonContext.writeTIFFRepresentationOfImage_toURL_format_colorSpace_options_error_(self._ciimage, 478 | outURL, 479 | kCIFormatRGBA8, 480 | singletonColorspace, 481 | options, 482 | None) 483 | 484 | def average(self): 485 | return self.areaAverage(extent=self.extent, clamped=False, cropped=False) 486 | 487 | def over(self, image): 488 | """ Composite on top of another image. """ 489 | assert isinstance(image, cimg) 490 | return cimg(self._ciimage.imageByCompositingOverImage_(image._ciimage)) 491 | 492 | def multiply(self, image): 493 | src = """ 494 | kernel vec4 _pyci_multiply(__sample a, __sample b) { 495 | return a*b; 496 | } 497 | """ 498 | return self.applyKernel(src, image) 499 | 500 | def crop(self, *args): 501 | """ 502 | :param args: (x,y,w,h) or (w,h) 503 | :return: a crop of this image 504 | """ 505 | 506 | # Attempt unpacking from CGRect, NSPoint or tuple 507 | if len(args) == 1: 508 | c = args[0] 509 | 510 | if isinstance(c, CGRect): 511 | args = c.origin.x, c.origin.y, c.size.width, c.size.height 512 | 513 | elif isinstance(c, CGSize): 514 | args = 0, 0, c.width, c.height 515 | 516 | elif isinstance(c, (tuple, list)): 517 | args = c 518 | 519 | elif isinstance(c, (float, int)): 520 | args = c, c 521 | 522 | # Parse crop region 523 | if len(args) == 2: 524 | x, y, w, h = 0, 0, args[0], args[1] 525 | elif len(args) == 4: 526 | x, y, w, h = args 527 | else: 528 | raise RuntimeError('Expecting 2 or 4 tuple or args (origin and size) but received \'{}\''.format(args)) 529 | 530 | rect = CGRectMake(x, y, w, h) 531 | return cimg(self.ciimage.imageByCroppingToRect_(rect)) 532 | 533 | return None 534 | 535 | def resize(self, size, preserveAspect=0): 536 | """ 537 | :param size: scalar or 2-tuple. 538 | :param preserveAspect: whether to preserve the aspect ratio (scaling down) if `size` is a scalar. 539 | A value of 0 means no aspect scaling, 1 means fix X axis, 2 fix Y axis. 540 | :return: a `cimg` 541 | """ 542 | 543 | if not isinstance(size, (tuple, list)): 544 | sx, sy = size, size 545 | 546 | elif isinstance(size, (tuple, list)) and len(size) == 2: 547 | sx, sy = [float(s) for s in size] 548 | 549 | else: 550 | raise RuntimeError('Specify a dimension to use for both axis, or specify them individually') 551 | 552 | sx, sy = float(sx), float(sy) 553 | 554 | if preserveAspect == 1: 555 | sy = sx * self.size[1] / self.size[0] 556 | 557 | elif preserveAspect == 2: 558 | sx = sy * self.size[0] / self.size[1] 559 | 560 | return self.scale(sx / self.size[0], sy / self.size[1]) 561 | 562 | def applyFilter(self, filterName, clamped=True, cropped=True, **filterInputKeyValues): 563 | 564 | # Clamp image to infinity (repeated boundary conditions) 565 | inputImage = self.ciimage 566 | if clamped: 567 | inputImage = inputImage.imageByClampingToExtent() 568 | 569 | # Massage input keys and values 570 | filterInputKeyValues = self._validateInputs(filterInputKeyValues) 571 | 572 | # Force set input image with backed CIImage 573 | filterInputKeyValues['inputImage'] = inputImage 574 | 575 | # Create the filter with specified input parameters 576 | filter = self._create_filter(filterName, filterInputKeyValues) 577 | 578 | result = filter.outputImage() 579 | 580 | if result is None: 581 | raise RuntimeError( 582 | 'Internal CoreImage returned nil on outputImage. Make sure all input parameters are set.') 583 | 584 | # Crop image back to input size 585 | if cropped: 586 | result = result.imageByCroppingToRect_(self.ciimage.extent()) 587 | 588 | return cimg(result) 589 | 590 | def applyKernel(self, kernelSource, *args, **kwargs): 591 | 592 | extent = kwargs['extent'] if 'extent' in kwargs else self.extent 593 | roi = kwargs['roi'] if 'roi' in kwargs else None 594 | args = [self._ciimage] + list(args) 595 | return cimg.fromKernel(kernelSource, extent, roi=roi, args=args) 596 | 597 | def applyImageBy(self, imageByMethod, *imageByArgs): 598 | 599 | type_number = (int, float) # , long) # python2 only 600 | # Native CoreImage 601 | imageBy = { 602 | 'applyTransform': ('imageByApplyingTransform_', [CGAffineTransform]), 603 | 'clamp': ('imageByClampingToExtent', []), 604 | } 605 | 606 | # Utility 607 | imageByUtils = { 608 | 'transform': ('imageByApplyingTransform_', [type_number] * 6, 609 | lambda a, b, c, d, tx, ty: [CGAffineTransformMake(a, b, c, d, tx, ty)],), 610 | 'scale': ('imageByApplyingTransform_', [type_number] * 2, 611 | lambda sx, sy: [CGAffineTransformMakeScale(sx, sy)]), 612 | 'translate': ('imageByApplyingTransform_', [type_number] * 2, 613 | lambda tx, ty: [CGAffineTransformMakeTranslation(tx, ty)]), 614 | 'rotate': ('imageByApplyingTransform_', [type_number], 615 | lambda angle: [CGAffineTransformMakeRotation(angle)]), 616 | } 617 | imageBy.update(imageByUtils) 618 | 619 | # Validate caller 620 | if imageByMethod not in imageBy: 621 | raise NotImplementedError('{}'.format(imageByMethod)) 622 | 623 | # Validate args 624 | imageByArgsExp = imageBy[imageByMethod][1] 625 | if len(imageByArgs) != len(imageByArgsExp): 626 | raise RuntimeError( 627 | 'Argument count mismatch for {}: expected {} but received {}'.format(imageByMethod, len(imageByArgsExp), 628 | len(imageByArgs))) 629 | 630 | for i, (arg, expected) in enumerate(zip(imageByArgs, imageByArgsExp)): 631 | if not isinstance(arg, expected): 632 | raise RuntimeError( 633 | 'Invalid argument {}: expected type {} but received {}'.format(i, expected, type(arg))) 634 | 635 | # Repackage arguments for utility methods 636 | if imageByMethod in imageByUtils: 637 | imageByArgs = imageByUtils[imageByMethod][2](*imageByArgs) 638 | 639 | # Apply 640 | # FIXME: clamping to extent should be an option 641 | 642 | result = getattr(self._ciimage, imageBy[imageByMethod][0])(*imageByArgs) 643 | 644 | return cimg(result) 645 | 646 | @staticmethod 647 | def fromFile(filename, useDepth=False, useMatte=False, useDisparity=False, 648 | options=None): 649 | if not os.path.exists(filename) or not os.path.isfile(filename): 650 | raise IOError('File does not exist: ' + str(filename)) 651 | 652 | url = NSURL.fileURLWithPath_(filename) 653 | 654 | options_defaults = { 655 | 'kCIImageApplyOrientationProperty': True, 656 | 'kCIImageCacheHint': False, 657 | 'kCIImageCacheImmediately': False, 658 | 'kCIImageAVDepthData': None, 659 | } 660 | options = options if options else options_defaults 661 | 662 | if useDepth: 663 | options['kCIImageAuxiliaryDepth'] = useDepth 664 | options['kCIImageApplyOrientationProperty'] = options[ 665 | 'kCIImageApplyOrientationProperty'] if 'kCIImageApplyOrientationProperty' in options else True 666 | 667 | elif useDisparity: 668 | options['kCIImageAuxiliaryDisparity'] = useDisparity 669 | options['kCIImageApplyOrientationProperty'] = options[ 670 | 'kCIImageApplyOrientationProperty'] if 'kCIImageApplyOrientationProperty' in options else True 671 | 672 | elif useMatte: 673 | options['kCIImageAuxiliaryPortraitEffectsMatte'] = useMatte 674 | options['kCIImageApplyOrientationProperty'] = options[ 675 | 'kCIImageApplyOrientationProperty'] if 'kCIImageApplyOrientationProperty' in options else True 676 | 677 | image = CIImage.imageWithContentsOfURL_options_(url, options) 678 | 679 | if image is None: 680 | raise RuntimeError( 681 | 'imageWithContentsOfURL return nil for url:\n{}\nand selected options:\n{}\nMake sure image contains depth and/or ' 682 | 'matte data when using useDepth=True and useMatte=True'.format(url, options)) 683 | 684 | return cimg(image) 685 | 686 | @staticmethod 687 | def fromCIImage(ciimage, context=None, colorspace=None, dtype=DEFAULT_FORMAT_RENDER): 688 | ary = cimg.realize_numpy_from_ciimage(ciimage, context=context, colorspace=colorspace, dtype=dtype) 689 | return cimg(ary, recipe=ciimage) 690 | 691 | @staticmethod 692 | def fromColor(r, g, b, a=None): 693 | c = (r, g, b, a) if a else (r, g, b) 694 | return cimg(CIImage.imageWithColor_(color(c))) 695 | 696 | @staticmethod 697 | def fromPixel(r, g, b, a=None): 698 | cimg._kernelCounter += 1 699 | 700 | a = a if a else 1.0 701 | 702 | src = 'kernel vec4 _pyci_frompixel_{}()'.format(cimg._kernelCounter) + '{' 703 | src += 'return vec4({}, {}, {}, {});'.format(r, g, b, a) 704 | src += '}' 705 | return cimg.fromKernel(src, extent=(1, 1)) 706 | 707 | @staticmethod 708 | def fromGenerator(generatorName, crop=None, **filterInputKeyValues): 709 | 710 | # Massage input keys and values 711 | filterInputKeyValues = cimg._validateInputs(filterInputKeyValues) 712 | 713 | # Create the filter with specified input parameters 714 | filter = cimg._create_filter(generatorName, filterInputKeyValues) 715 | 716 | result = filter.outputImage() 717 | 718 | if result is None: 719 | raise RuntimeError('outputImage returned nil') 720 | 721 | result = cimg(result) 722 | 723 | if crop: 724 | result = result.crop(crop) 725 | 726 | return result 727 | 728 | @staticmethod 729 | def fromKernel(kernelSource, extent, args=None, roi=None): 730 | 731 | # Prepare kernel inputs 732 | roi = roi if roi else lambda i, r: r 733 | args = args if args else [] 734 | 735 | # Make sure roi returns a CGRect proper 736 | roi_rect = lambda index, r: rectify(roi(index, r)) 737 | 738 | # Massage extent 739 | if isinstance(extent, (tuple, list)): 740 | if len(extent) == 2: 741 | extent = (0, 0, extent[0], extent[1]) 742 | elif len(extent) == 4: 743 | pass 744 | else: 745 | raise RuntimeError('Expecting 2- or 4-tuple for extent') 746 | 747 | rect = CGRectMake(*extent) 748 | 749 | if FLAG_DEBUG: 750 | print(kernelSource) 751 | 752 | kernel = CIKernel.kernelWithString_(kernelSource) 753 | 754 | # Parse inputs 755 | # TODO: add more polymorphism here, e.g., with collections 756 | inputs = [cimg._validateType(a) for a in args] 757 | 758 | if FLAG_DEBUG: 759 | for inp in inputs: 760 | print(type(inp), inp) 761 | 762 | if isinstance(kernel, CIColorKernel): 763 | if FLAG_DEBUG: 764 | print('Color kernel') 765 | 766 | result = kernel.applyWithExtent_arguments_(rect, inputs) 767 | else: 768 | if FLAG_DEBUG: 769 | print('Regular kernel') 770 | 771 | result = kernel.applyWithExtent_roiCallback_arguments_(rect, roi_rect, inputs) 772 | 773 | return cimg(result) 774 | 775 | @staticmethod 776 | def arrayWithCGImage(cg): 777 | """ Convert to numpy buffer. 778 | 779 | Args: 780 | cg (CGImageRef): Input CG image. 781 | 782 | Returns: 783 | CIArray. 784 | 785 | """ 786 | 787 | # 788 | # FIXME: this operation needs to be made as fast as possible 789 | # FIXME: use renderToBitmap 790 | # https://developer.apple.com/library/content/qa/qa1509/_index.html 791 | 792 | # Gather CG info 793 | width = CoreGraphics.CGImageGetWidth(cg) 794 | height = CoreGraphics.CGImageGetHeight(cg) 795 | bytesPerRow = CoreGraphics.CGImageGetBytesPerRow(cg) 796 | bpc = CoreGraphics.CGImageGetBitsPerComponent(cg) 797 | bpp = CoreGraphics.CGImageGetBitsPerPixel(cg) 798 | bytesPerPixel = bpp // 8 799 | 800 | # Get actual pixels 801 | provider = CoreGraphics.CGImageGetDataProvider(cg) 802 | pixeldata = CoreGraphics.CGDataProviderCopyData(provider) 803 | 804 | # FIXME: this does not seem necessary 805 | # pixeldata = CFDataGetBytes(pixeldata, (0, CFDataGetLength(pixeldata)), None) 806 | 807 | numComponents = bpp // bpc 808 | widthExact = bytesPerRow // bytesPerPixel 809 | # numBytes = len(pixeldata) 810 | # heightExact = numBytes / (widthExact * bytesPerPixel) 811 | 812 | # FIXME: currently assume either 8 (RGBA8) or 16 (RGBAh) bit per component 813 | dtype = None 814 | if bpc == 8: 815 | dtype = np.uint8 816 | 817 | elif bpc == 16: 818 | dtype = np.float16 819 | 820 | elif bpc == 32: 821 | dtype = np.float32 822 | 823 | assert dtype, 'Unsupported bits per component: %d' % bpc 824 | 825 | if FLAG_DEBUG: 826 | print('Inferred returned data type', dtype) 827 | print('Bytes per row', bytesPerRow) 828 | print('Number of bytes = ', len(pixeldata)) 829 | print('Bits per components / pixels', bpc, bpp) 830 | print('Expected dimensions: ', width, height) 831 | print('Calculated dimensions:', widthExact, height) 832 | print('Number of components:', numComponents) 833 | 834 | ary = np.frombuffer(pixeldata, dtype=dtype)[:height * widthExact * numComponents] 835 | ary = ary.reshape((height, widthExact, numComponents)) 836 | 837 | # Crop any extra padding 838 | ary = ary[:height, :width, :] 839 | 840 | return ary 841 | 842 | @staticmethod 843 | def realize_numpy_from_ciimage(ciimage, context=None, colorspace=None, dtype=DEFAULT_FORMAT_RENDER): 844 | """ Construct a numpy array from a CIImage 845 | 846 | Args: 847 | context: 848 | ciimage: 849 | 850 | Returns: 851 | 852 | """ 853 | context = context if context else singletonContext 854 | colorspace = colorspace if colorspace else context.workingColorSpace() 855 | format = dtype2formatRGBA[dtype] if dtype in dtype2formatRGBA else None 856 | 857 | if FLAG_DEBUG: 858 | print('Rendering CIImage to dtype {} ({}) using colorspace {}'.format(dtype, format, colorspace)) 859 | print('Rendering using context', context) 860 | 861 | # FIXME: Temporary workaround since createCGImage_fromRect_format_colorSpace_ does not guarantee the output 862 | # to be the one specified in its format arguments. 863 | # ciimage = ciimage.imageByApplyingFilter_('CIPassThroughGeneralFilter') 864 | 865 | ### Method 1: first to CG, then to NumPy 866 | # Render to numpy 867 | # cg = context.createCGImage_fromRect_format_colorSpace_(ciimage, ciimage.extent(), format, colorspace) 868 | # ary = cimg.arrayWithCGImage(cg) 869 | 870 | ### Method 2: (preferred) directly to NumPy 871 | w, h = int(ciimage.extent().size.width), int(ciimage.extent().size.height) 872 | buffer_format = np.float32 873 | outputFormat = kCIFormatRGBAf 874 | rb = w * 4 * np.dtype(buffer_format).itemsize 875 | bitmap = NSMutableData.dataWithLength_(h * rb) 876 | context.render_toBitmap_rowBytes_bounds_format_colorSpace_(ciimage, bitmap, rb, ciimage.extent(), 877 | outputFormat, colorspace) 878 | ary = np.frombuffer(bitmap, dtype=buffer_format) 879 | ary = ary.reshape((h, w, 4)) 880 | 881 | return ary 882 | 883 | @staticmethod 884 | def create_cgimage_from_numpy(ary): 885 | # Parse image size, number of components 886 | h, w = ary.shape[:2] 887 | numChan = ary.shape[2] if ary.ndim == 3 else 1 888 | 889 | # Extract channels: R, G, B, (A) 890 | chs = [ary[..., i] for i in range(numChan)] 891 | 892 | # Prepare data info 893 | bytesPerComponent = ary.dtype.itemsize 894 | bitsPerComponent = bytesPerComponent * 8 895 | bytesPerRow = numChan * bytesPerComponent * w 896 | bitsPerPixel = numChan * bitsPerComponent 897 | numBytes = h * bytesPerRow 898 | 899 | if FLAG_DEBUG: 900 | print('bytesPerComponent', bytesPerComponent) 901 | print('bytesPerRow', bytesPerRow) 902 | print('bitsPerPixel', bitsPerPixel) 903 | print('numBytes', numBytes) 904 | print('numChan', numChan) 905 | 906 | # Interleave channels 907 | # FIXME: Costly. Is this always necessary? 908 | if numChan == 3 or numChan == 4: 909 | interleaved = np.dstack(chs).reshape(h, -1) 910 | colorspace = singletonColorspace 911 | else: 912 | interleaved = np.ascontiguousarray(ary) 913 | colorspace = CGColorSpaceCreateDeviceGray() 914 | 915 | # Extract buffer and create CGDataProvider 916 | # buf = np.getbuffer(interleaved) 917 | # buf = interleaved.tobytes() 918 | buf = interleaved.data 919 | # buf = (np.ones(interleaved.size) * 256).astype(interleaved.dtype) 920 | 921 | nsdata = NSData.dataWithBytes_length_(buf, numBytes) 922 | provider = CGDataProviderCreateWithCFData(nsdata) 923 | 924 | # Specify bitmap flags 925 | bitmapInfo = kCGBitmapByteOrderDefault 926 | 927 | if numChan == 3: 928 | bitmapInfo = bitmapInfo # | kCGImageAlphaNoneSkipLast 929 | if numChan == 4: 930 | bitmapInfo = bitmapInfo | 1 # | kCGImageAlphaPremultipliedLast 931 | 932 | # Higher bits per components needs to specify endian explicitly since Quartz defaults to Big endian when not specified 933 | if ary.dtype.type is np.uint8: 934 | pass 935 | elif ary.dtype.type is np.float16: 936 | bitmapInfo = bitmapInfo | kCGBitmapFloatComponents | kCGBitmapByteOrder16Little 937 | elif ary.dtype.type is np.float32: 938 | bitmapInfo = bitmapInfo | kCGBitmapFloatComponents | kCGBitmapByteOrder32Little 939 | 940 | # decode array for the image. 941 | # If you do not want to allow remapping of the image's color values, pass NULL for the decode array 942 | decode = None 943 | 944 | # A Boolean value that specifies whether interpolation should occur. 945 | # The interpolation setting specifies whether Core Graphics should apply a pixel-smoothing algorithm to the image. 946 | shouldInterpolate = False 947 | 948 | if FLAG_DEBUG: 949 | print(w, h, bitsPerComponent, bitsPerPixel, bytesPerRow) 950 | print(colorspace) 951 | print(bitmapInfo) 952 | print('provider', provider) 953 | print('decode', decode) 954 | print('shouldInterpolate', shouldInterpolate) 955 | print('renderingIntent', kCGRenderingIntentDefault) 956 | 957 | cg = CGImageCreate(w, h, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, provider, decode, 958 | shouldInterpolate, kCGRenderingIntentDefault) 959 | 960 | assert cg 961 | 962 | return cg 963 | 964 | @staticmethod 965 | def create_ciimage_from_numpy(ary): 966 | """ 967 | Returns: The CIImage representation of self. 968 | """ 969 | if FLAG_DEBUG: 970 | print('Creating recipe from data', ary.dtype, ary.shape, 'for', inspect.stack()[1][3]) 971 | 972 | # CoreImage only supports 32 bit 973 | if ary.dtype == np.float64: 974 | ary = ary.astype(np.float32) 975 | 976 | if ary.dtype not in SUPPORTED_DTYPES: 977 | raise NotImplementedError( 978 | 'Incompatible image type: {}. Must be one of {}.'.format(ary.dtype, SUPPORTED_DTYPES)) 979 | 980 | # Parse image size, number of components 981 | cg = cimg.create_cgimage_from_numpy(ary) 982 | assert cg, 'CGImageCreate returned nil' 983 | 984 | ci = CIImage.imageWithCGImage_(cg) 985 | assert ci, 'CIImage.imageWithCGImage returned nil' 986 | 987 | return ci 988 | 989 | @staticmethod 990 | def _create_filter(filterName, filterInputKeyValues): 991 | 992 | # Support calling via shorthand, e.g., CIAreaAverage -> areaAverage 993 | if not filterName.startswith('CI'): 994 | filterName = 'CI' + filterName[0].upper() + filterName[1:] 995 | 996 | # Create an instance of this filter 997 | filter = CIFilter.filterWithName_(filterName) 998 | 999 | if filter is None: 1000 | # Find closest match and raise 1001 | matches = get_close_matches(filterName, cimg.filters(), n=10, cutoff=0.5) 1002 | matches = ' | '.join(['{}. {}'.format(i + 1, m) for (i, m) in enumerate(matches)]) 1003 | msg = 'No filter found by the name: {}. Did you mean:\n\t{}'.format(filterName, matches) 1004 | raise RuntimeError(msg) 1005 | 1006 | # Parse known input parameters for this filter 1007 | filterAttributes = CIFilter.filterWithName_(filterName).attributes() 1008 | filterInputs = [k for k in filterAttributes if k.startswith('input')] 1009 | 1010 | # Set inputs 1011 | for k, v in filterInputKeyValues.items(): 1012 | 1013 | # Certain filters require special needs 1014 | if filterName == 'CIQRCodeGenerator' and k == 'inputMessage': 1015 | try: 1016 | v = NSData.dataWithBytes_length_(bytes(v, 'utf-8'), len(v)) 1017 | except TypeError: 1018 | v = NSData.dataWithBytes_length_(v, len(v)) 1019 | 1020 | try: 1021 | filter.setValue_forKey_(v, k) 1022 | 1023 | except (AttributeError, KeyError) as e: 1024 | print('{} while setting attribute \'{}\' on \'{}\''.format(e, k, filterName)) 1025 | 1026 | matches = get_close_matches(k, filterInputs, n=5, cutoff=0.7) 1027 | matches = matches if len(matches) > 0 else filterInputs 1028 | matches = ' | '.join(['{}. {}'.format(i + 1, m) for (i, m) in enumerate(matches)]) 1029 | print('\tDid you mean: {}'.format(matches)) 1030 | 1031 | return filter 1032 | 1033 | @staticmethod 1034 | def _validateType(o, identifier=None): 1035 | 1036 | # Parse collections as CIVector or CIColor 1037 | if isinstance(o, (list, tuple)): 1038 | if identifier and 'color' in identifier.lower(): 1039 | return color(o) 1040 | 1041 | else: 1042 | # Default to vector 1043 | return vector(o) 1044 | 1045 | elif isinstance(o, cimg): 1046 | # Use backing CIImage 1047 | return o.ciimage 1048 | 1049 | elif isinstance(o, ndarray): 1050 | if o.size <= 4: 1051 | return cimg._validateType(o.tolist(), identifier=identifier) 1052 | 1053 | # Reinterpret numpy array as CIImage and use backing image 1054 | return cimg(o).ciimage 1055 | 1056 | return o 1057 | 1058 | @staticmethod 1059 | def _validateInputs(filterInputKeyValues): 1060 | 1061 | # Reformat key names to inputKeyName 1062 | inputKeys = [key for key in filterInputKeyValues] 1063 | for key in inputKeys: 1064 | if not key.startswith('input'): 1065 | keyInput = 'input' + key[0].upper() + key[1:] 1066 | filterInputKeyValues[keyInput] = filterInputKeyValues.pop(key) 1067 | 1068 | for key in filterInputKeyValues: 1069 | val = filterInputKeyValues[key] 1070 | filterInputKeyValues[key] = cimg._validateType(val, key) 1071 | 1072 | return filterInputKeyValues 1073 | 1074 | 1075 | def demo_minimal(filepath): 1076 | """ Minimal example of image filtering using pyci. """ 1077 | 1078 | # Support for most common image file types, including raw. 1079 | img = cimg.fromFile(filepath) 1080 | print(type(img)) 1081 | print(img.size) 1082 | print(img.ciimage) 1083 | 1084 | # List built-in filters 1085 | for i, f in enumerate(cimg.filters()): print('{:3d} {}'.format(i, f)) 1086 | 1087 | # Print more info (including inputs) for a given filter 1088 | print(cimg.inputs['CIGaussianBlur']) 1089 | 1090 | # Resize the image 1091 | img = img.resize(1024, preserveAspect=1) 1092 | 1093 | # Rotate the image 1094 | img = img.rotate(30 / 180.0 * np.pi) 1095 | 1096 | # Apply a filter 1097 | # Note: can use the full filter name "CIGaussianBlur" 1098 | r = 50 1099 | blur = img.gaussianBlur(radius=r) 1100 | 1101 | # Save to disk 1102 | blur.save(filepath + '.CIGaussianBlur.jpg') 1103 | 1104 | show([img, blur], title=['input', 'Gaussian blur with radius {}'.format(r)]) 1105 | --------------------------------------------------------------------------------