I'm currently working on a project involving integrating OpenCVSharp into Unity, to allow eye tracking within a game environment. I've managed to get OpenCVSharp integrated into the Unity editor and currently have eye-detection (not tracking) working within a game. It can find your eyes within a webcam image, then display where its currently detected them on a texture, which I display within the scene.
However its causing a HUGE fps drop, mainly because every frame its converting a webcam texture into an IPLimage so that OpenCV can handle it. It then has to convert it back to a 2Dtexture to be displayed within the scene, after its done all the eye detection. So understandably its too much for the CPU to handle. (As far as I can tell its only using 1 core on my CPU).
Is there a way to do all the eye detection without converting the texture to an IPLimage? Or any other way to fix the fps drop. Some things that I've tried include:
Limiting the frames that it updates on. However this just causes it
to run smoothly, then stutter horribly on the frame that it has to
update.
Looking at threading, but as far as I'm aware Unity doesn't allow it.
As far as I can tell its only using 1 core on my CPU which seems a bit silly. If there was a way to change this it could fix the issue?
Tried different resolutions on the camera, however the resolution that the game can actually run smoothly at, is too small for the eye's to actually be detected, let alone tracked.
I've included the code below, of if you would prefer to look at it in a code editor here is a link to the C# File. Any suggestions or help would be greatly appreciated!
For reference I used code from here (eye detection using opencvsharp).
using UnityEngine;
using System.Collections;
using System;
using System.IO;
using OpenCvSharp;
//using System.Xml;
//using OpenCvSharp.Extensions;
//using System.Windows.Media;
//using System.Windows.Media.Imaging;
public class CaptureScript : MonoBehaviour
{
public GameObject planeObj;
public WebCamTexture webcamTexture; //Texture retrieved from the webcam
public Texture2D texImage; //Texture to apply to plane
public string deviceName;
private int devId = 1;
private int imWidth = 640; //camera width
private int imHeight = 360; //camera height
private string errorMsg = "No errors found!";
static IplImage matrix; //Ipl image of the converted webcam texture
CvColor[] colors = new CvColor[]
{
new CvColor(0,0,255),
new CvColor(0,128,255),
new CvColor(0,255,255),
new CvColor(0,255,0),
new CvColor(255,128,0),
new CvColor(255,255,0),
new CvColor(255,0,0),
new CvColor(255,0,255),
};
const double Scale = 1.25;
const double ScaleFactor = 2.5;
const int MinNeighbors = 2;
// Use this for initialization
void Start ()
{
//Webcam initialisation
WebCamDevice[] devices = WebCamTexture.devices;
Debug.Log ("num:" + devices.Length);
for (int i=0; i<devices.Length; i++) {
print (devices [i].name);
if (devices [i].name.CompareTo (deviceName) == 1) {
devId = i;
}
}
if (devId >= 0) {
planeObj = GameObject.Find ("Plane");
texImage = new Texture2D (imWidth, imHeight, TextureFormat.RGB24, false);
webcamTexture = new WebCamTexture (devices [devId].name, imWidth, imHeight, 30);
webcamTexture.Play ();
matrix = new IplImage (imWidth, imHeight, BitDepth.U8, 3);
}
}
void Update ()
{
if (devId >= 0)
{
//Convert webcam texture to iplimage
Texture2DtoIplImage();
/*DO IMAGE MANIPULATION HERE*/
//do eye detection on iplimage
EyeDetection();
/*END IMAGE MANIPULATION*/
if (webcamTexture.didUpdateThisFrame)
{
//convert iplimage to texture
IplImageToTexture2D();
}
}
else
{
Debug.Log ("Can't find camera!");
}
}
void EyeDetection()
{
using(IplImage smallImg = new IplImage(new CvSize(Cv.Round (imWidth/Scale), Cv.Round(imHeight/Scale)),BitDepth.U8, 1))
{
using(IplImage gray = new IplImage(matrix.Size, BitDepth.U8, 1))
{
Cv.CvtColor (matrix, gray, ColorConversion.BgrToGray);
Cv.Resize(gray, smallImg, Interpolation.Linear);
Cv.EqualizeHist(smallImg, smallImg);
}
using(CvHaarClassifierCascade cascade = CvHaarClassifierCascade.FromFile (#"C:\Users\User\Documents\opencv\sources\data\haarcascades\haarcascade_eye.xml"))
using(CvMemStorage storage = new CvMemStorage())
{
storage.Clear ();
CvSeq<CvAvgComp> eyes = Cv.HaarDetectObjects(smallImg, cascade, storage, ScaleFactor, MinNeighbors, 0, new CvSize(30, 30));
for(int i = 0; i < eyes.Total; i++)
{
CvRect r = eyes[i].Value.Rect;
CvPoint center = new CvPoint{ X = Cv.Round ((r.X + r.Width * 0.5) * Scale), Y = Cv.Round((r.Y + r.Height * 0.5) * Scale) };
int radius = Cv.Round((r.Width + r.Height) * 0.25 * Scale);
matrix.Circle (center, radius, colors[i % 8], 3, LineType.AntiAlias, 0);
}
}
}
}
void OnGUI ()
{
GUI.Label (new Rect (200, 200, 100, 90), errorMsg);
}
void IplImageToTexture2D ()
{
int jBackwards = imHeight;
for (int i = 0; i < imHeight; i++) {
for (int j = 0; j < imWidth; j++) {
float b = (float)matrix [i, j].Val0;
float g = (float)matrix [i, j].Val1;
float r = (float)matrix [i, j].Val2;
Color color = new Color (r / 255.0f, g / 255.0f, b / 255.0f);
jBackwards = imHeight - i - 1; // notice it is jBackward and i
texImage.SetPixel (j, jBackwards, color);
}
}
texImage.Apply ();
planeObj.renderer.material.mainTexture = texImage;
}
void Texture2DtoIplImage ()
{
int jBackwards = imHeight;
for (int v=0; v<imHeight; ++v) {
for (int u=0; u<imWidth; ++u) {
CvScalar col = new CvScalar ();
col.Val0 = (double)webcamTexture.GetPixel (u, v).b * 255;
col.Val1 = (double)webcamTexture.GetPixel (u, v).g * 255;
col.Val2 = (double)webcamTexture.GetPixel (u, v).r * 255;
jBackwards = imHeight - v - 1;
matrix.Set2D (jBackwards, u, col);
//matrix [jBackwards, u] = col;
}
}
}
}
You can move these out of the per frame update loop :
using(CvHaarClassifierCascade cascade = CvHaarClassifierCascade.FromFile (#"C:\Users\User\Documents\opencv\sources\data\haarcascades\haarcascade_eye.xml"))
using(CvMemStorage storage = new CvMemStorage())
No reason to be building the recognizer graph each frame.
Threading is the logical way to go moving forward if you want real speed updates, unity itself is not threaded, but you can fold in other threads if your careful.
Do the texture -> ipl image on the main thread then trigger an event to fire off your thread.
The thread can do all the CV work, probably construct the tex2d and then push back to main to render.
You should also be able to gain some performance improvements if you use:
Color32[] pixels;
pixels = new Color32[webcamTexture.width * webcamTexture.height];
webcamTexture.GetPixels32(pixels);
The Unity doco suggests that this can be quite a bit faster than calling "GetPixels" (and certainly faster than calling GetPixel for each pixel), and then you don't need to scale each RGB channel against 255 manually.
Related
I am using Unity 2020.3.4f1 to create a 2D game for mobile.
I created a map builder for the game.
When I hit play, it makes a bunch of 'maps' that are saved as json files. The longer the game plays seems to result in extremely slow frame rates (200fps -> 2 fps) based on the stats menu while the game is running.
The strange thing is if I go to the "Scene" tab and left click on a sprite the fps instantly jumps back up again.
Screenshots
The problem seems related to taking screenshots within Unity.
The big bulge happens & only resets when I un-pause the game.
Questions
Why would the frame rates drop considerably over time the longer the game is running?
Why would the frame rate jump back up after selecting a sprite in the "Scene" tab?
What happens in Unity when selecting a sprite in the "Scene" tab? Is there a garbage collection method?
Script:
private void Awake()
{
myCamera = gameObject.GetComponent<Camera>();
instance = this;
width = 500;
height = 500;
}
private void OnPostRender()
{
if(takeScreenShotOnNextFrame)
{
takeScreenShotOnNextFrame = false;
RenderTexture renderTexture = myCamera.targetTexture;
Texture2D renderResult = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.ARGB32, false);
Rect rect = new Rect(0, 0, renderTexture.width, renderTexture.height);
renderResult.ReadPixels(rect, 0, 0);
float myValue = 0;
float totalPixels = renderResult.width * renderResult.height;
for (int i = 0; i < renderResult.width; i++)
{
for (int j = 0; j < renderResult.height; j++)
{
Color myColor = renderResult.GetPixel(i, j);
myValue += myColor.r;
//Debug.Log("Pixel (" + i + "," + j + "): " + myColor.r);
}
}
occlusion = ((myValue / totalPixels) * 100);
byte[] byteArray = renderResult.EncodeToPNG();
System.IO.File.WriteAllBytes(Application.dataPath + "/Resources/ScreenShots/CameraScreenshot.png", byteArray);
// Cleanup
RenderTexture.ReleaseTemporary(renderTexture);
myCamera.targetTexture = null;
renderResult = null;
}
}
private void TakeScreenshot(int screenWidth, int screenHeight)
{
width = screenWidth;
height = screenHeight;
if(myCamera.targetTexture != null)
{
RenderTexture.ReleaseTemporary(myCamera.targetTexture);
//Debug.Log("Camera target texture null: " + myCamera.targetTexture == null);
}
myCamera.targetTexture = RenderTexture.GetTemporary(width, height, 16);
takeScreenShotOnNextFrame = true;
}
public static void TakeScreenshot_Static(int screenWidth, int screenHeight)
{
instance.TakeScreenshot(screenWidth, screenHeight);
}
}
Smoothing Between Chunks
So I've been working on a game in unity and want to expand my world from a 150x150 map into a seemingly infinite procedural world. My plan is to use Perlin Noise as the base and use the different values from 0-1 to determine the terrain type. The issue I'm running into is when I draw out my chunks and offset accordingly my chunks do not line up correctly, which kind of break the illusion of an infinite world.
(seen here)
WorldChunk.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Unity.Mathematics;
[System.Serializable]
public class WorldChunk
{
public int2 Position;
public int[,] Data;
public float[,] Sample;
public WorldChunk(int chunkSize = 16){
Data = new int[chunkSize, chunkSize];
Sample = new float[chunkSize, chunkSize];
}
}
WorldGenerator.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using Unity.Mathematics;
public class WorldGenerator : MonoBehaviour
{
// Base World Data
public int ChunkSize = 75;
public string Seed = "";
[Range(1f, 40f)]
public float PerlinScale = 10f;
// Pseudo Random Number Generator
private System.Random pseudoRandom;
// Chunk Data Split into Sections (Each Chunk having Coords (x, y))
public Dictionary<string, WorldChunk> chunks = new Dictionary<string, WorldChunk>();
//============================================================
// Set Warm-Up Data
//============================================================
private void Awake() {
// Get/Create Seed
if (Seed == ""){
Seed = GenerateRandomSeed();
}
// Get Random Number Generator
pseudoRandom = new System.Random(Seed.GetHashCode());
// Using to Clear while Making Test Adjustments
chunks.Clear();
// Generate Starting Chunk
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
// Draw Test Chunks
GenerateChunk(x, y);
}
}
}
//============================================================
// Generation Code
//============================================================
// ===
// Create New Chunks
// ===
public void GenerateChunk(int x, int y){
// Set Key to use
string key = $"{x},{y}";
// Check if key exists if not Generate New Chunk
if (!chunks.ContainsKey(key)){
// Add Chunk, Set Position in chunk grid (for calling and block data later), Then Generate data
chunks.Add(key, new WorldChunk(ChunkSize));
chunks[key].Position = new int2(x, y);
GenerateChunkData(chunks[key]);
}
}
// ===
// Fill Chunks with Perlin Data
// ===
private void GenerateChunkData(WorldChunk chunk){
// Set Offsets
float xOffset = (float)chunk.Position.x * ChunkSize;
float yOffset = (float)chunk.Position.y * ChunkSize;
// Set Data to Chunk
for (int x = 0; x < ChunkSize; x++)
{
for (int y = 0; y < ChunkSize; y++)
{
// Get Perlin Map
float px = (float)(x) / ChunkSize * PerlinScale + xOffset;
float py = (float)(y) / ChunkSize * PerlinScale + yOffset;
// Set Temp Sample For Testing (This will change for Map Data (Hills and Water) later)
chunk.Sample[x,y] = Mathf.PerlinNoise(px, py);
}
}
}
// ===
// Generate Random Seed of Length
// ===
private string GenerateRandomSeed(int maxCharAmount = 10, int minCharAmount = 10){
//Set Characters To Pick from
const string glyphs= "abcdefghijklmnopqrstuvwxyz0123456789";
//Set Length from min to max
int charAmount = UnityEngine.Random.Range(minCharAmount, maxCharAmount);
// Set output Variable
string output = "";
// Do Random Addition
for(int i=0; i<charAmount; i++)
{
output += glyphs[UnityEngine.Random.Range(0, glyphs.Length)];
}
// Output New Random String
return output;
}
//============================================================
// Draw Example
//============================================================
private void OnDrawGizmos() {
// Do this because I'm lazy and don't want to draw pixels to generated Sprites
Awake();
// For Each WorldChunk in the chunk Data
foreach (WorldChunk c in chunks.Values)
{
// Check if it exists (Foreach is stupid sometimes... When live editing)
if (c != null){
// Get World Positions for Chunk (Should probably Set to a Variable in the Chunk Data)
Vector3 ChunkPosition = new Vector3(c.Position.x * ChunkSize, c.Position.y * ChunkSize);
// For Each X & For Each Y in the chunk
for (int x = 0; x < ChunkSize; x++)
{
for (int y = 0; y < ChunkSize; y++)
{
// Get Cell position
Vector3 cellPos = new Vector3((ChunkPosition.x - ChunkSize/2f) + x, (ChunkPosition.y - ChunkSize/2f) + y);
// Get Temp Sample and set to color
float samp = c.Sample[x,y];
Gizmos.color = new Color(samp, samp, samp);
// Draw Tile as Sample black or white.
Gizmos.DrawCube(cellPos, Vector3.one);
}
}
// Size for Cubes
Vector3 size = new Vector3(ChunkSize, ChunkSize, 1f);
// Set Color Opaque Green
Gizmos.color = new Color(0f, 1f, 0f, 0.25f);
// Draw Chunk Borders (Disable to show issue)
// Gizmos.DrawWireCube(ChunkPosition, size);
}
}
}
}
I would like to point out when I use:
// Get Perlin Map
float px = (float)(x + xOffset) / ChunkSize * PerlinScale;
float py = (float)(y + yOffset) / ChunkSize * PerlinScale;
instead of
// Get Perlin Map
float px = (float)(x) / ChunkSize * PerlinScale + xOffset;
float py = (float)(y) / ChunkSize * PerlinScale + yOffset;
Everything aligns up correctly but the perlin noise just repeats.
What would be the best way for me to smooth between the chunks so that everything matches up?
Is there a better way to write this maybe?
EDIT:
Thanks for the help Draykoon D! here is the updated info and links to the updated scripts on pastebin if anyone needs them!
Here is the update code for anyone who wants it:
** WorldGenerator.cs**
https://pastebin.com/3BjLy5Hk
** WorldGenerator.cs**
https://pastebin.com/v3JJte3N
Hope that helps!
The key word you are looking for is tileable.
But I have a great news for you, noise function such as perlin are periodic in nature.
So instead of calling ChunckSize * ChunkSize a noise function you should only call it once and then divide the results.
I will advice you to read this excellent tutorial:
https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/procedural-patterns-noise-part-1/creating-simple-1D-noise
Don't use Perlin noise. It has heavy bias towards the 45 and 90 degree directions. Your hills are all aligned to these, and aren't oriented along a more interesting variety of directions. You could use Unity.mathematics.noise.snoise(float2) but its repeat period is rather small, and it might not be very fast if you aren't using Unity Burst jobs. this is what I created/use/recommend, but it's certainly not the only option out there! Note that all these noises are range -1 to 1 rather than 0 to 1, so if that's important than do value=value*0.5+0.5; to rescale it.
Now that that's out of the way, to solve your issue you need to separate the idea of chunks and generation. This is a good idea in general, and I always believe in hiding backend implementation details (e.g chunks) from gameplay as much as possible (e.g. avoid visible boundaries). Each time you generate a chunk, you should find its start coordinate in the world, so that coordinates continue seamlessly with the rest. For example, if the chunks are 128x128, then the chunk starting at (0, 0) should have starting coordinate (0, 0), then the chunk starting at (0, 1) should have starting coordinate (0, 128). Only then, convert a world coordinate into a noise coordinate by multiplying by your desired frequency.
I have a class project in which we are supposed to use Unities Terrain 3D objects and create a 3x3 smoothly generated terrain. For this we have been told to create a central Terrain the has adjacent terrains in the 8 cardinal directions. I have gotten the Perlin Noise to work through this method
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TerrainNoiseGeneration : MonoBehaviour
{
private TerrainData myTerrainData;
public Vector3 worldSize;
public int resolution = 129;
private float userInput = (float)4.2;
public float offsetX;
public float offsetZ;
// Start is called before the first frame update
void Start()
{
myTerrainData = gameObject.GetComponent<TerrainCollider>().terrainData;
worldSize = new Vector3(100, 50, 100);
myTerrainData.size = worldSize;
myTerrainData.heightmapResolution = resolution;
float[,] heightArray = new float[resolution, resolution];
heightArray = PerlinNoise(userInput, offsetX, offsetZ);
myTerrainData.SetHeights(0, 0, heightArray);
}
// Update is called once per frame
void Update()
{
float[,] heightArray = new float[resolution, resolution];
heightArray = PerlinNoise(userInput, offsetX, offsetZ);
myTerrainData.SetHeights(0, 0, heightArray);
}
float[,] PerlinNoise(float userInput, float offsetX, float offsetZ)
{
float[,] heights = new float[resolution, resolution];
for (int z = 0; z < resolution; z++)
{
for (int x = 0; x < resolution; x++)
{
float nx = (x + offsetX) / resolution * userInput;
float ny = (z + offsetZ) / resolution * userInput;
heights[z, x] = Mathf.PerlinNoise(nx, ny);
}
}
return heights;
}
This code allows me to Generate a smooth terrain in the first Terrain object but when I try entering the offset values so that the edges can line-up they do not have the same values.
I would appreciate any assistance on this issue as I have tried a lot of different solutions, none of which are working
Update: I was able to solve the problem with a rather simple solution of the fact that I needed to use my resolution as the offset not the distance between the terrains
I needed to set the OffsetX and OffsetZ equal to that of their respective resolution positions instead of their unity positions.
For example my terrains are 100x100 so I was setting offset to 100 or -100 depending on its location but instead I needed to use 128 or -128 to keep it in line with the resolution
Im a C#/XNA student and I've recently been working on an isometric tile engine and so far it works fairly well. But im having problem trying to figure out on how to do collision, this is what my tile engine does at the moment:
Draws the world from an image and place a tile depending on what color is on my image. For instance color red would draw a grass tile. (Tiles are 64x32)
Camera following player, and my draw loop only draws what the camera sees.
This is how my game looks if that would be of any help:
I don't know what sort of collision would work best. Should i do collision points, or intersects or any other sort of collision. I've read somewhere that you could do Worldtoscreen/Screentoworld but im far to inexperienced and don't know how that works nor how the code would look like.
Here is my code drawing tiles etc:
class MapRow
{
public List<MapCell> Columns = new List<MapCell>();
}
class TileMap
{
public List<MapRow> Rows = new List<MapRow>();
public static Texture2D image;
Texture2D tileset;
TileInfo[,] tileMap;
Color[] pixelColor;
public TileMap(string TextureImage, string Tileset)
{
tileset = Game1.Instance.Content.Load<Texture2D>(Tileset);
image = Game1.Instance.Content.Load<Texture2D>(TextureImage);
pixelColor = new Color[image.Width * image.Height]; // pixelColor array that is holding all pixel in the image
image.GetData<Color>(pixelColor); // Save all the pixels in image to the array pixelColor
tileMap = new TileInfo[image.Height, image.Width];
int counter = 0;
for (int y = 0; y < image.Height; y++)
{
MapRow thisRow = new MapRow();
for (int x = 0; x < image.Width; x++)
{
tileMap[y, x] = new TileInfo();
if (pixelColor[counter] == new Color(0, 166, 81))
{
tileMap[y, x].cellValue = 1;//grass
}
if (pixelColor[counter] == new Color(0, 74, 128))
{
tileMap[y, x].cellValue = 2;//water
}
if (pixelColor[counter] == new Color(255, 255, 0))
{
tileMap[y, x].cellValue = 3;//Sand
}
tileMap[y, x].LoadInfoFromCellValue();//determine what tile it should draw depending on cellvalue
thisRow.Columns.Add(new MapCell(tileMap[y, x]));
counter++;
}
Rows.Add(thisRow);
}
}
public static int printx;
public static int printy;
public static int squaresAcross = Settings.screen.X / Tile.TileWidth;
public static int squaresDown = Settings.screen.Y / Tile.TileHeight;
int baseOffsetX = -32;
int baseOffsetY = -64;
public void draw(SpriteBatch spriteBatch)
{
printx = (int)Camera.Location.X / Tile.TileWidth;
printy = (int)Camera.Location.Y / Tile.TileHeight;
squaresAcross = (int)Camera.Location.X / Tile.TileWidth + Settings.screen.X / Tile.TileWidth;
squaresDown = 2*(int)Camera.Location.Y / Tile.TileHeight + Settings.screen.Y / Tile.TileHeight + 7;
for (printy = (int)Camera.Location.Y / Tile.TileHeight; printy < squaresDown; printy++)
{
int rowOffset = 0;
if ((printy) % 2 == 1)
rowOffset = Tile.OddRowXOffset;
for (printx = (int)Camera.Location.X / Tile.TileWidth; printx < squaresAcross; printx++)
{
if (tileMap[printy, printx].Collides(MouseCursor.mousePosition))
Console.WriteLine(tileMap[printy, printx].tileRect);
foreach (TileInfo tileID in Rows[printy].Columns[printx].BaseTiles)
{
spriteBatch.Draw(
tileset,
tileMap[printy, printx].tileRect = new Rectangle(
(printx * Tile.TileStepX) + rowOffset + baseOffsetX,
(printy * Tile.TileStepY) + baseOffsetY,
Tile.TileWidth, Tile.TileHeight),
Tile.GetSourceRectangle(tileID.cellValue),
Color.White,
0.0f,
Vector2.Zero,
SpriteEffects.None,
tileID.drawDepth);
}
}
}
}
}
Why don't you just draw stuff just like in normal tile based games, and then rotate the camera with a 45degree? Of course then you'd need to make your graphics a bit odd, but would be easier to handle the tiles.
But if you prefer your way, then I'd suggest using simple math to calculate the "tile to the right", "tile to the left" , "tile to the up" and "tile to the down" ones, you know, the tiles around the player(or another tile). You can simply work with your lists, and with some math, basic math, like getting the next tile, is quite simple.
Edit:
You could get the player's next position's tile value with a code something like this:
tileMap[Math.Floor((player.y+playerVelociy.Y)/tileHeight)]
[Math.Floor((player.x+playerVelocity.X)/tileWidth)]
In this code, I assume that the first tile is at 0,0 and you're drawing to right and down. (If not, then just change the Math.Floor to Math.Ceil)
THIS link could help you get the idea, however it's in AS3.0, only the syntax is different.
As title said:
I have form with 2 trackbars. One for frequency and one for amplitude. I set up timer for on-the-fly changing.
private void timer1_Tick(object sender, EventArgs e)
{
float amplitude, frequency;
amplitude = Convert.ToSingle(trackBar1.Value) / 100;
label1.Text = amplitude.ToString() + " V";
frequency = trackBar2.Value;
label2.Text = frequency.ToString() + " Hz";
}
I have also 4 radio-buttons to decide, which type of signal will be displayed (sine, square, triangle, sawthoot)
Now I have this implemented with ImageList (change image of signal).
How can I draw type of signal and regulate it with with trackbars? So it will be like in osciloscope.
Thanks for your answers and code.
Lets start by creating the different signal types, this is a function that creates one wavelength of amplitude 1:
private PointF[] CreateBaseSignal(SignalType signalType)
{
switch (signalType)
{
case SignalType.Sine:
const int oversampling = 32;
PointF[] signal = new PointF[oversampling];
for (int i = 0; i < signal.Length; i++)
{
signal[i].X = (float) i / oversampling;
signal[i].Y = Convert.ToSingle(Math.Sin((double) i / oversampling * 2 * Math.PI));
}
return signal;
case SignalType.Square:
return new PointF[]
{
new PointF(0.0f, -1.0f),
new PointF(0.5f, -1.0f),
new PointF(0.5f, 1.0f),
new PointF(1.0f, 1.0f),
};
case SignalType.Triangle:
return new PointF[]
{
new PointF(0.0f, -1.0f),
new PointF(0.5f, 1.0f),
};
case SignalType.Sawtooth:
return new PointF[]
{
new PointF(0.0f, -1.0f),
new PointF(1.0f, 1.0f),
};
default:
throw new ArgumentException("Invalid signal type", "signalType");
}
}
Then we create the actual signal with the selected amplitude and frequency:
private PointF[] CreateSignal(PointF[] baseSignal, float frequency, float amplitude)
{
PointF[] signal = new PointF[Convert.ToInt32(Math.Ceiling(baseSignal.Length * frequency))];
for(int i = 0; i < signal.Length; i++)
{
signal[i].X = baseSignal[i % baseSignal.Length].X / frequency + (i / baseSignal.Length) / frequency;
signal[i].Y = baseSignal[i % baseSignal.Length].Y * amplitude;
}
return signal;
}
Before attempting to plot this signal to a PictureBox, we scale the signal to fit the width and height:
private PointF[] ScaleSignal(PointF[] signal, int width, int height)
{
const float maximumAmplitude = 10.0f;
PointF[] scaledSignal = new PointF[signal.Length];
for(int i = 0; i < signal.Length; i++)
{
scaledSignal[i].X = signal[i].X * width;
scaledSignal[i].Y = signal[i].Y * height / 2 / maximumAmplitude;
}
return scaledSignal;
}
Using Graphics.DrawLine to plot the signal is way better than Bitmap.SetPixel, since the data points will be connected even at high frequencies. Bitmap.SetPixel is also very slow, you really need to use Bitmap.LockBits and unsafe code for manipulating single pixels to achieve any decent performance. Using Graphics.DrawLine, you also have control over line width, anti-aliasing etc.
Since we have stored the signal in a PointF array, we can use the simple Graphics.DrawLines method to plot the signal instead of iterating over the data points:
private void PlotSignal(PointF[] signal, PictureBox pictureBox)
{
Bitmap bmp = new Bitmap(pictureBox.ClientSize.Width, pictureBox.ClientSize.Height);
signal = ScaleSignal(signal, bmp.Width, bmp.Height); // Scale signal to fit image
using(Graphics gfx = Graphics.FromImage(bmp))
{
gfx.SmoothingMode = SmoothingMode.HighQuality;
gfx.TranslateTransform(0, bmp.Height / 2); // Move Y=0 to center of image
gfx.ScaleTransform(1, -1); // Make positive Y axis point upward
gfx.DrawLine(Pens.Black, 0, 0, bmp.Width, 0); // Draw zero axis
gfx.DrawLines(Pens.Blue, signal); // Draw signal
}
// Make sure the bitmap is disposed the next time around
Image old = pictureBox.Image;
pictureBox.Image = bmp;
if(old != null)
old.Dispose();
}
If you redraw the signal often, you probably want to reuse the the Bitmap and Graphics objects instead of creating new ones each time. Just remember to call Graphics.Clear between each redraw.
Putting everything together in one big statement:
PlotSignal(
CreateSignal(
CreateBaseSignal(signalType),
frequency,
amplitude),
thePictureBox);
If you're after a fast plotting library, I really like Dynamic Data Display
Dynamic Data Display
This is a WPF component, but for fast, smooth drawing applications I really think it is worthwhile to port to WPF sooner rathar than later. It feels like you're not too far into your project at the moment anyway.
Development for WPF seems to have stopped for this component (although it continues to be worked on for Silverlight). The documentation is terrible but the source code is available from the link above so you can extend it as needed (it's quite well written and very extensible) and the source is invaluable as a substitute for the near complete lack of any documentation.
Assuming you want to plot a sin wave on a picture box control, create a picture box control on your form then perform the following:
int width = pictureBox1.Width;
int height = pictureBox1.Height;
Bitmap b = new Bitmap(width, height);
for (int i = 0; i < width; i++)
{
int y = (int)((Math.Sin((double)i * 2.0 * Math.PI / width) + 1.0) * (height - 1) / 2.0);
b.SetPixel(i, y, System.Drawing.Color.Red);
}
pictureBox1.Image = b;