├── 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 |
--------------------------------------------------------------------------------