Skip to content

Commit 63b902d

Browse files
authored
Merge pull request #11 from lukaspanni/AutoGreyscale
Auto greyscale
2 parents 2c6e0d9 + f85089a commit 63b902d

File tree

7 files changed

+230
-35
lines changed

7 files changed

+230
-35
lines changed

ImageCopy/ActionRunner.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ImageCopy.Actions.AutoGreyscale import AutoGreyscale
12
from ImageCopy.Actions.ExifEditing import ExifEditing
23
from ImageCopy.Transformers.GroupingTransform import GroupingTransform
34
from ImageCopy.Transformers.RawSeparateTransform import RawSeparateTransform
@@ -19,6 +20,8 @@ def __init__(self, config: Config):
1920
self.path_transformers.append(RawSeparateTransform(config.raw_separate))
2021
if self.config.exif is not None:
2122
self.after_actions.append(ExifEditing(self.config.exif))
23+
if self.config.greyscale is not None:
24+
self.after_actions.append(AutoGreyscale(self.config.greyscale))
2225

2326
def execute_transform(self, images: dict):
2427
for transformer in self.path_transformers:

ImageCopy/Actions/AutoGreyscale.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import os
2+
import numpy as np
3+
from PIL import Image
4+
from ImageCopy.Actions.AfterCopyAction import AfterCopyAction
5+
from ImageCopy.Actions.GreyscaleConverter import GreyscaleConverter
6+
7+
8+
class AutoGreyscale(AfterCopyAction):
9+
"""
10+
Creates a greyscale copy of copied images
11+
"""
12+
13+
def __init__(self, config: dict):
14+
self.converter = GreyscaleConverter()
15+
if 'algorithm' not in config:
16+
self.converter.algorithm = "average"
17+
else:
18+
self.converter.algorithm = config['algorithm']
19+
if 'file_name' not in config:
20+
self.file_name = "greyscale"
21+
else:
22+
self.file_name = config['file_name']
23+
24+
def execute(self, images: dict):
25+
for img in images:
26+
if not img.is_raw():
27+
pil_img = Image.open(images[img])
28+
img_data = np.array(pil_img)
29+
pil_img = None
30+
greyscale_img = Image.fromarray(self.converter.execute(img_data))
31+
path = os.path.splitext(images[img])
32+
greyscale_img.save(path[0] + "_" + self.file_name + path[1])
33+
img_data = None
34+
greyscale_img = None

ImageCopy/Actions/ExifEditing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ def _load_exif_config(config: dict):
1616

1717

1818
class ExifEditing(AfterCopyAction):
19+
"""
20+
Sets Exif-Data in copied Images
21+
"""
1922

