├── .github └── workflows │ └── accimage.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── accimage.h ├── accimagemodule.c ├── chicago.jpg ├── imageops.c ├── jpegloader.c ├── setup.py └── test.py /.github/workflows/accimage.yml: -------------------------------------------------------------------------------- 1 | name: Accimage (Ubuntu latest) 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | os: ["ubuntu-latest"] 10 | python-version: [3.6, 3.7, 3.8, 3.9] 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Set up conda 15 | uses: conda-incubator/setup-miniconda@v2 16 | with: 17 | auto-update-conda: true 18 | python-version: ${{ matrix.python-version }} 19 | 20 | - name: Conda info 21 | shell: bash -l {0} 22 | run: conda info -a 23 | 24 | - name: Install deps 25 | shell: bash -l {0} 26 | run: | 27 | conda install \ 28 | -c pytorch \ 29 | -c conda-forge \ 30 | pip \ 31 | scipy \ 32 | numpy \ 33 | intel-ipp \ 34 | libjpeg-turbo 35 | pip install pytest imageio 36 | 37 | - name: Install Accimage 38 | shell: bash -l {0} 39 | run: | 40 | CPPFLAGS="-I$CONDA_PREFIX/include" LDFLAGS="-L$CONDA_PREFIX/lib" pip install . --no-deps -vv 41 | 42 | - name: Test Accimage 43 | shell: bash -l {0} 44 | run: | 45 | pytest test.py 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #### joe made this: http://goel.io/joe 2 | 3 | #####=== Python ===##### 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | 110 | #####=== JetBrains ===##### 111 | # User-specific stuff 112 | .idea/**/workspace.xml 113 | .idea/**/tasks.xml 114 | .idea/**/usage.statistics.xml 115 | .idea/**/dictionaries 116 | .idea/**/shelf 117 | 118 | # Sensitive or high-churn files 119 | .idea/**/dataSources/ 120 | .idea/**/dataSources.ids 121 | .idea/**/dataSources.local.xml 122 | .idea/**/sqlDataSources.xml 123 | .idea/**/dynamic.xml 124 | .idea/**/uiDesigner.xml 125 | .idea/**/dbnavigator.xml 126 | 127 | # Gradle 128 | .idea/**/gradle.xml 129 | .idea/**/libraries 130 | 131 | # CMake 132 | cmake-build-*/ 133 | 134 | # Mongo Explorer plugin 135 | .idea/**/mongoSettings.xml 136 | 137 | # File-based project format 138 | *.iws 139 | 140 | # IntelliJ 141 | out/ 142 | 143 | # mpeltonen/sbt-idea plugin 144 | .idea_modules/ 145 | 146 | # JIRA plugin 147 | atlassian-ide-plugin.xml 148 | 149 | # Cursive Clojure plugin 150 | .idea/replstate.xml 151 | 152 | # Crashlytics plugin (for Android Studio and IntelliJ) 153 | com_crashlytics_export_strings.xml 154 | crashlytics.properties 155 | crashlytics-build.properties 156 | fabric.properties 157 | 158 | # Editor-based Rest Client 159 | .idea/httpRequests 160 | 161 | # Rider-specific rules 162 | *.sln.iml 163 | 164 | .ropeproject 165 | test_*.jpg 166 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | sudo: true 4 | 5 | matrix: 6 | include: 7 | - python: "2.7" 8 | - python: "3.5" 9 | - python: "3.6" 10 | - python: "3.7" 11 | 12 | before_install: 13 | - sudo apt-get update 14 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh; 15 | - bash miniconda.sh -b -p $HOME/miniconda 16 | - export PATH="$HOME/miniconda/bin:$PATH" 17 | - hash -r 18 | - conda config --set always_yes yes --set changeps1 no 19 | - conda update -q conda 20 | # Useful for debugging any issues with conda 21 | - conda info -a 22 | - | 23 | conda create -q -n test-environment \ 24 | -c pytorch \ 25 | -c conda-forge \ 26 | python=$TRAVIS_PYTHON_VERSION \ 27 | pip \ 28 | scipy \ 29 | numpy \ 30 | intel-ipp \ 31 | libjpeg-turbo 32 | - source activate test-environment 33 | - pip install pytest imageio 34 | 35 | install: 36 | - env | sort 37 | - ls $CONDA_PREFIX/include 38 | - ls $CONDA_PREFIX/lib 39 | - CPPFLAGS="-I$CONDA_PREFIX/include" LDFLAGS="-L$CONDA_PREFIX/lib" pip install . --no-deps -vv 40 | 41 | script: 42 | - pytest test.py 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.2.0 2 | 3 | * Add support to reading images from a `bytes` buffer, e.g. 4 | ```python 5 | with open("chicago.jpg", "rb") as f: 6 | img = accimage.Image(f.read()) 7 | ``` 8 | (This new functionality is thanks to @brhcriteo!) 9 | 10 | # v0.1.1 11 | 12 | * Bug fix: Horizontal crops prior to v0.1.1 didn't work. 13 | * Add pytest tests.py 14 | * Add travis building support 15 | 16 | # v0.1.0 17 | 18 | Initial release 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to accimage 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Pull Requests 6 | We actively welcome your pull requests. 7 | 8 | 1. Fork the repo and create your branch from `master`. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. If you've changed APIs, update the documentation. 11 | 4. Ensure the test suite passes. 12 | 5. Make sure your code lints. 13 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 14 | 15 | ## Issues 16 | We use GitHub issues to track public bugs. Please ensure your description is 17 | clear and has sufficient instructions to be able to reproduce the issue. 18 | 19 | ## License 20 | By contributing to accimage, you agree that your contributions will be licensed 21 | under the LICENSE file in the root directory of this source tree. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2017, Facebook, Inc 4 | Copyright (c) 2016, Georgia Institute of Technology 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # accimage 2 | 3 | [![Build status](https://github.com/pytorch/accimage/actions/workflows/accimage.yml/badge.svg)](https://github.com/pytorch/accimage/actions/workflows/accimage.yml) 4 | [![Anaconda badge](https://anaconda.org/conda-forge/accimage/badges/version.svg)](https://anaconda.org/conda-forge/accimage) 5 | 6 | 7 | An accelerated Image loader and preprocessor leveraging [Intel 8 | IPP](https://software.intel.com/en-us/intel-ipp). 9 | 10 | accimage mimics the PIL API and can be used as a backend for 11 | [`torchvision`](https://github.com/pytorch/vision). 12 | 13 | Operations implemented: 14 | 15 | - `Image.resize((width, height))` 16 | - `Image.crop((left, upper, right, lower))` 17 | - `Image.transpose(PIL.Image.FLIP_LEFT_RIGHT)` 18 | 19 | Enable the torchvision accimage backend with: 20 | 21 | ```python 22 | torchvision.set_image_backend('accimage') 23 | ``` 24 | 25 | ## Installation 26 | 27 | accimage is available on conda-forge, simply run the following to install 28 | 29 | ``` 30 | $ conda install -c conda-forge accimage 31 | ``` 32 | -------------------------------------------------------------------------------- /accimage.h: -------------------------------------------------------------------------------- 1 | #ifndef ACCIMAGE_H 2 | #define ACCIMAGE_H 3 | 4 | #include 5 | 6 | typedef struct { 7 | PyObject_HEAD 8 | unsigned char* buffer; 9 | int channels; 10 | int height; 11 | int width; 12 | int row_stride; 13 | int y_offset; 14 | int x_offset; 15 | } ImageObject; 16 | 17 | void image_copy_deinterleave(ImageObject* self, unsigned char* output_buffer); 18 | void image_copy_deinterleave_float(ImageObject* self, float* output_buffer); 19 | void image_from_buffer(ImageObject* self, void* buf, size_t size); 20 | void image_from_jpeg(ImageObject* self, const char* path); 21 | void image_resize(ImageObject* self, int new_height, int new_width, int antialiasing); 22 | void image_flip_left_right(ImageObject* self); 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /accimagemodule.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "accimage.h" 5 | 6 | static struct PyMemberDef Image_members[] = { 7 | { "channels", T_INT, offsetof(ImageObject, channels), READONLY, "number of channels" }, 8 | { "height", T_INT, offsetof(ImageObject, height), READONLY, "image height in pixels" }, 9 | { "width", T_INT, offsetof(ImageObject, width), READONLY, "image width in pixels" }, 10 | { NULL } /* Sentinel */ 11 | }; 12 | 13 | static PyObject* Image_getsize(ImageObject* self, void* closure) { 14 | return Py_BuildValue("ii", self->width, self->height); 15 | } 16 | 17 | static PyGetSetDef Image_getseters[] = { 18 | { "size", (getter) Image_getsize, (setter) NULL, "Image width x height", NULL }, 19 | { NULL } /* Sentinel */ 20 | }; 21 | 22 | static PyObject* Image_resize(ImageObject* self, PyObject* args, PyObject* kwds) { 23 | static char* argnames[] = { "size", "interpolation", NULL }; 24 | PyObject* size = NULL; 25 | int interpolation = 0; 26 | int antialiasing = 1; 27 | int new_height, new_width; 28 | 29 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|i", argnames, &size, &interpolation)) { 30 | return NULL; 31 | } 32 | 33 | if (!PyArg_ParseTuple(size, "ii", &new_width, &new_height)) { 34 | return NULL; 35 | } 36 | 37 | if (new_height <= 0) { 38 | return PyErr_Format(PyExc_ValueError, "positive height expected; got %d", new_height); 39 | } 40 | 41 | if (new_width <= 0) { 42 | return PyErr_Format(PyExc_ValueError, "positive width expected; got %d", new_width); 43 | } 44 | 45 | // TODO: consider interpolation parameter 46 | image_resize(self, new_height, new_width, antialiasing); 47 | 48 | if (PyErr_Occurred()) { 49 | return NULL; 50 | } else { 51 | Py_INCREF(self); 52 | return (PyObject*) self; 53 | } 54 | } 55 | 56 | static PyObject* Image_crop(ImageObject* self, PyObject* args, PyObject* kwds) { 57 | static char* argnames[] = { "box", NULL }; 58 | PyObject* box_object; 59 | int left, upper, right, lower; 60 | 61 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", argnames, &box_object)) { 62 | return NULL; 63 | } 64 | 65 | if (!PyArg_ParseTuple(box_object, "iiii", &left, &upper, &right, &lower)) { 66 | return NULL; 67 | } 68 | 69 | if (left < 0) { 70 | return PyErr_Format(PyExc_ValueError, "non-negative left offset expected; got %d", left); 71 | } 72 | 73 | if (upper < 0) { 74 | return PyErr_Format(PyExc_ValueError, "non-negative upper offset expected; got %d", upper); 75 | } 76 | 77 | if (right > self->width) { 78 | return PyErr_Format(PyExc_ValueError, "right coordinate (%d) extends beyond image width (%d)", 79 | right, self->width); 80 | } 81 | 82 | if (lower > self->height) { 83 | return PyErr_Format(PyExc_ValueError, "lower coordinate (%d) extends beyond image height (%d)", 84 | lower, self->height); 85 | } 86 | 87 | if (right <= left) { 88 | return PyErr_Format(PyExc_ValueError, "right coordinate (%d) does not exceed left coordinate (%d)", 89 | right, left); 90 | } 91 | 92 | if (lower <= upper) { 93 | return PyErr_Format(PyExc_ValueError, "lower coordinate (%d) does not exceed upper coordinate (%d)", 94 | lower, upper); 95 | } 96 | 97 | self->y_offset += upper; 98 | self->x_offset += left; 99 | self->height = lower - upper; 100 | self->width = right - left; 101 | 102 | Py_INCREF(self); 103 | return (PyObject*) self; 104 | } 105 | 106 | static PyObject* Image_transpose(ImageObject* self, PyObject* args, PyObject* kwds) { 107 | static char* argnames[] = { "method", NULL }; 108 | int method; 109 | 110 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "i", argnames, &method)) 111 | return NULL; 112 | 113 | switch (method) { 114 | case 0: 115 | /* PIL.Image.FLIP_LEFT_RIGHT */ 116 | image_flip_left_right(self); 117 | break; 118 | case 1: 119 | PyErr_SetString(PyExc_ValueError, "unsupported method: PIL.Image.FLIP_TOP_BOTTOM"); 120 | return NULL; 121 | case 2: 122 | PyErr_SetString(PyExc_ValueError, "unsupported method: PIL.Image.ROTATE_90"); 123 | return NULL; 124 | case 3: 125 | PyErr_SetString(PyExc_ValueError, "unsupported method: PIL.Image.ROTATE_180"); 126 | return NULL; 127 | case 4: 128 | PyErr_SetString(PyExc_ValueError, "unsupported method: PIL.Image.ROTATE_270"); 129 | return NULL; 130 | case 5: 131 | PyErr_SetString(PyExc_ValueError, "unsupported method: PIL.Image.TRANSPOSE"); 132 | return NULL; 133 | default: 134 | return PyErr_Format(PyExc_ValueError, "unknown method (%d)", method); 135 | } 136 | 137 | if (PyErr_Occurred()) { 138 | return NULL; 139 | } else { 140 | Py_INCREF(self); 141 | return (PyObject*) self; 142 | } 143 | } 144 | 145 | static PyObject* Image_copyto(ImageObject* self, PyObject* args, PyObject* kwds) { 146 | static char* argnames[] = { "buffer", NULL }; 147 | static const int FLAGS = PyBUF_CONTIG | PyBUF_FORMAT; 148 | PyObject* buffer_object; 149 | Py_buffer buffer; 150 | 151 | if (!PyArg_ParseTupleAndKeywords(args, kwds, "O", argnames, &buffer_object)) { 152 | return NULL; 153 | } 154 | 155 | if (PyObject_GetBuffer(buffer_object, &buffer, FLAGS) < 0) { 156 | return NULL; 157 | } 158 | 159 | int expected_size = self->channels * self->height * self->width; 160 | if (strcmp(buffer.format, "f") == 0) { 161 | expected_size *= sizeof(float); 162 | } 163 | 164 | if (buffer.len < expected_size) { 165 | PyErr_Format(PyExc_IndexError, "buffer size (%lld) is smaller than image size (%d)", 166 | (long long) buffer.len, expected_size); 167 | goto cleanup; 168 | } 169 | 170 | if (buffer.format == NULL || strcmp(buffer.format, "B") == 0) { 171 | image_copy_deinterleave(self, (unsigned char*) buffer.buf); 172 | } else if (strcmp(buffer.format, "f") == 0) { 173 | image_copy_deinterleave_float(self, (float*) buffer.buf); 174 | } else { 175 | PyErr_SetString(PyExc_TypeError, "buffer of unsigned byte or float elements expected"); 176 | goto cleanup; 177 | } 178 | 179 | 180 | cleanup: 181 | PyBuffer_Release(&buffer); 182 | 183 | if (PyErr_Occurred()) { 184 | return NULL; 185 | } else { 186 | Py_RETURN_NONE; 187 | } 188 | } 189 | 190 | static PyMethodDef Image_methods[] = { 191 | { "resize", (PyCFunction) Image_resize, METH_VARARGS | METH_KEYWORDS, "Scale image to new size." }, 192 | { "crop", (PyCFunction) Image_crop, METH_VARARGS | METH_KEYWORDS, "Crop image to new size." }, 193 | { "transpose", (PyCFunction) Image_transpose, METH_VARARGS | METH_KEYWORDS, "Transpose/flip/rotate image." }, 194 | { "copyto", (PyCFunction) Image_copyto, METH_VARARGS | METH_KEYWORDS, "Copy data to a buffer." }, 195 | { NULL } /* Sentinel */ 196 | }; 197 | 198 | static PyTypeObject Image_Type = { 199 | /* The ob_type field must be initialized in the module init function 200 | * to be portable to Windows without using C++. */ 201 | PyVarObject_HEAD_INIT(NULL, 0) 202 | "accimage.Image", /*tp_name*/ 203 | sizeof(ImageObject), /*tp_basicsize*/ 204 | 0, /*tp_itemsize*/ 205 | /* methods */ 206 | 0, /* see initaccimage */ /*tp_dealloc*/ 207 | 0, /*tp_print*/ 208 | 0, /*tp_getattr*/ 209 | 0, /*tp_setattr*/ 210 | 0, /*tp_compare*/ 211 | 0, /*tp_repr*/ 212 | 0, /*tp_as_number*/ 213 | 0, /*tp_as_sequence*/ 214 | 0, /*tp_as_mapping*/ 215 | 0, /*tp_hash*/ 216 | 0, /*tp_call*/ 217 | 0, /*tp_str*/ 218 | 0, /*tp_getattro*/ 219 | 0, /*tp_setattro*/ 220 | 0, /*tp_as_buffer*/ 221 | Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ 222 | 0, /*tp_doc*/ 223 | 0, /*tp_traverse*/ 224 | 0, /*tp_clear*/ 225 | 0, /*tp_richcompare*/ 226 | 0, /*tp_weaklistoffset*/ 227 | 0, /*tp_iter*/ 228 | 0, /*tp_iternext*/ 229 | Image_methods, /*tp_methods*/ 230 | Image_members, /*tp_members*/ 231 | Image_getseters, /*tp_getset*/ 232 | 0, /* see initaccimage */ /*tp_base*/ 233 | 0, /*tp_dict*/ 234 | 0, /*tp_descr_get*/ 235 | 0, /*tp_descr_set*/ 236 | 0, /*tp_dictoffset*/ 237 | 0, /* see initaccimage */ /*tp_init*/ 238 | 0, /*tp_alloc*/ 239 | 0, /* see initaccimage */ /*tp_new*/ 240 | 0, /*tp_free*/ 241 | 0, /*tp_is_gc*/ 242 | }; 243 | 244 | static int Image_init(ImageObject *self, PyObject *args, PyObject *kwds) { 245 | const char *path; 246 | 247 | if (PyArg_ParseTuple(args, "s", &path)) { 248 | image_from_jpeg(self, path); 249 | } 250 | else { 251 | Py_buffer buffer; 252 | 253 | PyErr_Clear(); 254 | if (PyArg_ParseTuple(args, "y*", &buffer)) { 255 | void* buf = buffer.buf; 256 | Py_ssize_t size = buffer.len; 257 | image_from_buffer(self, buf, size); 258 | PyBuffer_Release(&buffer); 259 | } 260 | } 261 | 262 | return PyErr_Occurred() ? -1 : 0; 263 | } 264 | 265 | static void Image_dealloc(ImageObject *self) { 266 | if (self->buffer != NULL) { 267 | free(self->buffer); 268 | self->buffer = NULL; 269 | } 270 | } 271 | 272 | #if PY_MAJOR_VERSION == 2 273 | PyMODINIT_FUNC initaccimage(void) { 274 | #else 275 | PyMODINIT_FUNC PyInit_accimage(void) { 276 | #endif 277 | PyObject* m; 278 | 279 | /* 280 | * Due to cross platform compiler issues the slots must be filled here. 281 | * It's required for portability to Windows without requiring C++. 282 | */ 283 | Image_Type.tp_base = &PyBaseObject_Type; 284 | Image_Type.tp_init = (initproc) Image_init; 285 | Image_Type.tp_dealloc = (destructor) Image_dealloc; 286 | Image_Type.tp_new = PyType_GenericNew; 287 | 288 | #if PY_MAJOR_VERSION == 2 289 | m = Py_InitModule("accimage", NULL); 290 | #else 291 | static struct PyModuleDef module_def = { 292 | PyModuleDef_HEAD_INIT, 293 | "accimage", 294 | NULL, 295 | -1, 296 | NULL 297 | }; 298 | m = PyModule_Create(&module_def); 299 | #endif 300 | 301 | if (m == NULL) 302 | goto err; 303 | 304 | if (PyType_Ready(&Image_Type) < 0) 305 | goto err; 306 | 307 | PyModule_AddObject(m, "Image", (PyObject*) &Image_Type); 308 | 309 | #if PY_MAJOR_VERSION == 2 310 | return; 311 | #else 312 | return m; 313 | #endif 314 | 315 | err: 316 | #if PY_MAJOR_VERSION == 2 317 | return; 318 | #else 319 | return NULL; 320 | #endif 321 | } 322 | -------------------------------------------------------------------------------- /chicago.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pytorch/accimage/15ec9d4a95060ee54ab80fa3a7ae57a9ca7763ff/chicago.jpg -------------------------------------------------------------------------------- /imageops.c: -------------------------------------------------------------------------------- 1 | #include "accimage.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | 8 | void image_copy_deinterleave(ImageObject* self, unsigned char* output_buffer) { 9 | unsigned char* channel_buffers[3] = { 10 | output_buffer, 11 | output_buffer + self->height * self->width, 12 | output_buffer + 2 * self->height * self->width 13 | }; 14 | IppiSize roi = { self->width, self->height }; 15 | IppStatus ipp_status = ippiCopy_8u_C3P3R( 16 | self->buffer + (self->y_offset * self->row_stride + self->x_offset) * self->channels, 17 | self->row_stride * self->channels, 18 | channel_buffers, self->width, roi); 19 | if (ipp_status != ippStsNoErr) { 20 | PyErr_Format(PyExc_SystemError, "ippiCopy_8u_C3P3R failed with status %d", ipp_status); 21 | } 22 | } 23 | 24 | 25 | void image_copy_deinterleave_float(ImageObject* self, float* output_buffer) { 26 | unsigned char* tmp_buffer = NULL; 27 | IppiSize roi = { self->width, self->height }; 28 | 29 | tmp_buffer = malloc(self->height * self->width * self->channels); 30 | if (!tmp_buffer) { 31 | PyErr_NoMemory(); 32 | goto cleanup; 33 | } 34 | 35 | image_copy_deinterleave(self, tmp_buffer); 36 | if (PyErr_Occurred()) { 37 | goto cleanup; 38 | } 39 | 40 | IppStatus ipp_status = ippiConvert_8u32f_C3R( 41 | tmp_buffer, self->width * self->channels, 42 | output_buffer, self->width * self->channels * sizeof(float), 43 | roi); 44 | if (ipp_status != ippStsNoErr) { 45 | PyErr_Format(PyExc_SystemError, "ippiConvert_8u32f_C3R failed with status %d", ipp_status); 46 | } 47 | 48 | Ipp32f value[3] = {255.0f, 255.0f, 255.0f}; 49 | ipp_status = ippiDivC_32f_C3IR( 50 | value, output_buffer, self->width * self->channels * sizeof(float), 51 | roi); 52 | if (ipp_status != ippStsNoErr) { 53 | PyErr_Format(PyExc_SystemError, "ippiDivC_32f_C3IR failed with status %d", ipp_status); 54 | } 55 | 56 | cleanup: 57 | free(tmp_buffer); 58 | } 59 | 60 | 61 | void image_resize(ImageObject* self, int new_height, int new_width, int antialiasing) { 62 | IppStatus ipp_status; 63 | unsigned char* new_buffer = NULL; 64 | IppiSize old_size = { self->width, self->height }; 65 | IppiSize new_size = { new_width, new_height }; 66 | IppiPoint new_offset = { 0, 0 }; 67 | int specification_size = 0, initialization_buffer_size = 0, scratch_buffer_size = 0; 68 | IppiResizeSpec_32f* specification = NULL; 69 | Ipp8u* scratch_buffer = NULL; 70 | Ipp8u* initialization_buffer = NULL; 71 | 72 | new_buffer = malloc(new_height * new_width * self->channels); 73 | if (new_buffer == NULL) { 74 | PyErr_NoMemory(); 75 | goto cleanup; 76 | } 77 | 78 | ipp_status = ippiResizeGetSize_8u(old_size, new_size, ippLinear, antialiasing, 79 | &specification_size, &initialization_buffer_size); 80 | if (ipp_status != ippStsNoErr) { 81 | PyErr_Format(PyExc_SystemError, 82 | "ippiResizeGetSize_8u failed with status %d", ipp_status); 83 | goto cleanup; 84 | } 85 | 86 | initialization_buffer = malloc(initialization_buffer_size); 87 | if (initialization_buffer == NULL) { 88 | PyErr_NoMemory(); 89 | goto cleanup; 90 | } 91 | 92 | specification = malloc(specification_size); 93 | if (specification == NULL) { 94 | PyErr_NoMemory(); 95 | goto cleanup; 96 | } 97 | 98 | if (antialiasing) { 99 | ipp_status = ippiResizeAntialiasingLinearInit( 100 | old_size, new_size, specification, initialization_buffer); 101 | } else { 102 | ipp_status = ippiResizeLinearInit_8u(old_size, new_size, specification); 103 | } 104 | if (ipp_status != ippStsNoErr) { 105 | PyErr_Format(PyExc_SystemError, 106 | "ippiResizeLinearInit_8u failed with status %d", ipp_status); 107 | goto cleanup; 108 | } 109 | 110 | ipp_status = ippiResizeGetBufferSize_8u(specification, new_size, self->channels, &scratch_buffer_size); 111 | if (ipp_status != ippStsNoErr) { 112 | PyErr_Format(PyExc_SystemError, 113 | "ippiResizeGetBufferSize_8u failed with status %d", ipp_status); 114 | goto cleanup; 115 | } 116 | 117 | scratch_buffer = malloc(scratch_buffer_size); 118 | if (scratch_buffer == NULL) { 119 | PyErr_NoMemory(); 120 | goto cleanup; 121 | } 122 | 123 | if (antialiasing) { 124 | ipp_status = ippiResizeAntialiasing_8u_C3R( 125 | self->buffer + (self->y_offset * self->row_stride + self->x_offset) * self->channels, 126 | self->row_stride * self->channels, 127 | new_buffer, new_width * self->channels, new_offset, new_size, 128 | ippBorderRepl, NULL, specification, scratch_buffer); 129 | } else { 130 | ipp_status = ippiResizeLinear_8u_C3R( 131 | self->buffer + (self->y_offset * self->row_stride + self->x_offset) * self->channels, 132 | self->row_stride * self->channels, 133 | new_buffer, new_width * self->channels, new_offset, new_size, 134 | ippBorderRepl, NULL, specification, scratch_buffer); 135 | } 136 | if (ipp_status != ippStsNoErr) { 137 | PyErr_Format(PyExc_SystemError, 138 | "ippiResizeLinear_8u_C3R failed with status %d", ipp_status); 139 | goto cleanup; 140 | } 141 | 142 | free(self->buffer); 143 | self->buffer = new_buffer; 144 | new_buffer = NULL; 145 | 146 | self->height = new_height; 147 | self->width = new_width; 148 | self->row_stride = new_width; 149 | self->x_offset = 0; 150 | self->y_offset = 0; 151 | 152 | cleanup: 153 | free(new_buffer); 154 | free(specification); 155 | free(initialization_buffer); 156 | free(scratch_buffer); 157 | } 158 | 159 | 160 | void image_flip_left_right(ImageObject* self) { 161 | IppiSize roi = { self->width, self->height }; 162 | IppStatus ipp_status = ippiMirror_8u_C3IR( 163 | self->buffer + (self->y_offset * self->row_stride + self->x_offset) * self->channels, 164 | self->row_stride * self->channels, 165 | roi, ippAxsVertical); 166 | if (ipp_status != ippStsNoErr) 167 | PyErr_Format(PyExc_SystemError, "ippiMirror_8u_C3IR failed with status %d", ipp_status); 168 | } 169 | -------------------------------------------------------------------------------- /jpegloader.c: -------------------------------------------------------------------------------- 1 | #include "accimage.h" 2 | 3 | #include 4 | 5 | #include 6 | #ifndef LIBJPEG_TURBO_VERSION 7 | #error libjpeg-turbo required (not IJG libjpeg) 8 | #endif 9 | 10 | 11 | struct accimage_jpeg_error_mgr { 12 | struct jpeg_error_mgr pub; 13 | jmp_buf setjmp_buffer; 14 | }; 15 | 16 | 17 | static void accimage_jpeg_error_exit(j_common_ptr cinfo) { 18 | struct accimage_jpeg_error_mgr* accimage_err = (struct accimage_jpeg_error_mgr*) cinfo->err; 19 | longjmp(accimage_err->setjmp_buffer, 1); 20 | } 21 | 22 | 23 | void image_from_file(ImageObject* self, FILE* file) { 24 | struct jpeg_decompress_struct state = { 0 }; 25 | struct accimage_jpeg_error_mgr jpeg_error; 26 | unsigned char* buffer = NULL; 27 | 28 | state.err = jpeg_std_error(&jpeg_error.pub); 29 | jpeg_error.pub.error_exit = accimage_jpeg_error_exit; 30 | if (setjmp(jpeg_error.setjmp_buffer)) { 31 | char error_message[JMSG_LENGTH_MAX]; 32 | (*state.err->format_message)((j_common_ptr) &state, error_message); 33 | PyErr_Format(PyExc_IOError, "JPEG decoding failed: %s", error_message); 34 | goto cleanup; 35 | } 36 | 37 | jpeg_create_decompress(&state); 38 | jpeg_stdio_src(&state, file); 39 | jpeg_read_header(&state, TRUE); 40 | 41 | state.dct_method = JDCT_ISLOW; 42 | state.output_components = 3; 43 | state.out_color_components = 3; 44 | state.out_color_space = JCS_RGB; 45 | 46 | jpeg_start_decompress(&state); 47 | 48 | buffer = malloc(state.output_components * state.output_width * state.output_height); 49 | if (buffer == NULL) { 50 | PyErr_NoMemory(); 51 | goto cleanup; 52 | } 53 | 54 | while (state.output_scanline < state.output_height) { 55 | unsigned char* row = buffer + state.output_scanline * state.output_width * state.output_components; 56 | jpeg_read_scanlines(&state, &row, 1); 57 | } 58 | 59 | jpeg_finish_decompress(&state); 60 | 61 | /* Success. Commit object state */ 62 | self->buffer = buffer; 63 | buffer = NULL; 64 | 65 | self->channels = 3; 66 | self->height = state.output_height; 67 | self->width = state.output_width; 68 | self->row_stride = state.output_width; 69 | self->y_offset = 0; 70 | self->x_offset = 0; 71 | 72 | cleanup: 73 | jpeg_destroy_decompress(&state); 74 | 75 | free(buffer); 76 | 77 | if (file != NULL) { 78 | fclose(file); 79 | } 80 | } 81 | 82 | void image_from_buffer(ImageObject* self, void* buf, size_t size) { 83 | FILE* source = fmemopen(buf, size, "rb"); 84 | if (source == NULL) { 85 | PyErr_Format(PyExc_IOError, "failed to call fmemopen on buffer"); 86 | return; 87 | } 88 | 89 | image_from_file(self, source); 90 | } 91 | 92 | void image_from_jpeg(ImageObject* self, const char* path) { 93 | FILE* file = file = fopen(path, "rb"); 94 | if (file == NULL) { 95 | PyErr_Format(PyExc_IOError, "failed to open file %s", path); 96 | return; 97 | } 98 | 99 | image_from_file(self, file); 100 | } 101 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | 3 | accimage = Extension( 4 | "accimage", 5 | include_dirs=["/usr/local/opt/jpeg-turbo/include", "/opt/intel/ipp/include"], 6 | libraries=["jpeg", "ippi", "ipps"], 7 | library_dirs=["/usr/local/opt/jpeg-turbo/lib", "/opt/intel/ipp/lib"], 8 | sources=["accimagemodule.c", "jpegloader.c", "imageops.c"], 9 | ) 10 | 11 | setup( 12 | name="accimage", 13 | version="0.2.0", 14 | description="Accelerated image loader and preprocessor for Torch", 15 | author="Marat Dukhan", 16 | author_email="maratek@gmail.com", 17 | ext_modules=[accimage], 18 | ) 19 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import accimage 2 | import numpy as np 3 | import imageio 4 | import os 5 | 6 | ACCIMAGE_SAVE = os.environ.get('ACCIMAGE_SAVE', '') 7 | if len(ACCIMAGE_SAVE) and ACCIMAGE_SAVE.lower() not in {'0', 'false', 'no'}: 8 | SAVE_IMAGES = True 9 | else: 10 | SAVE_IMAGES = False 11 | 12 | def image_to_np(image): 13 | """ 14 | Returns: 15 | np.ndarray: Image converted to array with shape (width, height, channels) 16 | """ 17 | image_np = np.empty([image.channels, image.height, image.width], dtype=np.uint8) 18 | image.copyto(image_np) 19 | image_np = np.transpose(image_np, (1, 2, 0)) 20 | return image_np 21 | 22 | 23 | def save_image(path, image): 24 | imageio.imwrite(path, image_to_np(image)) 25 | 26 | 27 | def test_reading_image(): 28 | image = accimage.Image("chicago.jpg") 29 | if SAVE_IMAGES: 30 | save_image('test_reading_image.jpg', image) 31 | assert image.width == 1920 32 | assert image.height == 931 33 | 34 | 35 | def test_reading_image_from_memory(): 36 | from_file = accimage.Image("chicago.jpg") 37 | bytes = open("chicago.jpg", "rb").read() 38 | from_bytes = accimage.Image(bytes) 39 | if SAVE_IMAGES: 40 | save_image('test_reading_image_from_memory.jpg', from_bytes) 41 | assert from_bytes.width == 1920 42 | assert from_bytes.height == 931 43 | np.testing.assert_array_equal(image_to_np(from_file), image_to_np(from_bytes)) 44 | 45 | 46 | def test_resizing(): 47 | image = accimage.Image("chicago.jpg") 48 | 49 | image.resize(size=(200, 200)) 50 | if SAVE_IMAGES: 51 | save_image('test_resizing.jpg', image) 52 | 53 | assert image.width == 200 54 | assert image.height == 200 55 | 56 | def test_cropping(): 57 | image = accimage.Image("chicago.jpg") 58 | 59 | image.crop(box=(50, 50, 150, 150)) 60 | if SAVE_IMAGES: 61 | save_image('test_cropping.jpg', image) 62 | 63 | assert image.width == 100 64 | assert image.height == 100 65 | 66 | def test_flipping(): 67 | image = accimage.Image("chicago.jpg") 68 | original_image_np = image_to_np(image) 69 | 70 | FLIP_LEFT_RIGHT = 0 71 | image.transpose(FLIP_LEFT_RIGHT) 72 | if SAVE_IMAGES: 73 | save_image('test_flipping.jpg', image) 74 | 75 | new_image_np = image_to_np(image) 76 | assert image.width == 1920 77 | assert image.height == 931 78 | np.testing.assert_array_equal(new_image_np[:, ::-1, :], original_image_np) 79 | --------------------------------------------------------------------------------