Skip to content

Commit 58b1dc1

Browse files
authored
Merge pull request #1907 from ScriptLineStudios/grayscale
transform.grayscale and Color.grayscale
2 parents 121d303 + 510ae29 commit 58b1dc1

File tree

10 files changed

+214
-0
lines changed

10 files changed

+214
-0
lines changed

buildconfig/stubs/pygame/color.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class Color(Collection[int]):
4949
def set_length(self, length: int) -> None: ...
5050
def lerp(self, color: ColorValue, amount: float) -> Color: ...
5151
def premul_alpha(self) -> Color: ...
52+
def grayscale(self) -> Color: ...
5253
@overload
5354
def update(self, r: int, g: int, b: int, a: int = 255) -> None: ...
5455
@overload

buildconfig/stubs/pygame/transform.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ def scale_by(
1919
def rotate(surface: Surface, angle: float) -> Surface: ...
2020
def rotozoom(surface: Surface, angle: float, scale: float) -> Surface: ...
2121
def scale2x(surface: Surface, dest_surface: Optional[Surface] = None) -> Surface: ...
22+
def grayscale(surface: Surface, dest_surface: Optional[Surface] = None) -> Surface: ...
2223
def smoothscale(
2324
surface: Surface,
2425
size: Coordinate,

docs/reST/ref/color.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,18 @@
226226

227227
.. ## Color.set_length ##
228228
229+
.. method:: grayscale
230+
231+
| :sl:`returns the grayscale of a Color`
232+
| :sg:`grayscale() -> Color`
233+
234+
Returns a new Color object which represents the grayscaled version of self, using the luminosity formula,
235+
which weights red, green, and blue according to their relative contribution to perceived brightness.
236+
237+
.. versionadded:: 2.1.4
238+
239+
.. ## Color.grayscale ##
240+
229241
.. method:: lerp
230242

231243
| :sl:`returns a linear interpolation to the given Color.`

docs/reST/ref/transform.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,20 @@ Instead, always begin with the original image and scale to the desired size.)
237237

238238
.. ## pygame.transform.average_color ##
239239
240+
.. function:: grayscale
241+
242+
| :sl:`grayscale a surface`
243+
| :sg:`grayscale(surface, dest_surface=None) -> Surface`
244+
245+
Returns a grayscaled version of the original surface using the luminosity formula which weights red, green and blue according to their wavelengths.
246+
247+
An optional destination surface can be passed which is faster than creating a new Surface.
248+
This destination surface must have the same dimensions (width, height) and depth as the source Surface.
249+
250+
.. versionadded:: 2.1.4
251+
252+
.. ## pygame.transform.grayscale ##
253+
240254
.. function:: threshold
241255

242256
| :sl:`finds which, and how many pixels in a surface are within a threshold of a 'search_color' or a 'search_surf'.`

src_c/color.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ _color_set_length(pgColorObject *, PyObject *);
9393
static PyObject *
9494
_color_lerp(pgColorObject *, PyObject *, PyObject *);
9595
static PyObject *
96+
_color_grayscale(pgColorObject *);
97+
static PyObject *
9698
_premul_alpha(pgColorObject *, PyObject *);
9799
static PyObject *
98100
_color_update(pgColorObject *self, PyObject *const *args, Py_ssize_t nargs);
@@ -200,6 +202,8 @@ static PyMethodDef _color_methods[] = {
200202
DOC_COLORSETLENGTH},
201203
{"lerp", (PyCFunction)_color_lerp, METH_VARARGS | METH_KEYWORDS,
202204
DOC_COLORLERP},
205+
{"grayscale", (PyCFunction)_color_grayscale, METH_NOARGS,
206+
DOC_COLORGRAYSCALE},
203207
{"premul_alpha", (PyCFunction)_premul_alpha, METH_NOARGS,
204208
DOC_COLORPREMULALPHA},
205209
{"update", (PyCFunction)_color_update, METH_FASTCALL, DOC_COLORUPDATE},
@@ -782,6 +786,25 @@ _color_correct_gamma(pgColorObject *color, PyObject *args)
782786
return (PyObject *)_color_new_internal(Py_TYPE(color), rgba);
783787
}
784788