2023
def __init__(self, exif_data: dict):
2124
self.exif_data = _load_exif_config(exif_data)
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import numpy as np
2+
from numba import stencil, njit, cuda
3+
4+
5+
@stencil(neighborhood=((0, 0), (0, 0), (0, 2)))
6+
def stencil_avg(px):
7+
return np.mean(px[0, 0, 0:3])
8+
9+
10+
@stencil(neighborhood=((0, 0), (0, 0), (0, 2)))
11+
def stencil_luminosity(px):
12+
return px[0, 0, 0] * 0.21 + px[0, 0, 1] * 0.72 + px[0, 0, 2] * 0.07
13+
14+
15+
@stencil(neighborhood=((0, 0), (0, 0), (0, 2)))
16+
def stencil_lightness(px):
17+
return 0.5 * (np.max(px[0, 0, 0:3]) + np.min(px[0, 0, 0:3]))
18+
19+
20+
@njit(parallel=True)
21+
def greyscale_avg(arr):
22+
return stencil_avg(arr)
23+
24+
25+
@njit(parallel=True)
26+
def greyscale_luminosity(arr):
27+
return stencil_luminosity(arr)
28+
29+
30+
@njit(parallel=True)
31+
def greyscale_lightness(arr):
32+
return stencil_lightness(arr)
33+
34+
35+
@cuda.jit(device=True)
36+
def gpu_avg(px_val):
37+
return (px_val[0] + px_val[1] + px_val[2]) // 3
38+
39+
40+
@cuda.jit(device=True)
41+
def gpu_luminosity(px_val):
42+
return (px_val[0] * 0.21 + px_val[1] * 0.72 + px_val[2] * 0.07) // 1
43+
44+
45+
@cuda.jit(device=True)
46+
def gpu_lightness(px_val):
47+
return (0.5 * (max(px_val[0], px_val[1], px_val[2]) + min(px_val[0], px_val[1], px_val[2]))) // 1
48+
49+
50+
@cuda.jit()
51+
def gpu_greyscale_avg(img):
52+
x, y = cuda.grid(2)
53+
d1, d2 = cuda.gridsize(2)
54+
for i in range(x, img.shape[0], d1):
55+
for j in range(y, img.shape[1], d2):
56+
img[i][j] = gpu_avg(img[i][j])
57+
58+
59+
@cuda.jit()
60+
def gpu_greyscale_luminosity(img):
61+
x, y = cuda.grid(2)
62+
d1, d2 = cuda.gridsize(2)
63+
for i in range(x, img.shape[0], d1):
64+
for j in range(y, img.shape[1], d2):
65+
img[i][j] = gpu_luminosity(img[i][j])
66+
67+
68+
@cuda.jit()
69+
def gpu_greyscale_lightness(img):
70+
x, y = cuda.grid(2)
71+
d1, d2 = cuda.gridsize(2)
72+
for i in range(x, img.shape[0], d1):
73+
for j in range(y, img.shape[1], d2):
74+
img[i][j] = gpu_lightness(img[i][j])
75+
76+
77+
algorithms = {
78+
"average": {"cpu": greyscale_avg, "gpu": gpu_greyscale_avg},
79+
"luminosity": {"cpu": greyscale_luminosity, "gpu": gpu_greyscale_luminosity},
80+
"lightness": {"cpu": greyscale_lightness, "gpu": gpu_greyscale_lightness}
81+
}
82+
83+
84+
class GreyscaleConverter:
85+
algorithm: str
86+
87+
def __init__(self):
88+
self.gpu = cuda.is_available()
89+
90+
def execute(self, img_data):
91+
if not self.gpu:
92+
return algorithms[self.algorithm]["cpu"](img_data).astype(np.uint8)[:, :, 0]
93+
else:
94+
dev_arr = cuda.to_device(img_data)
95+
algorithms[self.algorithm]["gpu"][(32, 32), (16, 16)](dev_arr)
96+
return dev_arr.copy_to_host()

ImageCopy/config.example.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ modules:
22
- grouping
33
- raw_separate
44
- exif
5+
- greyscale
56
input-output:
67
input_dir: G:/
78
output_dir: E:/Images/2020
@@ -10,6 +11,12 @@ raw_separate:
1011
raw_dir_name: RAW
1112
grouping:
1213
year: yes
13-
month: yes
14-
month_named: yes
15-
day: no
14+
month: no
15+
month_named: no
16+
day: no
17+
exif:
18+
artist: Lukas Panni
19+
copyright: Lukas Panni
20+
greyscale:
21+
algorithm: average
22+
file_name: greyscale

README.md

Lines changed: 81 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
Tool for fast and easy copying of RAW/JPG images from a camera to a hard drive.
88

9-
## Features
9+
## Features (version 2.0)
1010

1111
- Search and copy all image files automatically.
1212
- Separate RAW and JPG images in different folders.
@@ -16,6 +16,8 @@ Tool for fast and easy copying of RAW/JPG images from a camera to a hard drive.
1616
- Month (either 1-12 or January-December)
1717
- Day
1818

19+
- Set EXIF Data in JPG images (artist and copyright)
20+
- Create a greyscale copy of JPG images
1921

2022
## Planned Features
2123

