Skip to content

Commit 13a45ca

Browse files
committed
Adding smoothing and balancing colors Minecraft map
This class gives you option top smooth out the colors when image shall be added to map. This is still work in progress.
1 parent 7f59309 commit 13a45ca

File tree

1 file changed

+276
-0
lines changed
  • Item Creator/src/main/java/org/broken/arrow/library/itemcreator/meta/map/color/parser

1 file changed

+276
-0
lines changed
Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
package org.broken.arrow.library.itemcreator.meta.map.color.parser;
2+
3+
import org.broken.arrow.library.itemcreator.meta.map.MapRendererData;
4+
5+
import java.awt.*;
6+
import java.awt.image.BufferedImage;
7+
8+
/**
9+
* Utility class for smoothing and balancing colors from a source image before rendering
10+
* onto a Minecraft-style map.
11+
* <p>
12+
* Certain images produce extreme brightness differences or isolated bright pixels
13+
* which result in visual artifacts (e.g., "snow" or white speckles).
14+
* This class applies lightweight filtering to reduce those issues while preserving detail.
15+
* <p>
16+
* The filtered pixels are forwarded into the provided {@link MapRendererData} instance.
17+
* <strong>Recommended:</strong> scale the image before passing it in. Map items only
18+
* support 128×128 pixels, so supplying very large images (e.g., 4000×4000) would be
19+
* unnecessarily expensive.
20+
*/
21+
public class RenderColors {
22+
23+
private RenderColors() {
24+
}
25+
26+
/**
27+
* Processes a scaled image, smooths brightness inconsistencies,
28+
* and sends final pixel colors to the provided {@link MapRendererData}.
29+
*
30+
* @param scaled the image already scaled to map resolution (typically 128×128)
31+
* @param mapRendererData the map data container that receives processed pixels
32+
*/
33+
public static void renderFromImage(final BufferedImage scaled, final MapRendererData mapRendererData) {
34+
int width = scaled.getWidth();
35+
int height = scaled.getHeight();
36+
37+
BufferedImage filtered = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
38+
39+
float[] neighB = new float[8];
40+
float[] hsb = new float[3];
41+
float[] hsb2 = new float[3];
42+
43+
for (int y = 1; y < height - 1; y++) {
44+
for (int x = 1; x < width - 1; x++) {
45+
46+
getHSB(scaled.getRGB(x, y), hsb);
47+
float centerB = hsb[2];
48+
int idx = 0;
49+
neighbouringPixels(scaled, neighB, idx, x, y, hsb2);
50+
51+
float median = getMedianBrightness(neighB);
52+
float diff = centerB - median;
53+
if (Math.abs(diff) > 0.18f) {
54+
centerB = centerB * 0.4f + median * 0.6f;
55+
}
56+
57+
float bilateral = 0f, weightSum = 0f;
58+
for (int dy = -1; dy <= 1; dy++) {
59+
for (int dx = -1; dx <= 1; dx++) {
60+
if (dx == 0 && dy == 0) continue;
61+
62+
float nb = neighB[idx++];
63+
float distW = (dx == 0 || dy == 0) ? 1f : 0.7f;
64+
float diffB = Math.abs(centerB - nb);
65+
float colorW = (diffB < 0.07f) ? 1f : 0.2f;
66+
float w = distW * colorW;
67+
68+
bilateral += nb * w;
69+
weightSum += w;
70+
}
71+
}
72+
73+
if (weightSum > 0) {
74+
float smoothB = bilateral / weightSum;
75+
centerB = centerB * 0.85f + smoothB * 0.15f;
76+
}
77+
78+
int rgb = HSBtoRGB(hsb[0], hsb[1], clamp(centerB, 0f, 1f));
79+
filtered.setRGB(x, y, rgb);
80+
}
81+
}
82+
83+
addPixels(mapRendererData, height, width, filtered);
84+
}
85+
86+
87+
/**
88+
* Converts a color from HSB components into packed RGB format (0xAARRGGBB).
89+
* This matches the RGB format used by {@link Color#getRGB()} and can be directly
90+
* passed to the {@link Color#Color(int)} constructor.
91+
*
92+
* @param hue hue component (any float; integer part is ignored)
93+
* @param saturation value in range 0.0–1.0
94+
* @param brightness value in range 0.0–1.0
95+
* @return a packed ARGB integer representing the color
96+
*/
97+
public static int HSBtoRGB(float hue, float saturation, float brightness) {
98+
int r = 0, g = 0, b = 0;
99+
if (saturation == 0) {
100+
r = g = b = (int) (brightness * 255.0f + 0.5f);
101+
} else {
102+
float h = (hue - (float) Math.floor(hue)) * 6.0f;
103+
float f = h - (float) java.lang.Math.floor(h);
104+
float p = brightness * (1.0f - saturation);
105+
float q = brightness * (1.0f - saturation * f);
106+
float t = brightness * (1.0f - (saturation * (1.0f - f)));
107+
switch ((int) h) {
108+
case 0:
109+
r = (int) (brightness * 255.0f + 0.5f);
110+
g = (int) (t * 255.0f + 0.5f);
111+
b = (int) (p * 255.0f + 0.5f);
112+
break;
113+
case 1:
114+
r = (int) (q * 255.0f + 0.5f);
115+
g = (int) (brightness * 255.0f + 0.5f);
116+
b = (int) (p * 255.0f + 0.5f);
117+
break;
118+
case 2:
119+
r = (int) (p * 255.0f + 0.5f);
120+
g = (int) (brightness * 255.0f + 0.5f);
121+
b = (int) (t * 255.0f + 0.5f);
122+
break;
123+
case 3:
124+
r = (int) (p * 255.0f + 0.5f);
125+
g = (int) (q * 255.0f + 0.5f);
126+
b = (int) (brightness * 255.0f + 0.5f);
127+
break;
128+
case 4:
129+
r = (int) (t * 255.0f + 0.5f);
130+
g = (int) (p * 255.0f + 0.5f);
131+
b = (int) (brightness * 255.0f + 0.5f);
132+
break;
133+
case 5:
134+
r = (int) (brightness * 255.0f + 0.5f);
135+
g = (int) (p * 255.0f + 0.5f);
136+
b = (int) (q * 255.0f + 0.5f);
137+
break;
138+
}
139+
}
140+
return 0xff000000 | (r << 16) | (g << 8) | (b << 0);
141+
}
142+
143+
/**
144+
* Extracts the hue, saturation, and brightness (HSB) values from a packed RGB integer.
145+
* <p>
146+
* This is a convenience wrapper around {@link #retrieveRGBToHSB(int, int, int, float[])}.
147+
* The input must be in the same packed format used by {@link java.awt.Color#getRGB()} or
148+
* by constructing a {@link java.awt.Color} with an integer.
149+
* <p>
150+
* If {@code hsbvals} is {@code null}, a new float[3] is created. Otherwise, the existing
151+
* array is reused and written to.
152+
*
153+
* @param rgb a packed 0xAARRGGBB or 0xRRGGBB integer
154+
* @param hsbvals optional array to store the result; may be {@code null}
155+
* @return an array of three floats containing hue, saturation, and brightness (in that order)
156+
*/
157+
public static float[] getHSB(final int rgb, final float[] hsbvals) {
158+
int r = (rgb >> 16) & 0xFF;
159+
int g = (rgb >> 8) & 0xFF;
160+
int b = rgb & 0xFF;
161+
return retrieveRGBToHSB(r, g, b, hsbvals);
162+
}
163+
164+
/**
165+
* Converts RGB components into hue, saturation, and brightness values (HSB).
166+
* <p>
167+
* Equivalent to {@link java.awt.Color#RGBtoHSB(int, int, int, float[])}, but implemented
168+
* locally to avoid allocations and improve performance when called frequently.
169+
* <p>
170+
* If {@code hsbvals} is {@code null}, a new array is allocated. If not null, values are
171+
* written directly into the provided array.
172+
*
173+
* @param r red component (0–255)
174+
* @param g green component (0–255)
175+
* @param b blue component (0–255)
176+
* @param hsbvals optional array to store the result; may be {@code null}
177+
* @return an array containing hue, saturation, and brightness (in that order).
178+
*
179+
* @see java.awt.Color#getRGB()
180+
* @see java.awt.Color#Color(int)
181+
* @see java.awt.Color#RGBtoHSB(int, int, int, float[])
182+
*/
183+
public static float[] retrieveRGBToHSB(int r, int g, int b, float[] hsbvals) {
184+
float hue, saturation, brightness;
185+
if (hsbvals == null) {
186+
hsbvals = new float[3];
187+
}
188+
int cmax = (r > g) ? r : g;
189+
if (b > cmax) cmax = b;
190+
int cmin = (r < g) ? r : g;
191+
if (b < cmin) cmin = b;
192+
193+
brightness = ((float) cmax) / 255.0f;
194+
if (cmax != 0)
195+
saturation = ((float) (cmax - cmin)) / ((float) cmax);
196+
else
197+
saturation = 0;
198+
if (saturation == 0)
199+
hue = 0;
200+
else {
201+
float redc = ((float) (cmax - r)) / ((float) (cmax - cmin));
202+
float greenc = ((float) (cmax - g)) / ((float) (cmax - cmin));
203+
float bluec = ((float) (cmax - b)) / ((float) (cmax - cmin));
204+
if (r == cmax)
205+
hue = bluec - greenc;
206+
else if (g == cmax)
207+
hue = 2.0f + redc - bluec;
208+
else
209+
hue = 4.0f + greenc - redc;
210+
hue = hue / 6.0f;
211+
if (hue < 0)
212+
hue = hue + 1.0f;
213+
}
214+
hsbvals[0] = hue;
215+
hsbvals[1] = saturation;
216+
hsbvals[2] = brightness;
217+
return hsbvals;
218+
}
219+
220+
private static void neighbouringPixels(final BufferedImage scaled, final float[] neighB, int idx, final int x, final int y, final float[] hsb2) {
221+
for (int dy = -1; dy <= 1; dy++) {
222+
for (int dx = -1; dx <= 1; dx++) {
223+
if (dx == 0 && dy == 0) continue;
224+
neighB[idx++] = getHSB(scaled.getRGB(x + dx, y + dy), hsb2)[2];
225+
}
226+
}
227+
}
228+
229+
230+
/**
231+
* Returns the median brightness value from eight neighboring pixels.
232+
* <p>
233+
* Only the five smallest values are partially sorted, which is sufficient
234+
* to find the true median for a fixed-size 8-element list. This is faster
235+
* than fully sorting the array.
236+
*
237+
* @param v an array of 8 brightness values (0–1 range)
238+
* @return the median brightness value
239+
*/
240+
private static float getMedianBrightness(float[] v) {
241+
for (int i = 0; i < 5; i++) {
242+
int min = i;
243+
for (int j = i + 1; j < 8; j++) {
244+
if (v[j] < v[min]) min = j;
245+
}
246+
float t = v[i];
247+
v[i] = v[min];
248+
v[min] = t;
249+
}
250+
return v[4];
251+
}
252+
253+
/**
254+
* Clamps a value into a min/max range. If the number is less than {@code min},
255+
* {@code min} is returned. If greater than {@code max}, {@code max} is returned.
256+
* Otherwise, the value itself is returned.
257+
*
258+
* @param v the value to clamp
259+
* @param min the lower bound
260+
* @param max the upper bound
261+
* @return a value within the inclusive range {@code [min, max]}
262+
*/
263+
private static float clamp(float v, float min, float max) {
264+
return v < min ? min : (v > max ? max : v);
265+
}
266+
267+
private static void addPixels(final MapRendererData mapRendererData, final int height, final int width, final BufferedImage filtered) {
268+
for (int y = 0; y < height; y++) {
269+
for (int x = 0; x < width; x++) {
270+
mapRendererData.addPixel(x, y, new Color(filtered.getRGB(x, y)));
271+
}
272+
}
273+
}
274+
275+
276+
}

0 commit comments

Comments
 (0)