789+
/**
790+
* color.grayscale()
791+
*/
792+
static PyObject *
793+
_color_grayscale(pgColorObject *self)
794+
{
795+
// RGBA to GRAY formula used by OpenCV
796+
Uint8 grayscale_pixel =
797+
(Uint8)(0.299 * self->data[0] + 0.587 * self->data[1] +
798+
0.114 * self->data[2]);
799+
800+
Uint8 new_rgba[4];
801+
new_rgba[0] = grayscale_pixel;
802+
new_rgba[1] = grayscale_pixel;
803+
new_rgba[2] = grayscale_pixel;
804+
new_rgba[3] = self->data[3];
805+
return (PyObject *)_color_new_internal(Py_TYPE(self), new_rgba);
806+
}
807+
785808
/**
786809
* color.lerp(other, x)
787810
*/

src_c/doc/color_doc.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#define DOC_COLORNORMALIZE "normalize() -> tuple\nReturns the normalized RGBA values of the Color."
1212
#define DOC_COLORCORRECTGAMMA "correct_gamma (gamma) -> Color\nApplies a certain gamma value to the Color."
1313
#define DOC_COLORSETLENGTH "set_length(len) -> None\nSet the number of elements in the Color to 1,2,3, or 4."
14+
#define DOC_COLORGRAYSCALE "grayscale() -> Color\nreturns the grayscale of a Color"
1415
#define DOC_COLORLERP "lerp(Color, float) -> Color\nreturns a linear interpolation to the given Color."
1516
#define DOC_COLORPREMULALPHA "premul_alpha() -> Color\nreturns a Color where the r,g,b components have been multiplied by the alpha."
1617
#define DOC_COLORUPDATE "update(r, g, b) -> None\nupdate(r, g, b, a=255) -> None\nupdate(color_value) -> None\nSets the elements of the color"
@@ -70,6 +71,10 @@ pygame.Color.set_length
7071
set_length(len) -> None
7172
Set the number of elements in the Color to 1,2,3, or 4.
7273
74+
pygame.Color.grayscale
75+
grayscale() -> Color
76+
returns the grayscale of a Color
77+
7378
pygame.Color.lerp
7479
lerp(Color, float) -> Color
7580
returns a linear interpolation to the given Color.

src_c/doc/transform_doc.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#define DOC_PYGAMETRANSFORMLAPLACIAN "laplacian(surface, dest_surface=None) -> Surface\nfind edges in a surface"
1515
#define DOC_PYGAMETRANSFORMAVERAGESURFACES "average_surfaces(surfaces, dest_surface=None, palette_colors=1) -> Surface\nfind the average surface from many surfaces."
1616
#define DOC_PYGAMETRANSFORMAVERAGECOLOR "average_color(surface, rect=None, consider_alpha=False) -> Color\nfinds the average color of a surface"
17+
#define DOC_PYGAMETRANSFORMGRAYSCALE "grayscale(surface, dest_surface=None) -> Surface\ngrayscale a surface"
1718
#define DOC_PYGAMETRANSFORMTHRESHOLD "threshold(dest_surface, surface, search_color, threshold=(0,0,0,0), set_color=(0,0,0,0), set_behavior=1, search_surf=None, inverse_set=False) -> num_threshold_pixels\nfinds which, and how many pixels in a surface are within a threshold of a 'search_color' or a 'search_surf'."
1819

1920

@@ -80,6 +81,10 @@ pygame.transform.average_color
8081
average_color(surface, rect=None, consider_alpha=False) -> Color
8182
finds the average color of a surface
8283
84+
pygame.transform.grayscale
85+
grayscale(surface, dest_surface=None) -> Surface
86+
grayscale a surface
87+
8388
pygame.transform.threshold
8489
threshold(dest_surface, surface, search_color, threshold=(0,0,0,0), set_color=(0,0,0,0), set_behavior=1, search_surf=None, inverse_set=False) -> num_threshold_pixels
8590
finds which, and how many pixels in a surface are within a threshold of a 'search_color' or a 'search_surf'.

src_c/transform.c

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2110,6 +2110,87 @@ clamp_4
21102110

21112111
#endif
21122112