@@ -28,43 +30,90 @@ Tool for fast and easy copying of RAW/JPG images from a camera to a hard drive.
2830

2931
**Reccomendation:** rename the config.example.yml and adjust settings there.
3032

33+
**Module config:** list of modules to use.
34+
````yaml
35+
modules:
36+
- grouping
37+
- raw_separate
38+
- exif
39+
- greyscale
40+
````
41+
3142
Basic Config (copies all images from `input_dir` including all subdirectories to `output_dir`):
3243
```yaml
3344
input-output:
34-
input_dir: /SD/DCIM/
35-
output_dir: /home/img/
36-
separate_raw: no
45+
input_dir: /SD/DCIM/
46+
output_dir: /home/img/
3747
```
38-
To create a separate folder for RAW-Images you have to set `separate_raw` to `yes` and provide a name for `raw_dir_name`.
3948

40-
Additionally there is the option to group images by creation date.
41-
You can choose to group images based on **year**, **month** and/or **date**.
42-
If you choose to group by month you can enable `month_named`. This will create directories with the months name instead of its number.
43-
The following example shows how to use the grouping-feature:
44-
```yaml
45-
grouping:
46-
year: yes
47-
month: yes
48-
month_named: yes
49-
day: no
50-
```
49+
#### Modules
50+
51+
Currently (Version 2.0) available:
52+
53+
- grouping
54+
55+
Group images by creation date.
56+
You can choose to group images based on **year**, **month** and/or **date**.
57+
If you choose to group by month you can enable `month_named`. This will create directories with the months name instead of its number.
58+
The following example shows how to use the grouping-feature:
59+
```yaml
60+
grouping:
61+
year: yes
62+
month: yes
63+
month_named: yes
64+
day: no
65+
```
66+
67+
The resulting directory structure will look something like this:
68+
69+
```
70+
/
71+
├── 2019
72+
│ └── May
73+
| └── DSC00192.JPG
74+
| └── DSC00193.JPG
75+
| └── November
76+
| └── DSC01337.JPG
77+
| └── DSC01342.JPG
78+
├── 2020
79+
│ └── March
80+
| └── DSC00042.JPG
81+
```
82+
83+
- raw_separate
84+
85+
Separate RAW Images in different directory specified by `raw_dir_name`. Example usage:
86+
````yaml
87+
raw_separate:
88+
separate_raw: yes
89+
raw_dir_name: RAW
90+
````
91+
92+
- exif
93+
94+
Set Artist and Copyright in JPG images, RAW images are not affected. Example usage:
95+
96+
```yaml
97+
exif:
98+
artist: Lukas Panni
99+
copyright: Lukas Panni
100+
```
101+
102+
- greyscale
103+
104+
Create a greyscale copy of every JPG image. Can use one of three different algorithms (average, luminosity, lightness) default: average.
105+
`file_name` specifies a string that is appended to the original file name to identify the greyscale copy.
106+
107+
```yaml
108+
greyscale:
109+
algorithm: average
110+
file_name: greyscale
111+
```
112+
113+
114+
51115

52-
The resulting directory structure will look something like this:
53-
```
54-
/
55-
├── 2019
56-
│ └── May
57-
| └── DSC00192.JPG
58-
| └── DSC00193.JPG
59-
| └── November
60-
| └── DSC01337.JPG
61-
| └── DSC01342.JPG
62-
├── 2020
63-
│ └── March
64-
| └── DSC00042.JPG
65-
```
66-
67116
### Execute
68117

69-
If the configuration file is set up you can execute the `image_copy.py` script.
118+
If the configuration file is set up you can execute the `image_copy.py` script or the bundled executable for your platform.
70119
When the input and output directories stay the same and you don't want to group differently you can execute it as often as you want wihtout changing the config.

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
piexif==1.1.3
22
PyYAML==5.3.1
33
pytest==5.4.3
4+
Pillow==7.2.0
5+
numba==0.50.0
6+
numpy==1.19.0

0 commit comments

Comments
 (0)