Edit: Rewrote my question after trying a few things and made it more specific.
Hi, so I'm creating a mobile RTS game with procedurally generated maps. I've worked out how to create a terrain with a basic perlin noise on it, and tried to integrate https://gamedev.stackexchange.com/questions/54276/a-simple-method-to-create-island-map-mask method to creating an island procedurally. This is the result so far:
The image below from http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/ shows the kind of terrain I'm after. The tutorial there is great but would be too intensive, thus the post.
I want the Random Shaped island with Perlin noise generated land mass, surrounded by water.
edit: Basic Perlin terrain gen working now =)
Here is my code. A script attached to a null with a button to activate Begin():
using UnityEngine;
using System.Collections;
using System.Runtime.InteropServices;
public class Gen_Perlin : MonoBehaviour {
public float Tiling = 0.5f;
private bool active = false;
public int mapHeight = 10;
public void Begin()
{
if (active == false) {
TerrainData terrainData = new TerrainData ();
const int size = 513;
terrainData.heightmapResolution = size;
terrainData.size = new Vector3 (2000, mapHeight, 2000);
terrainData.heightmapResolution = 513;
terrainData.baseMapResolution = 1024;
terrainData.SetDetailResolution (1024, 1024);
Terrain.CreateTerrainGameObject (terrainData);
GameObject obj = GameObject.Find ("Terrain");
obj.transform.parent = this.transform;
if (obj.GetComponent<Terrain> ()) {
GenerateHeights (obj.GetComponent<Terrain> (), Tiling);
}
} else {
GameObject obj = GameObject.Find ("Terrain");
if (obj.GetComponent<Terrain> ()) {
GenerateHeights (obj.GetComponent<Terrain> (), Tiling);
}
}
}
public void GenerateHeights(Terrain terrain, float tileSize)
{
Debug.Log ("Start_Height_Gen");
float[,] heights = new float[terrain.terrainData.heightmapWidth, terrain.terrainData.heightmapHeight];
for (int i = 0; i < terrain.terrainData.heightmapWidth; i++)
{
for (int k = 0; k < terrain.terrainData.heightmapHeight; k++)
{
heights[i, k] = 0.25f + Mathf.PerlinNoise(((float)i / (float)terrain.terrainData.heightmapWidth) * tileSize, ((float)k / (float)terrain.terrainData.heightmapHeight) * tileSize);
heights[i, k] *= makeMask( terrain.terrainData.heightmapWidth, terrain.terrainData.heightmapHeight, i, k, heights[i, k] );
}
}
terrain.terrainData.SetHeights(0, 0, heights);
}
public static float makeMask( int width, int height, int posX, int posY, float oldValue ) {
int minVal = ( ( ( height + width ) / 2 ) / 100 * 2 );
int maxVal = ( ( ( height + width ) / 2 ) / 100 * 10 );
if( getDistanceToEdge( posX, posY, width, height ) <= minVal ) {
return 0;
} else if( getDistanceToEdge( posX, posY, width, height ) >= maxVal ) {
return oldValue;
} else {
float factor = getFactor( getDistanceToEdge( posX, posY, width, height ), minVal, maxVal );
return oldValue * factor;
}
}
private static float getFactor( int val, int min, int max ) {
int full = max - min;
int part = val - min;
float factor = (float)part / (float)full;
return factor;
}
public static int getDistanceToEdge( int x, int y, int width, int height ) {
int[] distances = new int[]{ y, x, ( width - x ), ( height - y ) };
int min = distances[ 0 ];
foreach( var val in distances ) {
if( val < min ) {
min = val;
}
}
return min;
}
}
Yeah. The article in question is using a waaay complex method.
The best way of doing this is to take a function that represents the shape of your basic island, with height values between 0 and 1. For the type of island in the picture, you'd basically want something which smoothly rises from the edges, and smoothly dips back to zero where you want lakes.
Now you either add that surface to your basic fractal surface (if you want to preserve spikiness at low elevations) or you multiply it (if you want lower elevations to be smooth). Then you define a height, below which is water.
Here is my very quick go at doing this, rendered with Terragen:
I used a function that rises in a ring from the edge of the map to halfway to the middle, then drops again, to match a similar shape to the one from the article. In practice, you might only use this to get the shape of the island, and then carve the bit of terrain that matches the contour, and bury everything else.
I used my own fractal landscape generator as described here: https://fractal-landscapes.co.uk for the basic fractal.
Here is the C# code that modifies the landscape:
public void MakeRingIsland()
{
this.Normalize(32768);
var ld2 = (double) linearDimension / 2;
var ld4 = 4 / (double) linearDimension;
for (var y = 0u; y < linearDimension; y++)
{
var yMul = y * linearDimension;
for (var x = 0u; x < linearDimension; x++)
{
var yCoord = (y - ld2) * ld4;
var xCoord = (x - ld2) * ld4;
var dist = Math.Sqrt(xCoord * xCoord + yCoord * yCoord);
var htMul = dist > 2 ? 0 :
(dist < 1 ?
dist + dist - dist * dist :
1 - (dist - 1) * (dist - 1));
var height = samples[x + yMul];
samples[x + yMul] = (int) (height + htMul * 32768);
}
}
}
the image you are showing comes from article describing how to generate it
Related
I am attempting to create randomised terrain meshes as I have done so in the screenshot below:
However, the issue I am facing is when attempting to reduces the number of triangles and vertices (Level of Detail).
I understand that to do this I can just skip over vertices.
for example:
The above mesh is full detail in that the vertices are generated like so:
0->1->2->3->4->5->6->7->8->9->...
and to generate a lower level of detail i can skip vertices as long as the skipping of vertices does not exceed the length of vertices so i could do the following generation to lower detail:
0->2->4->6->8->10->12->14->16->...
or:
0->4->8->12->16->20->24->28->32->...
Using a 2D array and a nested loop makes this trivial as each iteration on 2D coordinates x/y can be incremented by the increment: 1, 2, 4, 8, however, i am dealing with 2D arrays in 1D format.
I have the following code which executes and almost correctly generates the above mesh in the screenshot above.
Unfortunately it does seem to be missing one line of of vertices on the top left (3d z axis) as seen below:
One caveat to the Execute(int, int) method below is that any access to the NativeArray which is not labeled [ReadOnly] will throw an exception if the array is accessing indexes outside of it's batch size.
public struct int6
{
public int a, b, c, d, e, f;
public int6(int a, int b, int c, int d, int e, int f) { this.a = a; this.b = b; this.c = c; this.d = d; this.e = e; this.f = f; }
}
public class MeshGeneratorJob2
{
[ReadOnly] public static int width = 241;
[ReadOnly] public static int height = 241;
[ReadOnly] public static float topLeftX = (width - 1) / -2f;
[ReadOnly] public static float topLeftZ = (height - 1) / 2f;
[ReadOnly] public static NativeArray<float> heightMap = new NativeArray<float>(width * height, Allocator.TempJob);
public static NativeArray<float> heightCurveSamples;
public static NativeArray<float3> vertices = new NativeArray<float3>(width * height, Allocator.TempJob);
public static NativeArray<int6> triangles = new NativeArray<int6>((width - 1) * (height - 1), Allocator.TempJob);
public static NativeArray<float2> uvs = new NativeArray<float2>(width * height, Allocator.TempJob);
public void Execute()
{
for (int i = 0; i < vertices.Length; i += 5)
{
Execute(i, 5);
}
}
private void Execute(int startIndex, int count)
{
for (int vertexIndex = startIndex; vertexIndex < startIndex + count; vertexIndex++)
{
int x = vertexIndex % width;
int y = vertexIndex / width;
vertices[vertexIndex] = new float3(topLeftX + x, heightMap[vertexIndex] * 16.67f, topLeftZ - y);
uvs[vertexIndex] = new float2(x / (float)width, y / (float)height);
if (vertexIndex < triangles.Length && x < width - 1 && y < height - 1)
{
triangles[vertexIndex] = new int6(vertexIndex, vertexIndex + width + 1, vertexIndex + width,
vertexIndex + width + 1, vertexIndex, vertexIndex + 1);
}
}
}
}
I have come up with the following solution to this problem:
The first issue i solved was using a nested for loop y, x with y always starting at startIndex.
this, however, caused an issue as the vertexIndex could be higher than the length of the triangles length, so i calculated the current vertexIndex at the supplied startIndex as follows:
Here i introduced an incrementer value which increments both the x and y loops rather than y++, x++ however in this example incrementer is 1 which is essentially the same thing.
int vertexIndex = (int)(math.ceil((float)width / incrementer) * math.ceil((float)startIndex / incrementer));
however calculating the vertexIndex caused another issue which again caused out of bounds exceptions on setting the vertices.
This was due to the startIndex being incremented by count, where count was not the same as the incrementer.
To solve this I at the start of the method added the following code to round the startIndex up to the next incremental count if needed.
startIndex += startIndex % incrementer;
and altogether i then get the following code:
public struct int6
{
public int a, b, c, d, e, f;
public int6(int a, int b, int c, int d, int e, int f) { this.a = a; this.b = b; this.c = c; this.d = d; this.e = e; this.f = f; }
}
public class MeshGeneratorJob2
{
public static int width = 241;
public static int height = 241;
public static float topLeftX = (width - 1) / -2f;
public static float topLeftZ = (height - 1) / 2f;
public static int increment = 1;
public static NativeArray<float> heightMap = new NativeArray<float>(width * height, Allocator.TempJob);
public static NativeArray<float> heightCurveSamples;
public static NativeArray<float3> vertices = new NativeArray<float3>(width * height, Allocator.TempJob);
public static NativeArray<int6> triangles = new NativeArray<int6>((width - 1) * (height - 1), Allocator.TempJob);
public static NativeArray<float2> uvs = new NativeArray<float2>(width * height, Allocator.TempJob);
public void Execute()
{
for (int i = 0; i < vertices.Length; i += 5)
{
Execute(i, 5);
}
}
private void Execute(int startIndex, int count)
{
startIndex += startIndex % increment;
int vertexIndex = (int)(math.ceil((float)width / increment) * math.ceil((float)startIndex / increment));
for (int y = startIndex; y < startIndex + count && y < height; y++)
{
for (int x = 0; x < width; x += increment)
{
vertices[vertexIndex] = new float3(topLeftX + x, heightMap[vertexIndex] * 16.67f, topLeftZ - y);
uvs[vertexIndex] = new float2(x / (float)width, y / (float)height);
if (vertexIndex < triangles.Length && x < width - 1 && y < height - 1)
{
triangles[vertexIndex] = new int6(vertexIndex, vertexIndex + width + 1, vertexIndex + width,
vertexIndex + width + 1, vertexIndex, vertexIndex + 1);
}
vertexIndex++;
}
}
}
}
I'm able to point zoom on the Mandelbrot set, as long as the mouse doesn't move after zooming has begun. I've tried calculating a normalized delta (new coordinate - old coordinate)*(oldzoom), but what happens is the image appears to jump around to a new location. I've seen this issue before. I'm struggling more here because I have to somehow convert this mouse position delta back to the -2,2 coordinate space of the Mandelbrot set.
Here's my code. What's important is the GetZoomPoint method, and then the lines of code that define x0 and y0. Also, I use the Range class to scale values from one range to another. I WAS using deltaTrans (thats the thing I was talking about earlier where I normalize the mouse delta with the old scale).
using OpenTK.Graphics.OpenGL;
using SpriteSheetMaker;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Fractal.Fractal
{
public class Mandelbrot : BaseTexture
{
private static Transform GlobalTransform = SpriteSheetMaker.Global.Transform;
private static Vector3 GlobalScale = GlobalTransform.Scale;
private static Vector3 GlobalTrans = GlobalTransform.Translation;
private static Vector3 LastWindowPoint = null;
private static Vector3 ZoomFactor = Vector3.ONE * 1.2f;
private static Vector3 Displacement = Vector3.ZERO;
private static int WindowSize = 100;
public static Vector3 GetZoomPoint()
{
var zP = OpenGLHelpers.LastZoomPoint.Clone();
if (LastWindowPoint == null)
{
LastWindowPoint = zP.Clone();
}
var delta = zP - LastWindowPoint;
var oldZoom = GlobalScale / ZoomFactor;
var deltaTrans = delta.XY * oldZoom.XY;
var factor = ZoomFactor.Clone();
Range xR = new Range(0, WindowSize);
Range yR = new Range(0, WindowSize);
Range complexRange = new Range(-2, 2);
// Calculate displacement of zooming position.
var dx = (zP.X - Displacement.X) * (factor.X - 1f);
var dy = (zP.Y - Displacement.Y) * (factor.Y - 1f);
// Compensate for displacement.
Displacement.X -= dx;
Displacement.Y -= dy;
zP -= Displacement;
var x = complexRange.ScaleValue(zP.X, xR);
var y = complexRange.ScaleValue(zP.Y, yR);
var rtn = new Vector3(x, y);
LastWindowPoint = zP.Clone();
return rtn;
}
public static Mandelbrot Generate()
{
var size = new Size(WindowSize, WindowSize);
var radius = new Size(size.Width / 2, size.Height / 2);
Bitmap bmp = new Bitmap(size.Width, size.Height);
LockBitmap.LockBitmapUnsafe lbm = new LockBitmap.LockBitmapUnsafe(bmp);
lbm.LockBits();
var pt = Mandelbrot.GetZoomPoint();
Parallel.For(0, size.Width, i =>
{
// float x0 = complexRangeX.ScaleValue(i, xRange);
float x0 = ((i - radius.Width) / GlobalScale.X) + pt.X;
Parallel.For(0, size.Height, j =>
{
// float y0 = complexRangeY.ScaleValue(j, yRange);
float y0 = ((j - radius.Height) / GlobalScale.Y) + pt.Y;
float value = 0f;
float x = 0.0f;
float y = 0.0f;
int iteration = 0;
int max_iteration = 100;
while (x * x + y * y <= 4.0 && iteration < max_iteration)
{
float xtemp = x * x - y * y + x0;
y = 2.0f * x * y + y0;
x = xtemp;
iteration += 1;
if (iteration == max_iteration)
{
value = 255;
break;
}
else
{
value = iteration * 50f % 255f;
}
}
int v = (int)value;
lbm.SetPixel(i, j, new ColorLibrary.HSL(v / 255f, 1.0, 0.5).ToDotNetColor());
});
});
lbm.UnlockBits();
var tex = new BaseTextureImage(bmp);
var rtn = new Mandelbrot(tex);
return rtn;
}
public override void Draw()
{
base._draw();
}
private Mandelbrot(BaseTextureImage graphic)
{
var topLeft = new Vector3(0, 1);
var bottomLeft = new Vector3(0, 0);
var bottomRight = new Vector3(1, 0);
var topRight = new Vector3(1, 1);
this.Vertices = new List<Vector3>()
{
topLeft,bottomLeft,bottomRight,topRight
};
this.Size.X = WindowSize;
this.Size.Y = WindowSize;
this.Texture2D = graphic;
}
}
}
I refactored my code, and also figured out a solution to this problem. 2 big wins in one. Ok, so I found a solution on CodeProject written in C# which I was readily able to adapt to my project. I'm not sure why I didn't realize this when I posted the question, but what I needed to solve this issue was to create a 'window' of zoom and not think in terms of a 'point zoom'. Yes, even if I am trying to zoom directly into a point, that point is just the center of some sort of a window.
Here is the method I have, which expects start and end mousedown coordinates (screen space), and converts the mandelbrot set window size accordingly.
public void ApplyZoom(double x0, double y0, double x1, double y1)
{
if (x1 == x0 && y0 == y1)
{
//This was just a click, no movement occurred
return;
}
/*
* XMin, YMin and XMax, YMax are the current extent of the set
* mx0,my0 and mx1,my1 are the part we selected
* do the math to draw the selected rectangle
* */
double scaleX, scaleY;
scaleX = (XMax - XMin) / (float)BitmapSize;
scaleY = (YMax - YMin) / (float)BitmapSize;
XMax = (float)x1 * scaleX + XMin;
YMax = (float)y1 * scaleY + YMin;
XMin = (float)x0 * scaleX + XMin;
YMin = (float)y0 * scaleY + YMin;
this.Refresh(); // force mandelbrot to redraw
}
Basically, whats happening is we calculate the ratio between the mandelbrot window size versus the screen size we are drawing to. Then, using that scale, we basically convert our mousedown coordinates to mandelbrot set coordinates (x1*scaleX, etc) and manipulate the current Min and Max coordinates with them, using the Min values as the pivot point.
Here's the link to the CodeProject I used as a reference: CodeProject link
So, I was trying to come up with my own simple implementation of Perlin/Fractal noise(I'm just adding the noise to itself resampled multiple times).
When it comes to the resampling part of the algorithm, to achieve lower frequency noise I'm using bilinear interpolation on a smaller part of the array to effectively "stretch out" that smaller part (i.e: look at a 16x16 part of a 256x256 sheet of noise, but treat it as if it was a 256x256 array of pixels)
The site I use (well, one of them) for reference is http: //lodev.org/cgtutor/randomnoise.html
From the site the first two pictures is the result I'm looking for but instead i get something like this (I used a 64x64 array):
Original
Resampled using x8 Zoom
All of the code I'm using is in Unity (I know there is a built-in Perlin-generator for it, but I wanted my own):
using UnityEngine;
using System.Collections;
public class NoiseGenerator : MonoBehaviour {
//Dimensions of the array
public int width;
public int height;
//RNG Stuff for reproducability
public bool useTimeAsSeed;
public string seed;
//The array the nopise gets stored in
int[,] noise;
//These are used in the editor to control if we zoon in or not, original image gets resized to 1/ratio
public bool zoom;
public int ratio=1;
//Function used to fill up the array with white noise (random numbers with a uniform distribution between 0-255)
void InitNoise() {
noise = new int[width, height];
if (useTimeAsSeed) {
seed = Time.time.ToString();
}
System.Random r = new System.Random(seed.GetHashCode());
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
noise[x, y] = r.Next(0,256);
}
}
}
//Function to smooth the noise when it's being resampled
/*
AB
CD
|when resampled
v
A--R---B
| X |
| |
| |
C--J---D
Uses bilinear Interpolation to get the wighted average of A B C and D, which are all neighbouring pixels in the original picture
Returns an int value between 0-255
*/
int SmoothNoise(float x, float y) {
float fractX = x - (int)x; //Fractional part of the X-coord, used as the weight for Lerping in the x-direction
float fractY = y - (int)y; //Fractional part of the Y-coord, used as the weight for Lerping in the y-direction
//Calculating the neigbouring pixels
//For edge values the respective "missing pixel" is filled in by the other end of the noise sheet
int x1 = ((int)x + width) % width;
int x2 = (x1 + width - 1) % width;
int y1 = ((int)y + height) % height;
int y2 = (y1 + height - 1) % height;
//Calculating R and J by Lerping the respective pixels
float R = Mathf.Lerp(noise[x1, y1], noise[x2, y1], fractX);
float J = Mathf.Lerp(noise[x2, y1], noise[x2, y2], fractX);
//Finally Lerp R and J to get the value for X
return (int)(Mathf.Lerp(R,J,fractY));
}
void Start() {
InitNoise();
}
//Easy way to display the array as a temporary solution until actual terrain display is implemented
void OnDrawGizmos() {
if (noise != null) {
for (int x = 0; x < width; x++){
for (int y = 0; y < height; y++){
float component = (float)noise[x, y] / 255;
if (zoom) {
//Using the result of SmoothNoise normalised to be between 0-1 we construct a color component to be used when displaying
component = (float)SmoothNoise((float)x/ratio, (float)y/ratio) / 255;
}
Gizmos.color = new Color(component,component,component);
Vector3 pos = new Vector3(-width/2 + x + .5f, 0, -height/2 + y + .5f);
Gizmos.DrawCube(pos, Vector3.one);
}
}
}
}
}
I was searching for a stretch like warp algorithm to apply in my Windows 8 store application.
I found this android (java) code below.
Most things I can port to C# but some android sdk specific things are blurry for me to port to .NET code.
For example the call too : canvas.drawBitmapMesh , is there some .NET counterpart for this?
The Matrix classes in .NET are also a bit different I think but I can figure that out I think.
Any tips on helping converting/porting the code below to .NET are very welcome.
public class BitmapMesh extends GraphicsActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new SampleView(this));
}
private static class SampleView extends View {
private static final int WIDTH = 20;
private static final int HEIGHT = 20;
private static final int COUNT = (WIDTH + 1) * (HEIGHT + 1);
private final Bitmap mBitmap;
private final float[] mVerts = new float[COUNT*2];
private final float[] mOrig = new float[COUNT*2];
private final Matrix mMatrix = new Matrix();
private final Matrix mInverse = new Matrix();
private static void setXY(float[] array, int index, float x, float y) {
array[index*2 + 0] = x;
array[index*2 + 1] = y;
}
public SampleView(Context context) {
super(context);
setFocusable(true);
mBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.beach);
float w = mBitmap.getWidth();
float h = mBitmap.getHeight();
// construct our mesh
int index = 0;
for (int y = 0; y <= HEIGHT; y++) {
float fy = h * y / HEIGHT;
for (int x = 0; x <= WIDTH; x++) {
float fx = w * x / WIDTH;
setXY(mVerts, index, fx, fy);
setXY(mOrig, index, fx, fy);
index += 1;
}
}
mMatrix.setTranslate(10, 10);
mMatrix.invert(mInverse);
}
#Override protected void onDraw(Canvas canvas) {
canvas.drawColor(0xFFCCCCCC);
canvas.concat(mMatrix);
canvas.drawBitmapMesh(mBitmap, WIDTH, HEIGHT, mVerts, 0,
null, 0, null);
}
private void warp(float cx, float cy) {
final float K = 10000;
float[] src = mOrig;
float[] dst = mVerts;
for (int i = 0; i < COUNT*2; i += 2) {
float x = src[i+0];
float y = src[i+1];
float dx = cx - x;
float dy = cy - y;
float dd = dx*dx + dy*dy;
float d = FloatMath.sqrt(dd);
float pull = K / (dd + 0.000001f);
pull /= (d + 0.000001f);
// android.util.Log.d("skia", "index " + i + " dist=" + d + " pull=" + pull);
if (pull >= 1) {
dst[i+0] = cx;
dst[i+1] = cy;
} else {
dst[i+0] = x + dx * pull;
dst[i+1] = y + dy * pull;
}
}
}
private int mLastWarpX = -9999; // don't match a touch coordinate
private int mLastWarpY;
#Override public boolean onTouchEvent(MotionEvent event) {
float[] pt = { event.getX(), event.getY() };
mInverse.mapPoints(pt);
int x = (int)pt[0];
int y = (int)pt[1];
if (mLastWarpX != x || mLastWarpY != y) {
mLastWarpX = x;
mLastWarpY = y;
warp(pt[0], pt[1]);
invalidate();
}
return true;
}
}
}
There is no direct equivalent in Windows Store apps to be able to draw a texture mapped mesh (and represent image warping and distortion that way). You will have to use Direct3D to draw the texture mapped mesh onto your page.
If you're reluctant to mix C++ and C# in your app, you have two choices.
SharpDX - these are .NET bindings to DirectX.
MonoGame - this is an XNA-compliant, cross-platform graphics interface that uses DirectX on WinRT.
Hope this helps!
I am working in OMR project and we are using C#. When we come to scan the answer sheets, the images are skewed. How can we deskew them?
VB.Net Code for this is available here, however since you asked for C# here is a C# translation of their Deskew class (note: Binarize (strictly not necessary, but works much better) and Rotate are exercises left to the user).
public class Deskew
{
// Representation of a line in the image.
private class HougLine
{
// Count of points in the line.
public int Count;
// Index in Matrix.
public int Index;
// The line is represented as all x,y that solve y*cos(alpha)-x*sin(alpha)=d
public double Alpha;
}
// The Bitmap
Bitmap _internalBmp;
// The range of angles to search for lines
const double ALPHA_START = -20;
const double ALPHA_STEP = 0.2;
const int STEPS = 40 * 5;
const double STEP = 1;
// Precalculation of sin and cos.
double[] _sinA;
double[] _cosA;
// Range of d
double _min;
int _count;
// Count of points that fit in a line.
int[] _hMatrix;
public Bitmap DeskewImage(Bitmap image, int type, int binarizeThreshold)
{
Size oldSize = image.Size;
_internalBmp = BitmapFunctions.Resize(image, new Size(1000, 1000), true, image.PixelFormat);
Binarize(_internalBmp, binarizeThreshold);
return Rotate(image, GetSkewAngle());
}
// Calculate the skew angle of the image cBmp.
private double GetSkewAngle()
{
// Hough Transformation
Calc();
// Top 20 of the detected lines in the image.
HougLine[] hl = GetTop(20);
// Average angle of the lines
double sum = 0;
int count = 0;
for (int i = 0; i <= 19; i++)
{
sum += hl[i].Alpha;
count += 1;
}
return sum / count;
}
// Calculate the Count lines in the image with most points.
private HougLine[] GetTop(int count)
{
HougLine[] hl = new HougLine[count];
for (int i = 0; i <= count - 1; i++)
{
hl[i] = new HougLine();
}
for (int i = 0; i <= _hMatrix.Length - 1; i++)
{
if (_hMatrix[i] > hl[count - 1].Count)
{
hl[count - 1].Count = _hMatrix[i];
hl[count - 1].Index = i;
int j = count - 1;
while (j > 0 && hl[j].Count > hl[j - 1].Count)
{
HougLine tmp = hl[j];
hl[j] = hl[j - 1];
hl[j - 1] = tmp;
j -= 1;
}
}
}
for (int i = 0; i <= count - 1; i++)
{
int dIndex = hl[i].Index / STEPS;
int alphaIndex = hl[i].Index - dIndex * STEPS;
hl[i].Alpha = GetAlpha(alphaIndex);
//hl[i].D = dIndex + _min;
}
return hl;
}
// Hough Transforamtion:
private void Calc()
{
int hMin = _internalBmp.Height / 4;
int hMax = _internalBmp.Height * 3 / 4;
Init();
for (int y = hMin; y <= hMax; y++)
{
for (int x = 1; x <= _internalBmp.Width - 2; x++)
{
// Only lower edges are considered.
if (IsBlack(x, y))
{
if (!IsBlack(x, y + 1))
{
Calc(x, y);
}
}
}
}
}
// Calculate all lines through the point (x,y).
private void Calc(int x, int y)
{
int alpha;
for (alpha = 0; alpha <= STEPS - 1; alpha++)
{
double d = y * _cosA[alpha] - x * _sinA[alpha];
int calculatedIndex = (int)CalcDIndex(d);
int index = calculatedIndex * STEPS + alpha;
try
{
_hMatrix[index] += 1;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.ToString());
}
}
}
private double CalcDIndex(double d)
{
return Convert.ToInt32(d - _min);
}
private bool IsBlack(int x, int y)
{
Color c = _internalBmp.GetPixel(x, y);
double luminance = (c.R * 0.299) + (c.G * 0.587) + (c.B * 0.114);
return luminance < 140;
}
private void Init()
{
// Precalculation of sin and cos.
_cosA = new double[STEPS];
_sinA = new double[STEPS];
for (int i = 0; i < STEPS; i++)
{
double angle = GetAlpha(i) * Math.PI / 180.0;
_sinA[i] = Math.Sin(angle);
_cosA[i] = Math.Cos(angle);
}
// Range of d:
_min = -_internalBmp.Width;
_count = (int)(2 * (_internalBmp.Width + _internalBmp.Height) / STEP);
_hMatrix = new int[_count * STEPS];
}
private static double GetAlpha(int index)
{
return ALPHA_START + index * ALPHA_STEP;
}
}
Scanned document are always skewed for an average [-10;+10] degrees angle.
It's easy to deskew them using the Hough transform, like Lou Franco said. This transform detects lines on your image for several angles. You just have to select the corresponding one to your document horizontal lines, then rotate it.
try to isolate the pixel corresponding to your document horizontal lines (for instance, black pixels that have a white pixel at their bottom).
Run Hough transform. Do not forget to use 'unsafe' mode in C# to fasten the process of your whole image by using a pointor.
Rotate your document in the opposite angle found.
Works like a charm on binary documents (easily extendable to grey level ones)
Disclaimer: I work at Atalasoft, DotImage Document Imaging can do this with a couple of lines of code.
Deskew is a term of art that describes what you are trying to do. As Ben Voigt said, it's technically rotation, not skew -- however, you will find algorithms under automatic deskew if you search.
The normal way to do this is to do a hough transform to look for the prevalent lines in the image. With normal documents, many of them will be orthogonal to the sides of the paper.
Are you sure it's "skew" rather than "rotation" (rotation preserves angles, skew doesn't).
Use some sort of registration mark (in at least two places) which you can recognize even when rotated.
Find the coordinates of these marks and calculate the rotation angle.
Apply a rotation transformation matrix to the image.