2113+
SDL_Surface *
2114+
grayscale(pgSurfaceObject *srcobj, pgSurfaceObject *dstobj)
2115+
{
2116+
SDL_Surface *src = pgSurface_AsSurface(srcobj);
2117+
SDL_Surface *newsurf;
2118+
2119+
if (!dstobj) {
2120+
newsurf = newsurf_fromsurf(src, srcobj->surf->w, srcobj->surf->h);
2121+
if (!newsurf)
2122+
return NULL;
2123+
}
2124+
else {
2125+
newsurf = pgSurface_AsSurface(dstobj);
2126+
}
2127+
2128+
if (newsurf->w != src->w || newsurf->h != src->h) {
2129+
return (SDL_Surface *)(RAISE(
2130+
PyExc_ValueError,
2131+
"Destination surface must be the same size as source surface."));
2132+
}
2133+
2134+
if (src->format->BytesPerPixel != newsurf->format->BytesPerPixel) {
2135+
return (SDL_Surface *)(RAISE(
2136+
PyExc_ValueError,
2137+
"Source and destination surfaces need the same format."));
2138+
}
2139+
2140+
int x, y;
2141+
for (y = 0; y < src->h; y++) {
2142+
for (x = 0; x < src->w; x++) {
2143+
Uint32 pixel;
2144+
Uint8 *pix;
2145+
SURF_GET_AT(pixel, src, x, y, (Uint8 *)src->pixels, src->format,
2146+
pix);
2147+
Uint8 r, g, b, a;
2148+
SDL_GetRGBA(pixel, src->format, &r, &g, &b, &a);
2149+
2150+
// RGBA to GRAY formula used by OpenCV
2151+
Uint8 grayscale_pixel = (Uint8)(0.299 * r + 0.587 * g + 0.114 * b);
2152+
Uint32 new_pixel =
2153+
SDL_MapRGBA(newsurf->format, grayscale_pixel, grayscale_pixel,
2154+
grayscale_pixel, a);
2155+
SURF_SET_AT(new_pixel, newsurf, x, y, (Uint8 *)newsurf->pixels,
2156+
newsurf->format, pix);
2157+
}
2158+
}
2159+
2160+
SDL_UnlockSurface(newsurf);
2161+
2162+
return newsurf;
2163+
}
2164+
2165+
static PyObject *
2166+
surf_grayscale(PyObject *self, PyObject *args, PyObject *kwargs)
2167+
{
2168+
pgSurfaceObject *surfobj;
2169+
pgSurfaceObject *surfobj2 = NULL;
2170+
SDL_Surface *newsurf;
2171+
2172+
static char *keywords[] = {"surface", "dest_surface", NULL};
2173+
2174+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!|O!", keywords,
2175+
&pgSurface_Type, &surfobj,
2176+
&pgSurface_Type, &surfobj2))
2177+
return NULL;
2178+
2179+
newsurf = grayscale(surfobj, surfobj2);
2180+
2181+
if (!newsurf) {
2182+
return NULL;
2183+
}
2184+
2185+
if (surfobj2) {
2186+
Py_INCREF(surfobj2);
2187+
return (PyObject *)surfobj2;
2188+
}
2189+
else {
2190+
return (PyObject *)pgSurface_New(newsurf);
2191+
}
2192+
}
2193+
21132194
/*
21142195
number to use for missing samples
21152196
*/
@@ -2997,6 +3078,8 @@ static PyMethodDef _transform_methods[] = {
29973078
METH_VARARGS | METH_KEYWORDS, DOC_PYGAMETRANSFORMAVERAGESURFACES},
29983079
{"average_color", (PyCFunction)surf_average_color,
29993080
METH_VARARGS | METH_KEYWORDS, DOC_PYGAMETRANSFORMAVERAGECOLOR},
3081+
{"grayscale", (PyCFunction)surf_grayscale, METH_VARARGS | METH_KEYWORDS,
3082+
DOC_PYGAMETRANSFORMGRAYSCALE},
30003083
{NULL, NULL, 0, NULL}};
30013084

30023085
MODINIT_DEFINE(transform)

test/color_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,31 @@ def test_color_contains(self):
11101110
self.assertRaises(TypeError, lambda: "string" in c)
11111111
self.assertRaises(TypeError, lambda: 3.14159 in c)
11121112

1113+
def test_grayscale(self):
1114+
Color = pygame.color.Color
1115+
1116+
color = Color(255, 0, 0, 255)
1117+
self.assertEqual(color.grayscale(), Color(76, 76, 76, 255))
1118+
color = Color(3, 5, 7, 255)
1119+
self.assertEqual(color.grayscale(), Color(4, 4, 4, 255))
1120+
color = Color(3, 5, 70, 255)
1121+
self.assertEqual(color.grayscale(), Color(11, 11, 11, 255))
1122+
color = Color(3, 50, 70, 255)
1123+
self.assertEqual(color.grayscale(), Color(38, 38, 38, 255))
1124+
color = Color(30, 50, 70, 255)
1125+
self.assertEqual(color.grayscale(), Color(46, 46, 46, 255))
1126+
1127+
color = Color(255, 0, 0, 144)
1128+
self.assertEqual(color.grayscale(), Color(76, 76, 76, 144))
1129+
color = Color(3, 5, 7, 144)
1130+
self.assertEqual(color.grayscale(), Color(4, 4, 4, 144))
1131+
color = Color(3, 5, 70, 144)
1132+
self.assertEqual(color.grayscale(), Color(11, 11, 11, 144))
1133+
color = Color(3, 50, 70, 144)
1134+
self.assertEqual(color.grayscale(), Color(38, 38, 38, 144))
1135+
color = Color(30, 50, 70, 144)
1136+
self.assertEqual(color.grayscale(), Color(46, 46, 46, 144))
1137+
11131138
def test_lerp(self):
11141139
# setup
11151140
Color = pygame.color.Color

test/transform_test.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,51 @@ def test_smoothscale_by(self):
158158
dest = pygame.Surface((64, 48))
159159
pygame.transform.smoothscale_by(s, (2.0, 1.5), dest_surface=dest)
160160

161+
def test_grayscale(self):
162+
s = pygame.Surface((32, 32))
163+
s.fill((255, 0, 0))
164+
165+
s2 = pygame.transform.grayscale(s)
166+
self.assertEqual(pygame.transform.average_color(s2)[0], 76)
167+
self.assertEqual(pygame.transform.average_color(s2)[1], 76)
168+
self.assertEqual(pygame.transform.average_color(s2)[2], 76)
169+
170+
dest = pygame.Surface((32, 32), depth=32)
171+
pygame.transform.grayscale(s, dest)
172+
self.assertEqual(pygame.transform.average_color(dest)[0], 76)
173+
self.assertEqual(pygame.transform.average_color(dest)[1], 76)
174+
self.assertEqual(pygame.transform.average_color(dest)[2], 76)
175+
176+
dest = pygame.Surface((32, 32), depth=32)
177+
s.fill((34, 12, 65))
178+
pygame.transform.grayscale(s, dest)
179+
self.assertEqual(pygame.transform.average_color(dest)[0], 24)
180+
self.assertEqual(pygame.transform.average_color(dest)[1], 24)
181+
self.assertEqual(pygame.transform.average_color(dest)[2], 24)
182+
183+
dest = pygame.Surface((32, 32), depth=32)
184+
s.fill((123, 123, 123))
185+
pygame.transform.grayscale(s, dest)
186+
self.assertIn(pygame.transform.average_color(dest)[0], [123, 122])
187+
self.assertIn(pygame.transform.average_color(dest)[1], [123, 122])
188+
self.assertIn(pygame.transform.average_color(dest)[2], [123, 122])
189+
190+
s = pygame.Surface((32, 32), depth=24)
191+
s.fill((255, 0, 0))
192+
dest = pygame.Surface((32, 32), depth=24)
193+
pygame.transform.grayscale(s, dest)
194+
self.assertEqual(pygame.transform.average_color(dest)[0], 76)
195+
self.assertEqual(pygame.transform.average_color(dest)[1], 76)
196+
self.assertEqual(pygame.transform.average_color(dest)[2], 76)
197+
198+
s = pygame.Surface((32, 32), depth=16)
199+
s.fill((255, 0, 0))
200+
dest = pygame.Surface((32, 32), depth=16)
201+
pygame.transform.grayscale(s, dest)
202+
self.assertEqual(pygame.transform.average_color(dest)[0], 72)
203+
self.assertEqual(pygame.transform.average_color(dest)[1], 76)
204+
self.assertEqual(pygame.transform.average_color(dest)[2], 72)
205+
161206
def test_threshold__honors_third_surface(self):
162207
# __doc__ for threshold as of Tue 07/15/2008
163208

0 commit comments

Comments
 (0)